Merge pull request #74 from HueByte/aurion_23-07-2026

[AurionDocs] Full documentation update
This commit is contained in:
Hue
2026-07-23 11:47:30 +02:00
committed by GitHub
144 changed files with 5098 additions and 6498 deletions
+37 -38
View File
@@ -5,53 +5,39 @@
> dependencies, and open the linked per-file docs for behaviour — instead of
> scanning the source tree.
Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files · 648 dependency edges
Commit `40aea9a04b2b4bd3a2e431cdd1cf4e1bfa11c343` · 201 symbols · 128 files · 655 dependency edges
## Subsystems
*Structural clusters detected from the dependency graph — groups of symbols more densely wired to each other than to the rest of the codebase.*
### src/EchoHub.Core/DTOs · ChatService
23 symbols across 14 files. Key symbols (by connectivity):
- [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md) (class) — `src/EchoHub.Server/Services/ChatService.cs`
- [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`UserStatus`](../Code/src/EchoHub.Core/Models/UserStatus.cs.md) (enum) — `src/EchoHub.Core/Models/UserStatus.cs`
- [`EchoHubConnection`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (class) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
- [`ChannelDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`UserPresenceDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
- [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) (class) — `src/EchoHub.Server/Hubs/ChatHub.cs`
- [`IChatService`](../Code/src/EchoHub.Core/Contracts/IChatService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatService.cs`
- *…and 15 more (see symbol-graph.json)*
### src/EchoHub.Client/Services · AppOrchestrator
21 symbols across 16 files. Key symbols (by connectivity):
37 symbols across 23 files. Key symbols (by connectivity):
- [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) (class) — `src/EchoHub.Client/AppOrchestrator.cs`
- [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) (class) — `src/EchoHub.Client/Services/ConnectionManager.cs`
- [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`EchoHubConnection`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (class) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
- [`RefreshToken`](../Code/src/EchoHub.Core/Models/RefreshToken.cs.md) (class) — `src/EchoHub.Core/Models/RefreshToken.cs`
- [`ConnectResult`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) (record) — `src/EchoHub.Client/Services/ConnectionManager.cs`
- [`CommandHandler`](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) (class) — `src/EchoHub.Client/Commands/CommandHandler.cs`
- [`LoginResponse`](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/AuthDtos.cs`
- [`JoinOutcome`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (record) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
- [`ChannelPasswordRequiredException`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (class) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
- *…and 13 more (see symbol-graph.json)*
- [`SavedServer`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
- [`RoomKeyStore`](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) (class) — `src/EchoHub.Client/Services/RoomKeyStore.cs`
- [`IrcMessageFormatter`](../Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md) (class) — `src/EchoHub.Server.Irc/IrcMessageFormatter.cs`
- *…and 29 more (see symbol-graph.json)*
### src/EchoHub.Client/UI · MainWindow
21 symbols across 18 files. Key symbols (by connectivity):
22 symbols across 19 files. Key symbols (by connectivity):
- [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) (class) — `src/EchoHub.Client/UI/MainWindow.cs`
- [`ChatMessageManager`](../Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs`
- [`ChatLine`](../Code/src/EchoHub.Client/UI/Chat/ChatLine.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatLine.cs`
- [`ProfileViewDialog`](../Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
- [`AttachmentKind`](../Code/src/EchoHub.Core/Models/AttachmentKind.cs.md) (enum) — `src/EchoHub.Core/Models/AttachmentKind.cs`
- [`AttachmentDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`ChatColors`](../Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatColors.cs`
- [`ChatSegment`](../Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md) (record) — `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
- [`ChatListSource`](../Code/src/EchoHub.Client/UI/Chat/ChatListSource.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatListSource.cs`
- *…and 13 more (see symbol-graph.json)*
- *…and 14 more (see symbol-graph.json)*
### src/EchoHub.Core/DTOs · ApiClient
@@ -69,7 +55,7 @@ Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files ·
### src/EchoHub.Server · User
17 symbols across 15 files. Key symbols (by connectivity):
19 symbols across 17 files. Key symbols (by connectivity):
- [`User`](../Code/src/EchoHub.Core/Models/User.cs.md) (class) — `src/EchoHub.Core/Models/User.cs`
- [`UsersController`](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) (class) — `src/EchoHub.Server/Controllers/UsersController.cs`
@@ -79,7 +65,7 @@ Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files ·
- [`UserProfileDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
- [`UserService`](../Code/src/EchoHub.Server/Services/UserService.cs.md) (class) — `src/EchoHub.Server/Services/UserService.cs`
- [`IUserService`](../Code/src/EchoHub.Core/Contracts/IUserService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IUserService.cs`
- *…and 9 more (see symbol-graph.json)*
- *…and 11 more (see symbol-graph.json)*
### src/EchoHub.Core/DTOs · ChannelService
@@ -109,17 +95,19 @@ Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files ·
- [`LinkEmbedService`](../Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md) (class) — `src/EchoHub.Server/Services/LinkEmbedService.cs`
- *…and 5 more (see symbol-graph.json)*
### src/EchoHub.Client/Config
### src/EchoHub.Core/Contracts · ChannelDto
7 symbols across 5 files. Key symbols (by connectivity):
9 symbols across 9 files. Key symbols (by connectivity):
- [`RoomKeyStore`](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) (class) — `src/EchoHub.Client/Services/RoomKeyStore.cs`
- [`ClientConfig`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
- [`SavedServer`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
- [`RoomCrypto`](../Code/src/EchoHub.Core/Security/RoomCrypto.cs.md) (class) — `src/EchoHub.Core/Security/RoomCrypto.cs`
- [`ConfigManager`](../Code/src/EchoHub.Client/Config/ConfigManager.cs.md) (class) — `src/EchoHub.Client/Config/ConfigManager.cs`
- [`RoomKeyProtector`](../Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md) (class) — `src/EchoHub.Client/Services/RoomKeyProtector.cs`
- [`AccountPreset`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
- [`ChannelDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`UserPresenceDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
- [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) (class) — `src/EchoHub.Server/Hubs/ChatHub.cs`
- [`IChatService`](../Code/src/EchoHub.Core/Contracts/IChatService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatService.cs`
- [`IChatBroadcaster`](../Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
- [`IrcBroadcaster`](../Code/src/EchoHub.Server.Irc/IrcBroadcaster.cs.md) (class) — `src/EchoHub.Server.Irc/IrcBroadcaster.cs`
- [`SignalRBroadcaster`](../Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md) (class) — `src/EchoHub.Server/Services/SignalRBroadcaster.cs`
- [`IEchoHubClient`](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) (interface) — `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
- *…and 1 more (see symbol-graph.json)*
### src/EchoHub.Server · ServerDirectoryService
@@ -144,6 +132,17 @@ Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files ·
- [`SearchResult`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
- [`IrcNumericReply`](../Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md) (class) — `src/EchoHub.Server.Irc/IrcNumericReply.cs`
### src/EchoHub.Client/UI · UserStatus
6 symbols across 5 files. Key symbols (by connectivity):
- [`UserStatus`](../Code/src/EchoHub.Core/Models/UserStatus.cs.md) (enum) — `src/EchoHub.Core/Models/UserStatus.cs`
- [`StatusDialog`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
- [`UserSession`](../Code/src/EchoHub.Client/Services/UserSession.cs.md) (class) — `src/EchoHub.Client/Services/UserSession.cs`
- [`StatusDialogResult`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
- [`UserDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`UpdateStatusRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
### src/EchoHub.Core/DTOs · ChannelsController
6 symbols across 3 files. Key symbols (by connectivity):
@@ -166,7 +165,7 @@ Commit `4dcb480d1d43e4efaaa9b2e190c7275fcab94d23` · 201 symbols · 128 files ·
- [`MessageType`](../Code/src/EchoHub.Core/Models/MessageType.cs.md) (enum) — `src/EchoHub.Core/Models/MessageType.cs`
- [`ApiResponse`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
*…and 12 smaller subsystems (see symbol-graph.json).*
*…and 11 smaller subsystems (see symbol-graph.json).*
## Most connected symbols
@@ -174,7 +173,7 @@ The load-bearing symbols — changes here have the widest blast radius.
| Symbol | Kind | Used by | Uses | File |
|---|---|---|---|---|
| [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) | class | 3 | 47 | `src/EchoHub.Client/AppOrchestrator.cs` |
| [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) | class | 3 | 54 | `src/EchoHub.Client/AppOrchestrator.cs` |
| [`Program`](../Code/src/EchoHub.Server/Program.cs.md) | file | 0 | 34 | `src/EchoHub.Server/Program.cs` |
| [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) | class | 3 | 29 | `src/EchoHub.Client/Services/ApiClient.cs` |
| [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md) | class | 2 | 29 | `src/EchoHub.Server/Services/ChatService.cs` |
+77 -74
View File
@@ -1,26 +1,22 @@
{
"schema": "auriondocs.agent-graph/v1",
"repository": "HueByte/EchoHub",
"commit": "4dcb480d1d43e4efaaa9b2e190c7275fcab94d23",
"commit": "40aea9a04b2b4bd3a2e431cdd1cf4e1bfa11c343",
"grouping": "communities",
"stats": {
"symbols": 201,
"files": 128,
"edges": 648,
"subsystems": 24
"edges": 655,
"subsystems": 23
},
"subsystems": [
{
"label": "src/EchoHub.Core/DTOs · ChatService",
"symbols": 23
},
{
"label": "src/EchoHub.Client/Services · AppOrchestrator",
"symbols": 21
"symbols": 37
},
{
"label": "src/EchoHub.Client/UI · MainWindow",
"symbols": 21
"symbols": 22
},
{
"label": "src/EchoHub.Core/DTOs · ApiClient",
@@ -28,7 +24,7 @@
},
{
"label": "src/EchoHub.Server · User",
"symbols": 17
"symbols": 19
},
{
"label": "src/EchoHub.Core/DTOs · ChannelService",
@@ -39,8 +35,8 @@
"symbols": 13
},
{
"label": "src/EchoHub.Client/Config",
"symbols": 7
"label": "src/EchoHub.Core/Contracts · ChannelDto",
"symbols": 9
},
{
"label": "src/EchoHub.Server · ServerDirectoryService",
@@ -50,6 +46,10 @@
"label": "src/EchoHub.Client/UI · Channel",
"symbols": 6
},
{
"label": "src/EchoHub.Client/UI · UserStatus",
"symbols": 6
},
{
"label": "src/EchoHub.Core/DTOs · ChannelsController",
"symbols": 6
@@ -71,15 +71,15 @@
"symbols": 5
},
{
"label": "src/EchoHub.Client/Services · IMessageEncryptionService",
"label": "src/EchoHub.Server · ChatService",
"symbols": 4
},
{
"label": "src/EchoHub.Client/UI · InviteCode",
"symbols": 4
"label": "src/EchoHub.Client/Services · PickerOutcome",
"symbols": 3
},
{
"label": "src/EchoHub.Client/Services · FolderPickResult",
"label": "src/EchoHub.Core/Contracts · IMessageEncryptionService",
"symbols": 3
},
{
@@ -90,10 +90,6 @@
"label": "src/EchoHub.Server · ServerStatsReportService",
"symbols": 3
},
{
"label": "src/EchoHub.Server · SpamGuard",
"symbols": 3
},
{
"label": "src/EchoHub.Server.Irc",
"symbols": 3
@@ -116,12 +112,13 @@
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.093,
"inDegree": 3,
"outDegree": 47,
"outDegree": 54,
"doc": "Code/src/EchoHub.Client/AppOrchestrator.cs.md",
"dependsOn": [
"src/EchoHub.Client/Commands/CommandHandler.cs::CommandHandler",
"src/EchoHub.Client/Config/ClientConfig.cs::AccountPreset",
"src/EchoHub.Client/Config/ClientConfig.cs::ClientConfig",
"src/EchoHub.Client/Config/ClientConfig.cs::SavedServer",
"src/EchoHub.Client/Config/ConfigManager.cs::ConfigManager",
"src/EchoHub.Client/Services/AsyncRunner.cs::AsyncRunner",
"src/EchoHub.Client/Services/AudioPlaybackService.cs::AudioPlaybackService",
@@ -130,15 +127,20 @@
"src/EchoHub.Client/Services/ConnectionManager.cs::ConnectionManager",
"src/EchoHub.Client/Services/EchoHubConnection.cs::ChannelPasswordRequiredException",
"src/EchoHub.Client/Services/EchoHubConnection.cs::JoinOutcome",
"src/EchoHub.Client/Services/NativeFolderPicker.cs::NativeFolderPicker",
"src/EchoHub.Client/Services/NativeFolderPicker.cs::PickerOutcome",
"src/EchoHub.Client/Services/NotificationSoundService.cs::NotificationSoundService",
"src/EchoHub.Client/Services/OutgoingAttachment.cs::OutgoingAttachment",
"src/EchoHub.Client/Services/UpdateBackupService.cs::UpdateBackupService",
"src/EchoHub.Client/Services/UpdateChecker.cs::UpdateChecker",
"src/EchoHub.Client/Services/UserSession.cs::UserSession",
"src/EchoHub.Client/Themes/Theme.cs::Theme",
"src/EchoHub.Client/Themes/ThemeManager.cs::ThemeManager",
"src/EchoHub.Client/UI/Chat/ChatMessageManager.cs::ChatMessageManager",
"src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs::AudioPlayerDialog",
"src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs::ChannelPasswordDialog",
"src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs::ConnectDialog",
"src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs::ConnectDialogResult",
"src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs::CreateChannelDialog",
"src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs::ProfileEditDialog",
"src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs::ProfileAction",
@@ -156,6 +158,7 @@
"src/EchoHub.Core/DTOs/ProfileDtos.cs::UpdateProfileRequest",
"src/EchoHub.Core/DTOs/ProfileDtos.cs::UserPresenceDto",
"src/EchoHub.Core/DTOs/ProfileDtos.cs::UserProfileDto",
"src/EchoHub.Core/Models/Attachment.cs::Attachment",
"src/EchoHub.Core/Models/Channel.cs::Channel",
"src/EchoHub.Core/Models/Message.cs::Message",
"src/EchoHub.Core/Models/RefreshToken.cs::RefreshToken",
@@ -204,7 +207,7 @@
"name": "AccountPreset",
"kind": "class",
"file": "src/EchoHub.Client/Config/ClientConfig.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.231,
"inDegree": 2,
"outDegree": 0,
@@ -216,7 +219,7 @@
"name": "ClientConfig",
"kind": "class",
"file": "src/EchoHub.Client/Config/ClientConfig.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.331,
"inDegree": 4,
"outDegree": 3,
@@ -244,9 +247,9 @@
"name": "SavedServer",
"kind": "class",
"file": "src/EchoHub.Client/Config/ClientConfig.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.384,
"inDegree": 5,
"inDegree": 6,
"outDegree": 2,
"doc": "Code/src/EchoHub.Client/Config/ClientConfig.cs.md",
"dependsOn": [
@@ -259,7 +262,7 @@
"name": "ConfigManager",
"kind": "class",
"file": "src/EchoHub.Client/Config/ConfigManager.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.477,
"inDegree": 4,
"outDegree": 2,
@@ -377,7 +380,7 @@
"name": "ClientEncryptionService",
"kind": "class",
"file": "src/EchoHub.Client/Services/ClientEncryptionService.cs",
"subsystem": "src/EchoHub.Client/Services · IMessageEncryptionService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.542,
"inDegree": 2,
"outDegree": 1,
@@ -479,7 +482,7 @@
"name": "EchoHubConnection",
"kind": "class",
"file": "src/EchoHub.Client/Services/EchoHubConnection.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.652,
"inDegree": 2,
"outDegree": 16,
@@ -522,7 +525,7 @@
"name": "RoomLockedException",
"kind": "class",
"file": "src/EchoHub.Client/Services/EchoHubConnection.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.264,
"inDegree": 1,
"outDegree": 0,
@@ -534,7 +537,7 @@
"name": "FolderPickResult",
"kind": "record",
"file": "src/EchoHub.Client/Services/NativeFolderPicker.cs",
"subsystem": "src/EchoHub.Client/Services · FolderPickResult",
"subsystem": "src/EchoHub.Client/Services · PickerOutcome",
"complexity": 0.093,
"inDegree": 2,
"outDegree": 1,
@@ -548,9 +551,9 @@
"name": "NativeFolderPicker",
"kind": "class",
"file": "src/EchoHub.Client/Services/NativeFolderPicker.cs",
"subsystem": "src/EchoHub.Client/Services · FolderPickResult",
"subsystem": "src/EchoHub.Client/Services · PickerOutcome",
"complexity": 0.408,
"inDegree": 0,
"inDegree": 1,
"outDegree": 2,
"doc": "Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md",
"dependsOn": [
@@ -563,9 +566,9 @@
"name": "PickerOutcome",
"kind": "enum",
"file": "src/EchoHub.Client/Services/NativeFolderPicker.cs",
"subsystem": "src/EchoHub.Client/Services · FolderPickResult",
"subsystem": "src/EchoHub.Client/Services · PickerOutcome",
"complexity": 0.026,
"inDegree": 2,
"inDegree": 3,
"outDegree": 1,
"doc": "Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md",
"dependsOn": [
@@ -617,7 +620,7 @@
"name": "RoomKeyProtector",
"kind": "class",
"file": "src/EchoHub.Client/Services/RoomKeyProtector.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.472,
"inDegree": 2,
"outDegree": 1,
@@ -631,7 +634,7 @@
"name": "RoomKeyStore",
"kind": "class",
"file": "src/EchoHub.Client/Services/RoomKeyStore.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.62,
"inDegree": 2,
"outDegree": 6,
@@ -678,7 +681,7 @@
"file": "src/EchoHub.Client/Services/UpdateBackupService.cs",
"subsystem": "src/EchoHub.Client/Services · UpdateBackupService",
"complexity": 0.535,
"inDegree": 3,
"inDegree": 4,
"outDegree": 3,
"doc": "Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md",
"dependsOn": [
@@ -708,7 +711,7 @@
"name": "UserSession",
"kind": "class",
"file": "src/EchoHub.Client/Services/UserSession.cs",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.244,
"inDegree": 1,
"outDegree": 1,
@@ -904,7 +907,7 @@
"file": "src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.425,
"inDegree": 0,
"inDegree": 1,
"outDegree": 1,
"doc": "Code/src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md",
"dependsOn": [
@@ -928,7 +931,7 @@
"name": "ConnectDialog",
"kind": "class",
"file": "src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs",
"subsystem": "src/EchoHub.Client/UI · InviteCode",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.292,
"inDegree": 1,
"outDegree": 4,
@@ -945,9 +948,9 @@
"name": "ConnectDialogResult",
"kind": "record",
"file": "src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs",
"subsystem": "src/EchoHub.Client/UI · InviteCode",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.138,
"inDegree": 2,
"inDegree": 3,
"outDegree": 1,
"doc": "Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md",
"dependsOn": [
@@ -1091,7 +1094,7 @@
"name": "StatusDialog",
"kind": "class",
"file": "src/EchoHub.Client/UI/Dialogs/StatusDialog.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.25,
"inDegree": 1,
"outDegree": 3,
@@ -1107,7 +1110,7 @@
"name": "StatusDialogResult",
"kind": "record",
"file": "src/EchoHub.Client/UI/Dialogs/StatusDialog.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.093,
"inDegree": 1,
"outDegree": 1,
@@ -1330,7 +1333,7 @@
"name": "IChatBroadcaster",
"kind": "interface",
"file": "src/EchoHub.Core/Contracts/IChatBroadcaster.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.602,
"inDegree": 7,
"outDegree": 3,
@@ -1346,7 +1349,7 @@
"name": "IChatService",
"kind": "interface",
"file": "src/EchoHub.Core/Contracts/IChatService.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.625,
"inDegree": 8,
"outDegree": 5,
@@ -1364,7 +1367,7 @@
"name": "IEchoHubClient",
"kind": "interface",
"file": "src/EchoHub.Core/Contracts/IEchoHubClient.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.595,
"inDegree": 4,
"outDegree": 3,
@@ -1380,7 +1383,7 @@
"name": "IMessageEncryptionService",
"kind": "interface",
"file": "src/EchoHub.Core/Contracts/IMessageEncryptionService.cs",
"subsystem": "src/EchoHub.Client/Services · IMessageEncryptionService",
"subsystem": "src/EchoHub.Core/Contracts · IMessageEncryptionService",
"complexity": 0.39,
"inDegree": 11,
"outDegree": 0,
@@ -1499,7 +1502,7 @@
"name": "RegisterRequest",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/AuthDtos.cs",
"subsystem": "src/EchoHub.Client/UI · InviteCode",
"subsystem": "src/EchoHub.Server · User",
"complexity": 0.108,
"inDegree": 2,
"outDegree": 1,
@@ -1513,7 +1516,7 @@
"name": "AttachmentDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · MainWindow",
"complexity": 0.176,
"inDegree": 5,
"outDegree": 1,
@@ -1539,7 +1542,7 @@
"name": "ChannelDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.146,
"inDegree": 16,
"outDegree": 0,
@@ -1564,7 +1567,7 @@
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChannelsController",
"complexity": 0.108,
"complexity": 0.123,
"inDegree": 2,
"outDegree": 0,
"doc": "Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md",
@@ -1575,7 +1578,7 @@
"name": "EmbedDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.123,
"inDegree": 6,
"outDegree": 0,
@@ -1587,7 +1590,7 @@
"name": "JoinChannelResult",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.123,
"inDegree": 2,
"outDegree": 1,
@@ -1601,7 +1604,7 @@
"name": "MessageDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.153,
"inDegree": 20,
"outDegree": 3,
@@ -1629,7 +1632,7 @@
"name": "ReplyRefDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.234,
"inDegree": 4,
"outDegree": 1,
@@ -1679,7 +1682,7 @@
"name": "UserDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ChatDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.123,
"inDegree": 0,
"outDegree": 1,
@@ -1885,7 +1888,7 @@
"name": "UpdateStatusRequest",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ProfileDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.093,
"inDegree": 0,
"outDegree": 1,
@@ -1899,7 +1902,7 @@
"name": "UserPresenceDto",
"kind": "record",
"file": "src/EchoHub.Core/DTOs/ProfileDtos.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.131,
"inDegree": 12,
"outDegree": 2,
@@ -1955,7 +1958,7 @@
"file": "src/EchoHub.Core/Models/Attachment.cs",
"subsystem": "src/EchoHub.Core/Models",
"complexity": 0.467,
"inDegree": 5,
"inDegree": 6,
"outDegree": 2,
"doc": "Code/src/EchoHub.Core/Models/Attachment.cs.md",
"dependsOn": [
@@ -2006,7 +2009,7 @@
"name": "InviteCode",
"kind": "class",
"file": "src/EchoHub.Core/Models/InviteCode.cs",
"subsystem": "src/EchoHub.Client/UI · InviteCode",
"subsystem": "src/EchoHub.Server · User",
"complexity": 0.339,
"inDegree": 7,
"outDegree": 0,
@@ -2100,7 +2103,7 @@
"name": "UserStatus",
"kind": "enum",
"file": "src/EchoHub.Core/Models/UserStatus.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/UI · UserStatus",
"complexity": 0.026,
"inDegree": 21,
"outDegree": 0,
@@ -2112,7 +2115,7 @@
"name": "RoomCrypto",
"kind": "class",
"file": "src/EchoHub.Core/Security/RoomCrypto.cs",
"subsystem": "src/EchoHub.Client/Config",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.679,
"inDegree": 7,
"outDegree": 0,
@@ -2162,7 +2165,7 @@
"name": "IrcBroadcaster",
"kind": "class",
"file": "src/EchoHub.Server.Irc/IrcBroadcaster.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.712,
"inDegree": 1,
"outDegree": 9,
@@ -2260,7 +2263,7 @@
"name": "IrcMessageFormatter",
"kind": "class",
"file": "src/EchoHub.Server.Irc/IrcMessageFormatter.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Client/Services · AppOrchestrator",
"complexity": 0.485,
"inDegree": 2,
"outDegree": 6,
@@ -2596,7 +2599,7 @@
"name": "ChatHub",
"kind": "class",
"file": "src/EchoHub.Server/Hubs/ChatHub.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.597,
"inDegree": 3,
"outDegree": 11,
@@ -2698,7 +2701,7 @@
"name": "ChatService",
"kind": "class",
"file": "src/EchoHub.Server/Services/ChatService.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Server · ChatService",
"complexity": 0.161,
"inDegree": 2,
"outDegree": 29,
@@ -2805,7 +2808,7 @@
"name": "MessageEncryptionService",
"kind": "class",
"file": "src/EchoHub.Server/Services/MessageEncryptionService.cs",
"subsystem": "src/EchoHub.Client/Services · IMessageEncryptionService",
"subsystem": "src/EchoHub.Core/Contracts · IMessageEncryptionService",
"complexity": 0.518,
"inDegree": 1,
"outDegree": 1,
@@ -3013,7 +3016,7 @@
"name": "SignalRBroadcaster",
"kind": "class",
"file": "src/EchoHub.Server/Services/SignalRBroadcaster.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.658,
"inDegree": 1,
"outDegree": 7,
@@ -3033,7 +3036,7 @@
"name": "SpamGuard",
"kind": "class",
"file": "src/EchoHub.Server/Services/SpamGuard.cs",
"subsystem": "src/EchoHub.Server · SpamGuard",
"subsystem": "src/EchoHub.Server · ChatService",
"complexity": 0.656,
"inDegree": 3,
"outDegree": 6,
@@ -3052,7 +3055,7 @@
"name": "SpamVerdict",
"kind": "record",
"file": "src/EchoHub.Server/Services/SpamGuard.cs",
"subsystem": "src/EchoHub.Server · SpamGuard",
"subsystem": "src/EchoHub.Server · ChatService",
"complexity": 0.122,
"inDegree": 1,
"outDegree": 1,
@@ -3066,7 +3069,7 @@
"name": "SpamVerdictKind",
"kind": "enum",
"file": "src/EchoHub.Server/Services/SpamGuard.cs",
"subsystem": "src/EchoHub.Server · SpamGuard",
"subsystem": "src/EchoHub.Server · ChatService",
"complexity": 0.026,
"inDegree": 4,
"outDegree": 0,
@@ -3217,7 +3220,7 @@
"name": "FakeChatService",
"kind": "class",
"file": "src/EchoHub.Tests/Irc/TestHelpers.cs",
"subsystem": "src/EchoHub.Core/DTOs · ChatService",
"subsystem": "src/EchoHub.Core/Contracts · ChannelDto",
"complexity": 0.695,
"inDegree": 0,
"outDegree": 6,
@@ -3236,7 +3239,7 @@
"name": "FakeEncryptionService",
"kind": "class",
"file": "src/EchoHub.Tests/Irc/TestHelpers.cs",
"subsystem": "src/EchoHub.Client/Services · IMessageEncryptionService",
"subsystem": "src/EchoHub.Core/Contracts · IMessageEncryptionService",
"complexity": 0.423,
"inDegree": 0,
"outDegree": 1,
+3 -3
View File
@@ -1,7 +1,7 @@
# HueByte/EchoHub — Documentation
*Generated from branch `master`*
*Commit: `4dcb480d`*
*Commit: `40aea9a0`*
## Languages
@@ -11,8 +11,8 @@
- **Files processed:** 128
- **Lines of code:** 22,133
- **Symbols documented:** 598
- **Validation retries:** 77
- **Symbols documented:** 617
- **Validation retries:** 69
## Documentation Index
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -20,15 +20,24 @@ public class AccountPreset
```
AccountPreset is a lightweight data container that groups three optional account identity properties—DisplayName, Bio, and NicknameColorso callers can apply or persist a predefined persona for an account. It is intended for use in client configuration (ClientConfig.cs), enabling a consistent, reusable identity profile to be attached to account-related logic.
AccountPreset is a lightweight data container that groups optional account presentation attributes used by client configuration. It encapsulates a DisplayName, Bio, and NicknameColor so a named preset can be stored, transferred, or reapplied as a unit to influence how an account is presented in the UI.
## Remarks
AccountPreset exists to keep related identity attributes together, reducing the surface area of APIs that need to accept or propagate persona data. It aligns with a configuration/templating pattern in the client, making it easier to serialize, store, and reuse account personas across components that render or modify user identity.
This type exists to package related display properties together, enabling reuse and persistence of account presentation presets. Since all properties are nullable, consumers can merge a preset with existing data and only override the attributes that are explicitly set.
## Example
```csharp
var preset = new AccountPreset
{
DisplayName = "Nova",
Bio = "Exploring the stars of code",
NicknameColor = "#1E90FF"
};
```
## Notes
- All properties are nullable; callers must define default behavior when a property is null (e.g., preserve existing values or apply a fallback).
- Null-valued properties may be serialized depending on the chosen serializer; configure to ignore nulls if you prefer a clean configuration payload.
- There is no validation here; enforce constraints instead in the surrounding configuration or UI logic.
- Null properties indicate that the corresponding attribute should not override any existing value when applying the preset to an existing account.
---
@@ -41,31 +50,10 @@ public class ClientConfig
```
ClientConfig is the central container for a user's preferences and runtime state in the EchoHub client. It aggregates saved servers, the active account preset, the UI theme, notification settings, and attachment-handling options such as the download path and ASCII-rendering size.
ClientConfig is a simple data container that groups the clients preferences and runtime settings into a single object. It includes the list of configured servers (`SavedServers`), the default account preset (`DefaultPreset`), the currently selected theme (`ActiveTheme`), and the notification configuration (`Notifications`). It also carries optional application paths and rendering settings: `DownloadPath` specifies where attachments are saved (null means use the OS Downloads folder), and `DefaultAsciiSize` selects the ASCII-art rendering size for attached images (values 's', 'm', or 'l', defaulting to 'm').
## Remarks
It acts as a single source of truth for components that configure server connectivity, UI theming, and how attachments are stored and rendered. Centralizing defaults and user-specific values reduces duplication and helps ensure consistent behavior across sessions and test environments.
## Example
```csharp
var config = new ClientConfig
{
SavedServers = new List<SavedServer>
{
new SavedServer { Name = "Work", Url = "https://work.example", RememberMe = true }
},
DownloadPath = @"C:\Downloads",
DefaultAsciiSize = "m"
};
```
## Notes
- DownloadPath being null means attachments and saved images go to the OS Downloads folder. Ensure the application has write permissions to that location when relying on the default.
- DefaultAsciiSize accepts "s" (40×40), "m" (80×80), or "l" (120×120). This size applies to copy-paste/drag-drop attachments that do not carry a per-file size flag.
ClientConfig centralizes user preferences and runtime settings, so components can rely on a single source of truth for initialization, persistence, and UI decisions. It folds server configuration (`SavedServers`) together with user-facing settings like the default preset (`DefaultPreset`), the active theme (`ActiveTheme`), and notification behavior (`Notifications`), reducing coupling between subsystems. By exposing `DownloadPath` and `DefaultAsciiSize`, it also captures file-management and rendering preferences that affect attachments across the app.
---
@@ -78,28 +66,10 @@ public class NotificationConfig
```
NotificationConfig is a lightweight data container used by the EchoHub client to express how notifications should behave. It encapsulates three related knobs: Enabled, Volume, and SoundFile. Developers instantiate this class to configure or override the client's notification behavior when wiring up configuration (for example, within ClientConfig) or when configuring the notifier component. The defaults indicate that notifications are enabled by default, a modest default volume, and no custom sound file unless specified.
The `NotificationConfig` class is a small, strongly-typed container for notification playback settings used by the client. It exposes `Enabled`, `Volume`, and an optional `SoundFile` to customize sound behavior. By default, `Enabled` is `true`, `Volume` is `30`, and `SoundFile` is unset, making it ready to bind from configuration sources.
## Remarks
By grouping notification-related settings into a single object, NotificationConfig reduces coupling between components that render or play notification sounds and the rest of the configuration. It also provides a clean extension point: new knobs can be added in the future without scattering settings across call sites, since a single configuration object can be passed around.
## Example
```csharp
var config = new NotificationConfig
{
Enabled = true,
Volume = 40,
SoundFile = "assets/notify.wav"
};
```
## Notes
- Volume is stored as a byte (0255). If your UI operates in a 0100 range, map or clamp values appropriately before consumption.
- SoundFile is nullable; when it is null, the consumer should handle the absence of a custom sound (e.g., fall back to a default sound or skip audible notification based on the environment).
This is a lightweight configuration object that decouples notification behavior from business logic and supports binding from JSON or other configuration providers. It keeps the surface minimal while making it easy to override defaults without code changes.
---
@@ -112,13 +82,34 @@ public class SavedServer
```
SavedServer is a client-side representation of a per-server configuration and its associated local state for the EchoHub client. It stores credentials and connection details (Name, Url, Username, RefreshToken), a RememberMe flag, and the last connection timestamp (LastConnected). It also holds per-channel state that remains on the client: ChannelKeys (end-to-end encrypted keys cached per channel), LeftChannels (channels the user explicitly left), and LastReadMessages (per-channel read markers). These keys live only on the user's machine; the server never sees them.
SavedServer is a client-side representation of a configured server for the EchoHub client. It aggregates the server identity (Name and Url), optional user credentials (Username and RefreshToken), user preferences (RememberMe), and per-server state needed to restore a session across restarts. Notably, it includes per-channel encryption state (ChannelKeys), channel-level navigation state (LeftChannels), and per-channel read-tracking (LastReadMessages). These members are stored locally and are not exposed to the server; the server never sees the encryption keys, which are encrypted at rest and scoped to the local machine (see [`RoomKeyProtector`](../Services/RoomKeyProtector.cs.md)). At startup, the client can deserialize this object to rehydrate connections, rejoin channels (excluding those the user explicitly left), and persist unread counts and mentions across restarts.
## Remarks
SavedServer acts as the single source of truth for a user's relationship to a particular server within the client. By keeping ChannelKeys and LastReadMessages client-side, the app can decrypt and present channel content and maintain read state even after restarts, without leaking sensitive information to the server. LeftChannels honors user intent by preventing auto-joining of channels the user has consciously left, until they rejoin. This abstraction fits alongside other per-server configuration objects and collates server identity, credentials, and per-channel metadata for efficient session restore and UX.
The `SavedServer` acts as a simple data container that binds together server identity, user identity (when supplied), and user-driven state that enhances the reconnect experience. It sits at the boundary between the persistence layer and the networking layer: serialization of this object enables quick restoration of a user session without re-issuing authentication or resynchronizing channel state. The `ChannelKeys` field, in particular, represents sensitive data tied to end-to-end encrypted channels and is kept on the client; its lifecycle is intentionally scoped to the users device and is managed with the same care prescribed for the `RefreshToken`.
## Example
```csharp
var server = new SavedServer
{
Name = "EchoHub",
Url = "https://echo.example",
Username = "alice",
RememberMe = true,
LastConnected = DateTimeOffset.UtcNow,
ChannelKeys = new Dictionary<string, string>
{
{ "general", "base64encryptedKeyHere" }
},
LeftChannels = new List<string> { "old-channel" },
LastReadMessages = new Dictionary<string, string>
{
{ "general", "12345" }
}
};
```
## Notes
- Sensitive data such as RefreshToken and ChannelKeys should be stored securely at rest; the server never holds these values.
- These collections are mutable; ensure proper synchronization if accessed from multiple threads to avoid data races or inconsistent state.
- Treat `ChannelKeys` as sensitive data: avoid logging them or exposing them to the UI; ensure at-rest encryption via the clients security model. The keys are stored only on the client device and are not sent to `server` endpoints.
- This class is intended as a plain data carrier (DTO) used by the persistence and connection layers; do not embed domain logic here. When upgrading or migrating fields, consider versioning in the surrounding storage layer to preserve compatibility.
---
@@ -8,14 +8,18 @@ public static class ConfigManager
```
ConfigManager provides a thread-safe, single-point API for loading and persisting the client's configuration to disk. Use Load to read the current ClientConfig and Save/SaveServer/RemoveServer to apply changes from the UI or background tasks (for example, after token refreshes or updating saved servers).
ConfigManager is a static helper that persists the client configuration to a JSON file under the user's profile directory and provides focused APIs for loading, saving, and managing saved servers. It centralizes file I/O behind a private lock to serialize access from UI actions and background tasks (token refresh, room keys, last-read checkpoints), helping prevent race conditions that could corrupt the config.
Use `ConfigManager.Load()` to obtain the current configuration (or a default [`ClientConfig`](ClientConfig.cs.md) when the file is missing or unreadable), modify the returned object, and persist changes with `ConfigManager.Save(config)`.
To manage saved servers, use `ConfigManager.SaveServer(...)` to upsert by `Url` and `ConfigManager.RemoveServer(string url)` to delete by `Url` (case-insensitive).
## Remarks
ConfigManager stores the configuration in a JSON file named config.json inside a per-user directory (.echohub) under the current user's profile. All file I/O is serialized with a private lock (FileLock) to prevent concurrent access from UI threads and background tasks. When you call SaveServer, the code locates an existing SavedServer by URL (case-insensitive) and updates it, or appends a new one if none exists; RemoveServer deletes entries by URL. The design uses best-effort error handling—exceptions are swallowed to avoid disrupting the app—but this means persistence failures are not surfaced to callers unless they implement their own checks.
All file I/O performed by `ConfigManager` is guarded by a single static lock (the private `Lock` named `FileLock`), ensuring reads and writes do not interleave across threads. The design favors resilience: a missing or unreadable config yields a fresh [`ClientConfig`](ClientConfig.cs.md), and save errors are swallowed to avoid crashing the host process. When upserting or removing saved servers, the code compares the server URLs using a case-insensitive match (`StringComparison.OrdinalIgnoreCase`), so entries differing only by casing do not duplicate and removals reliably locate targets.
## Notes
- Persistence operations swallow all exceptions, making failures non-fatal but potentially leading to invisible data loss.
- SavedServers are deduplicated by URL using a case-insensitive comparison; updating an existing URL won't create a duplicate.
- ConfigDir uses Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); on systems where a user profile is unavailable or access is restricted, initialization may fall back to a default path.
- Saves are best-effort; any exception during persistence is swallowed so callers should not depend on hard failures for user feedback.
- If the config file is absent, the directory is created and a default [`ClientConfig`](ClientConfig.cs.md) is used when loading.
- URL-based operations for saved servers use case-insensitive matching to maintain a consistent, deduplicated set.
@@ -4,12 +4,12 @@
> **Kind:** file
The Program file serves as the application's entry point and startup bootstrap for the EchoHub client. It coordinates early startup tasks such as rollback handling, permission checks, configuration provisioning, logging setup, PATH preparation, post-update cleanup, and UI initialization before handing control to the main orchestrator and theme system.
The `Program` file serves as the entry point for the EchoHub client. It bootstraps startup by handling a potential CLI rollback (`--rollback`), performing a best-effort Unix execute-permission check, provisioning configuration (loading from `appsettings.json` with a fallback embedded resource at `EchoHub.Client.appsettings.example.json`), and configuring `Serilog` from the configuration before loading the runtime settings via [`ConfigManager`](Config/ConfigManager.cs.md) and initializing the Terminal.Gui UI with `Application.Create().Init()`.
## Remarks
It functions as a central bootstrap that hides cross-cutting concerns from downstream components. By coordinating UpdateBackupService for rollback support, PathSetup for PATH hygiene, and ThemeManager for theming, it decouples startup sequencing from the rest of the application and ensures the runtime begins in a well-defined state.
This file centralizes environment preparation and startup orchestration, encapsulating cross-platform concerns (rollback handling, permission checks, path setup, and post-update housekeeping) so the rest of the application can assume a ready, consistent runtime context. It also exposes a clear, testable bootstrap path that wires configuration, logging, and the UI startup in a single phase, reducing duplication across modules.
## Notes
- Rollback path exits the process after attempting a restore; normal startup does not proceed.
- If appsettings.json is missing, the code seeds it from an embedded example; if the resource isn't available, startup continues with defaults.
- Several operations are best-effort and exceptions are swallowed to avoid stopping startup (e.g., Unix permissions adjustments, cleanup of a leftover .old executable).
- Rolling back can terminate startup early because `UpdateBackupService.RestoreBackup()` or subsequent error paths invoke `Environment.Exit`.
- Unix permission checks are best-effort and any failures are swallowed to avoid blocking startup on platform quirks.
- The initial configuration may be sourced from an embedded resource (`EchoHub.Client.appsettings.example.json`) if `appsettings.json` is absent, providing a safe fallback during first-run scenarios.
File diff suppressed because it is too large Load Diff
@@ -8,11 +8,14 @@ public static class AsyncRunner
```
Eliminates repeated Task.Run/try/catch/app.Invoke(ShowError) boilerplate by consolidating the common pattern of running background work and surfacing errors to the UI. It runs the provided async work on a background thread and routes any exceptions to the UI thread for user notification.
Runs the provided asynchronous work on a background thread and routes exceptions to the UI, eliminating boilerplate like `Task.Run`/try/catch/`app.Invoke(ShowError)`.
`AsyncRunner.Run` takes an `IApplication` (`app`), a `Func<Task>` representing the work, an `Action<string>` (`showError`), a string (`errorPrefix`) used in the user-facing error, and an optional `string? logContext` to enrich logs; if an exception occurs, it logs with `Log.Error` and invokes the UI thread to display the error via `showError`.
This pattern centralizes background execution and UI-error reporting, so callers need only supply the work and error message components and can rely on consistent logging and user feedback.
## Remarks
AsyncRunner encapsulates a cross-cutting concern: performing asynchronous work without blocking the UI and centralizing error reporting. It uses Task.Run to execute work off the calling thread and app.Invoke to marshal the error surface back to the UI. When an exception occurs, it logs the failure with the provided context (logContext if supplied, otherwise errorPrefix) and shows a UI message using showError prefixed by errorPrefix. Because Run is fire-and-forget (it returns void), callers should not rely on it for completion or exception propagation; choose a different pattern if you need to observe results.
This abstraction isolates the cross-cutting concerns of background execution and UI error presentation. By encapsulating this pattern, it avoids duplicating boilerplate across call sites and ensures errors are logged with contextual information and surfaced on the UI thread via `IApplication.Invoke`.
## Notes
- This method is fire-and-forget; exceptions are caught and surfaced but not propagated to the caller.
- The UI update and logging rely on the provided IApplication and showError delegate; ensure they are safe to call from a background thread; app.Invoke is used to marshal to the UI thread.
- This method is fire-and-forget: it launches the work and does not return a `Task`; callers cannot await completion or observe exceptions from the caller's context. If you need completion signaling, consider returning a `Task` or providing a completion callback.
@@ -8,17 +8,12 @@ public class AudioPlaybackService
```
AudioPlaybackService is a thread-safe wrapper around an underlying audio player that exposes asynchronous playback controls and a finished event surface. Use it when you need serialized access to play, pause, resume, or stop audio and a consistent event interface without managing locks and state machines yourself.
AudioPlaybackService provides a thread-safe, asynchronous facade for audio playback using a private `Player` instance. It exposes the playback state via `IsPlaying` and `IsPaused`, and it forwards a `PlaybackFinished` event when the underlying `Player` completes playback. All public operations are serialized with a private `SemaphoreSlim` named `_lock` to prevent concurrent access to the player. When you call `PlayAsync`, if something is already playing it stops it before starting the new file; `PauseAsync`, `ResumeAsync`, and [`StopAsync`](../../EchoHub.Server/Services/ServerDirectoryService.cs.md) similarly acquire the lock, perform the appropriate operation if possible, and log any exceptions with `Log.Warning`. Volume is controlled via `SetVolumeAsync`, which clamps the requested volume to a maximum of 100 using `Math.Min`.
## Remarks
To prevent concurrent calls from interfering with playback state, the class serializes all operations using a SemaphoreSlim. When PlayAsync is invoked while something is already playing, it stops the current track before starting the new file; PauseAsync, ResumeAsync, and StopAsync perform their actions only when appropriate states are detected. The PlaybackFinished event is forwarded from the internal player, so callers can react to completion without depending on the concrete implementation of the _player. Exceptions raised by the underlying player are caught and logged with a warning, ensuring playback issues do not crash the application.
## Example
```csharp
// Example usage
var audio = new AudioPlaybackService();
await audio.PlayAsync("path/to/file.mp3");
```
This abstraction centralizes concurrency concerns and error handling around audio playback. By bridging the `Player` with a single, serialized surface, it reduces race conditions when multiple callers request playback from different parts of the application. The `PlaybackFinished` event provides a clean notification channel to consumers without exposing the internal player, enabling a decoupled UI or service layer to react to completion.
## Notes
- This wrapper serializes calls to avoid race conditions; however, it is not cancellation-aware. If you need to cancel an in-flight operation, extend the class with cancellation support or a dedicated cancellation mechanism.
- Exceptions during playback operations are swallowed after being logged with `Log.Warning`, so callers do not observe crashes but must rely on the logs to diagnose issues.
- All playback-related methods acquire the `_lock` semaphore, meaning long-running operations inside any call can block other playback requests and should be kept短-lived to avoid contention.
- `SetVolumeAsync` caps the volume at 100 via `Math.Min`, ensuring the underlying player never receives an out-of-range value.
@@ -8,26 +8,7 @@ internal static class AvatarHelper
```
AvatarHelper centralizes the shared logic for uploading avatars by accepting either a local file path or an HTTP(S) URL, resolving the input to a stream, and uploading it via ApiClient.UploadAvatarAsync. It returns the ASCII art response from the server, providing a straightforward way to obtain the server-side representation of the uploaded avatar without duplicating local-file or network-handling code.
AvatarHelper provides a single entry point to upload an avatar from either a local file path or a remote URL by converting the target into a `Stream`, then delegating the actual upload to `ApiClient.UploadAvatarAsync`. It abstracts away the file I/O and HTTP fetch logic, ensuring callers don't need to manage streams or HTTP requests themselves. It returns the server's ASCII art response as a `string?` and guarantees the `Stream` is disposed after the upload.
## Remarks
By supporting both local and remote sources behind a single UploadAsync entry point, AvatarHelper hides the mechanics of data retrieval and stream management from call sites and ensures consistent disposal of the stream. The actual upload is delegated to ApiClient, keeping concerns separated between data acquisition and server interaction. The class is internal, reinforcing its role as a reusable utility within the client layer rather than a public API.
The method propagates errors from file access, HTTP fetch, or the server upload to the caller, which is appropriate for a small, focused helper that prioritizes simplicity over internal retries or resilience policies.
## Example
```csharp
// Example usage within the same assembly
var client = new ApiClient("https://api.example.org");
string? artFromFile = await AvatarHelper.UploadAsync(client, @"C:\avatars\user.png");
string? artFromUrl = await AvatarHelper.UploadAsync(client, "https://example.org/avatars/user.png");
```
## Notes
- Creating a new HttpClient per invocation can lead to socket exhaustion in high-throughput scenarios; consider reusing a shared HttpClient instance or using HttpClientFactory in production code.
- If the local path does not exist, a FileNotFoundException is thrown.
- When targeting a URL, if the URL's file name is missing or lacks an extension, the code defaults to using avatar.png as the upload file name.
- Exceptions from the HTTP request or the server upload propagate to the caller; there is no retry logic within this helper.
AvatarHelper isolates avatar uploading behind a focused API, so higher-level code doesn't need to know whether the source is a local file or a URL. It accepts either a local path or an HTTP(S) URL, resolves a valid `fileName` (defaulting to `avatar.png` when the URL doesn't supply one), and streams the content to `ApiClient.UploadAvatarAsync`. The helper ensures proper resource management by disposing the `Stream` after the upload, and it centralizes the cross-cutting concern of avatar uploads to a single place.
@@ -8,28 +8,24 @@ public sealed class ClientEncryptionService : IMessageEncryptionService
```
ClientEncryptionService provides client-side encryption for messages by applying AES-256-GCM using a key supplied by the server. It mirrors the servers encryption format so messages are encrypted end-to-end between client and server. When no key has been set, Encrypt is a no-op and returns the plaintext to preserve compatibility with unauthenticated flows; once initialized, Encrypt produces a prefixed, base64-encoded payload containing the nonce and ciphertext+tag, and Decrypt reverses this process. If decryption fails due to a missing or mismatched key or corrupted data, a sentinel message is returned to indicate the failure and prompt re-authentication to refresh the key.
ClientEncryptionService implements client-side encryption using AES-256-GCM to protect messages before sending them to the server, aligning with the server's ciphertext format so decryption occurs only with the shared key. After you provide a base64-encoded key via `SetKey`, it encrypts plaintext by generating a fresh 12-byte nonce and a 16-byte authentication tag, returning a string that starts with the `EncryptionPrefix` and includes base64-encoded nonce and payload; if no key has been set (`_key` is null), `Encrypt` returns the plaintext unchanged.
## Remarks
This abstraction isolates cryptography behind a single, testable service that can be swapped or disabled without changing business logic. It enforces a clear security boundary: encryption only happens after a server-provided key is loaded, reducing the risk of leaking plaintext. The pre-key pass-through behavior preserves compatibility with existing flows during login or in environments where the key has not yet been fetched.
This class hides cryptography behind the [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) contract, offering a simple, predictable API for encryption and decryption while keeping key material private. It ensures that only a server-provisioned key enables encryption, and it produces self-contained ciphertext that carries its nonce and tag so the server can decrypt it reliably. The design also provides nullable-friendly helpers (`EncryptNullable`, `DecryptNullable`) to gracefully handle missing values.
## Example
```csharp
// Example: encrypt and decrypt with a server-provided key
var client = new ClientEncryptionService();
// Create a 32-byte key for demonstration (replace with real server-provided key)
var keyBytes = new byte[32];
var base64Key = Convert.ToBase64String(keyBytes);
client.SetKey(base64Key);
// Example usage of client-side encryption
var encryption = new ClientEncryptionService();
string base64Key = "<32-byte-base64-key>";
encryption.SetKey(base64Key);
string plaintext = "Secret message";
string encrypted = client.Encrypt(plaintext);
string decrypted = client.Decrypt(encrypted);
// decrypted should equal plaintext
string ciphertext = encryption.Encrypt(plaintext);
string decrypted = encryption.Decrypt(ciphertext);
```
## Notes
- Encrypt and Decrypt only work after a 32-byte key has been provided via SetKey; otherwise Encrypt returns plaintext and Decrypt returns content unchanged.
- If the encrypted content is tampered with, the key is wrong, or the payload is malformed, Decrypt returns the special placeholder: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
- The key is held in memory and is not rotated automatically; ensure proper key management and re-fetch after key rotation on the server.
- Encrypt before calling `SetKey` is a no-op: the input plaintext is returned unchanged when `_key` is null.
- Decrypt returns the original content if `_key` is null or the input does not start with the expected `EncryptionPrefix`.
- `SetKey` enforces a 32-byte (256-bit) key length and throws `InvalidOperationException` if the length is not exactly 32 bytes.
- Decryption errors are handled gracefully; if decryption fails for any reason, a sentinel message is returned: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
@@ -8,25 +8,20 @@ public static class ClipboardFiles
```
ClipboardFiles reads file paths from the clipboard when the clipboard contains a file-list (such as after copying files in Explorer/Finder). Use TryGetFiles to retrieve those paths so you can attach copied files directly without pasting textual paths; this works on Windows and Linux, while macOS and other platforms do not expose a file-list clipboard.
ClipboardFiles reads the OS clipboard to obtain a list of files when the clipboard holds a file-list (such as after copying files in a file manager). This enables scenarios where a copied set of files can be pasted or attached directly, without requiring the user to paste raw text paths. Call `TryGetFiles` to retrieve existing file paths from the clipboard; the method returns true when one or more valid paths are found, and false otherwise (including on platforms without file-list clipboard support).
## Remarks
ClipboardFiles encapsulates platform differences behind a single API. It isolates Windows-specific CF_HDROP handling and Linux's text/uri-list retrieval, performing path existence checks and filtering out non-file entries to return a clean list of existing paths. It returns true only when at least one file is found; otherwise false, letting callers gracefully fall back to other input methods.
This helper abstracts away platform differences in clipboard formats and presents a single, cohesive API for retrieving file lists from the clipboard. On Windows it enumerates files via the CF_HDROP channel and returns the paths that point to existing files. On Linux it reads a `text/uri-list` from the clipboard (via `wl-paste` or `xclip`), converts `file://` URLs to local paths, and keeps only paths that exist. The implementation favors a graceful failure path: any read-time exception is logged and the caller simply receives a non-success result, allowing callers to degrade gracefully without crashing. The API design emphasizes a simple success/failure boolean along with a concrete list of files, enabling straightforward integration into UX flows that want to treat copied files as attachable entities rather than plain text.
## Example
```csharp
if (ClipboardFiles.TryGetFiles(out var files))
{
Console.WriteLine($"Clipboard contains {files.Count} file(s): {string.Join(", ", files)}");
}
else
{
Console.WriteLine("Clipboard does not contain a file-list or contains only non-existent paths.");
foreach (var path in files)
Console.WriteLine(path);
}
```
## Notes
- Returns only existing files; non-existent or inaccessible paths are ignored.
- Windows implementation relies on CF_HDROP with a brief retry loop to tolerate clipboard contention.
- Linux implementation uses wl-paste or xclip (one must be available for success).
- macOS and other platforms do not provide file-list clipboard support.
- macOS and other non-supported platforms do not provide a file-list clipboard, so `TryGetFiles` returns false there.
- The method only returns paths that actually exist on disk; non-existent or malformed clipboard entries are ignored, and an empty result yields false.
@@ -8,22 +8,15 @@ public static class ClipboardImage
```
Reads raw image data from the OS clipboard and returns PNG-encoded bytes suitable for saving, embedding, or transmitting. Use this when you need a single, consistent PNG representation of whatever image the user has copied (browser-copied PNGs, screenshots, editor bitmaps) so callers don't need per-OS or per-format handling.
Reads raw image bytes from the platform clipboard and returns them as a PNG byte array when available. Use `ClipboardImage.TryGetPng` when you need a canonical, pasteable PNG representation of whatever image the user has on the clipboard (for example, when accepting pasted screenshots or images in a terminal or chat input that cannot accept raw bitmap data).
## Remarks
This class normalizes multiple clipboard image formats into PNG. It prefers native clipboard PNG formats when available (preserving transparency) and falls back to platform clipboard bitmaps (CF_DIB on Windows) by wrapping the DIB bytes in a minimal BMP file header and decoding/re-encoding them as PNG. TryGetPng routes to OS-specific helpers and catches/logs errors, returning false on failure rather than throwing.
`ClipboardImage` centralizes platform-specific clipboard handling: `TryGetPng` dispatches to `TryGetWindows`, `TryGetLinux`, or `TryGetMacOS` depending on `OperatingSystem` checks, and normalizes all outputs to PNG. When the clipboard format already contains PNG bytes (detected using the `PngMagic` signature or platform-registered PNG formats such as those discovered via `RegisterClipboardFormatW` on Windows), the bytes are passed through to preserve fidelity and transparency. When the clipboard exposes a DIB/bitmap (`CfDib` on Windows), the `DibToPng` helper builds a minimal BMP wrapper around the DIB bytes, decodes it with `Image.Load`, and re-encodes the result as PNG; this covers screenshots and editors that expose only device-independent bitmaps.
## Example
```csharp
// Save whatever image is on the clipboard to a file named clipboard.png
if (ClipboardImage.TryGetPng(out var png))
{
System.IO.File.WriteAllBytes("clipboard.png", png);
}
```
The class intentionally swallows and logs exceptions (via `Log.Warning`) from clipboard access and image decoding so callers get a simple success/failure result from `TryGetPng` instead of propagating clipboard or image-library exceptions.
## Notes
- DibToPng returns null for malformed or undecodable DIB input; TryGetPng propagates that as a failure (false).
- The implementation prefers registered PNG clipboard formats to preserve alpha; CF_DIB bitmaps are re-encoded and may lose or change metadata.
- Re-encoding a bitmap to PNG allocates memory and does CPU work; callers should avoid doing this in a tight loop.
- TryGetPng checks the platform (Windows/Linux/macOS) and will return false on unsupported platforms; failures are logged rather than thrown.
- Clipboard APIs are platform and threading sensitive. On Windows the OS clipboard typically requires running on an STA thread; calling `TryGetPng` from a non-STA thread may fail or return false. Ensure clipboard access is performed on an appropriate thread context for the platform.
- `DibToPng` validates the DIB header (minimum 40 bytes, header size bounds) and returns null for malformed input. Decoding can still fail at `Image.Load` for unsupported or corrupted bitmaps; such failures are logged and surface as a failure to `TryGetPng`.
- Re-encoding a DIB to PNG may not preserve alpha/transparency if the original bitmap format lacks alpha channels (DIB/CF_DIB often does not include alpha). If preserving exact alpha semantics is required, prefer sources that supply native PNG clipboard formats when possible.
- Converting clipboard data allocates buffers (the BMP wrapper and the resulting PNG byte array) and performs image decode/encode work; callers should expect a non-trivial CPU and memory cost for large images.
@@ -18,16 +18,15 @@ internal sealed class ConnectionManager : IAsyncDisposable
```
Manages the full lifecycle of a live chat connection: authenticating with the server, establishing end-to-end encryption keys, creating and wiring the SignalR (EchoHub) connection, tracking joined channels, and exposing a thin event surface that the UI (AppOrchestrator) can subscribe to. Use this when you want a single, high-level component to own connection state and SignalR event forwarding instead of manipulating ApiClient and EchoHubConnection directly.
Manages a server connection end-to-end: handles authentication via [`ApiClient`](ApiClient.cs.md), establishes end-to-end encryption, creates and wires an [`EchoHubConnection`](EchoHubConnection.cs.md), tracks joined channels, and exposes SignalR events so higher-level orchestrators can react without touching connection internals. Reach for `ConnectionManager` when you want UI code (for example an [`AppOrchestrator`](../AppOrchestrator.cs.md)) to observe connection and chat events through simple events rather than managing [`ApiClient`](ApiClient.cs.md) and [`EchoHubConnection`](EchoHubConnection.cs.md) yourself.
## Remarks
This class centralizes the responsibilities that would otherwise be scattered across UI code: authentication and token rotation, attempting to fetch and apply the E2E encryption key, instantiating and wiring an EchoHubConnection, and keeping track of which channels have been joined. It forwards SignalR events as simple .NET events so the UI layer can react without needing to know SignalR details. ConnectionManager also implements IAsyncDisposable so callers can cleanly tear down both the EchoHubConnection and the underlying ApiClient.
`ConnectionManager` centralizes lifecycle concerns: it authenticates (login/registration/refresh), subscribes to token rotation, attempts to fetch and apply the E2E encryption key, constructs and registers handlers on the [`EchoHubConnection`](EchoHubConnection.cs.md), and ensures channel membership state is tracked. It forwards the hub's runtime events (for example `MessageReceived`, `UserJoined`, `ChannelUpdated`) so callers receive high-level notifications and do not need to bind SignalR handlers directly. The class is intended as the single place that composes [`ApiClient`](ApiClient.cs.md), [`ClientEncryptionService`](ClientEncryptionService.cs.md)/[`RoomKeyStore`](RoomKeyStore.cs.md), and [`EchoHubConnection`](EchoHubConnection.cs.md) into a usable connection for the UI.
## Notes
- ConnectionManager may raise forwarded events from background threads (SignalR callbacks). UI handlers should marshal to the UI thread if required by the UI framework.
- ConnectAsync reports progress via the onStatus callback and will throw on authentication failure; callers are expected to handle expired saved sessions or retry logic.
- Failure to fetch the encryption key is treated as non-fatal: the manager logs a warning and proceeds without message encryption.
- Dispose of the manager (DisposeAsync) when the app shuts down to ensure the hub connection and ApiClient are cleaned up.
- `ConnectAsync` reports progress via the `onStatus` callback and will throw on authentication failure — callers are expected to handle saved-session expiry and similar error flows.
- Event handlers (for example `MessageReceived`, `UserJoined`, `ConnectionStatusChanged`) may be invoked from signalr/connection threads; subscribers should not assume they run on the UI thread and must marshal to the UI thread when necessary.
- Always `await` disposing the manager (it implements `IAsyncDisposable`) so underlying resources such as the [`EchoHubConnection`](EchoHubConnection.cs.md) and [`ApiClient`](ApiClient.cs.md) are cleanly released; failing to do so can leave connections or background work active.
---
@@ -51,13 +50,12 @@ internal record ConnectResult(
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
Represents the outcome of a successful connection, returned to AppOrchestrator for UI updates. It bundles the authentication result, the current set of channels, and the initial histories for every auto-joined channel (keyed by channel name and including the default channel). As an immutable record, it serves as a single, self-contained snapshot that the UI can bootstrap from after a connect.
ConnectResult represents the payload returned after a successful connection, carrying everything the [`AppOrchestrator`](../AppOrchestrator.cs.md) needs to update the UI. It includes the authenticated login information (`Login`), the collection of available channels (`Channels`), and the initial per-channel histories (`Histories`), where each channel name maps to its starting list of messages, always including the default channel.
## Remarks
This object centralizes the data needed to render the initial connected state, decoupling the connection logic from the UI orchestration. By passing a single ConnectResult, the AppOrchestrator can immediately populate channel lists and histories without issuing additional fetches, promoting a clean separation between connection handling and presentation concerns.
ConnectResult is a `record`, so it participates in value-based equality and can be treated as a single unit when comparing connection outcomes. Note that its `Channels` and `Histories` collections are mutable (`List<ChannelDto>` and `Dictionary<string, List<MessageDto>>`); if you need true immutability, expose read-only wrappers or clone the collections when passing them onward.
## Notes
- ConnectResult is immutable; to reflect changes (e.g., new messages or channels), construct and pass a new instance rather than mutating the existing one.
- Histories is a dictionary keyed by channel name that contains the initial per-channel histories; ensure channel names in the dictionary align with the Channels list to avoid inconsistencies.
- The contained `List<ChannelDto>` and `Dictionary<string, List<MessageDto>>` are mutable; avoid mutating them in place and consider treating the `ConnectResult` as a snapshot that should be cloned if you require immutability downstream.
---
@@ -20,26 +20,13 @@ public sealed class ChannelPasswordRequiredException : Exception
```
Thrown when joining a channel fails because a password is required or the provided password is incorrect. The UI catches this to prompt the user for credentials and retry the join, using ChannelName to provide channel context.
ChannelPasswordRequiredException represents the domain condition that a join operation on a channel cannot proceed because a password is required or the provided password was invalid. It is intended to be caught by the UI layer, which then prompts the user for the correct password and retries the join operation. The exception carries the channel name via the `ChannelName` property to identify which channel needs authentication.
## Remarks
ChannelPasswordRequiredException provides a precise signal for a password-related join failure. By carrying the ChannelName, it enables the UI to present a meaningful prompt and retry flow without inspecting lower-level errors. This focused exception helps keep join logic cohesive and testable by separating password-entry concerns from generic failure handling.
## Example
```csharp
try
{
// Code that attempts to join a channel and may throw ChannelPasswordRequiredException
}
catch (ChannelPasswordRequiredException ex)
{
Console.WriteLine($"Password is required to join channel '{ex.ChannelName}'.");
// Prompt the user for a password and retry the join using the provided channel name
}
```
Using a distinct exception type to signal password-related authentication flows keeps the connection logic decoupled from the UI. The `ChannelName` property provides channel-specific context for prompts, enabling precise feedback such as prompting for the password of the channel identified by `ChannelName` when retrying.
## Notes
- Be mindful that ChannelName may be null if constructed with null; guard accordingly before displaying it to users.
- Use a specific catch for `ChannelPasswordRequiredException` rather than a broad catch of `Exception`, to avoid handling unrelated failures; access the `ChannelName` to present a contextual, channel-specific prompt.
---
@@ -52,35 +39,30 @@ public sealed class EchoHubConnection : IAsyncDisposable
```
A SignalR-backed client wrapper that manages a HubConnection to the Echo chat hub, integrates client-side encryption/room-key lookup, and exposes simple event callbacks for incoming messages, presence and channel events. Reach for EchoHubConnection when you need a higher-level, event-driven connection to the server that automatically handles authentication token provisioning and reconnect behavior while decrypting incoming payloads for the UI.
A lightweight, event-driven wrapper around a SignalR `HubConnection` that manages authentication, reconnection and client-side handlers for the chat protocol. Use `EchoHubConnection` when you need a high-level, strongly-typed bridge between the server's [`IEchoHubClient`](../../EchoHub.Core/Contracts/IEchoHubClient.cs.md) callbacks and your UI or application logic — it registers the server method handlers, decrypts incoming content, exposes simple events (for messages, presence, channel updates, errors, etc.), and surfaces connection state changes.
## Remarks
EchoHubConnection encapsulates the SignalR HubConnection lifecycle and maps server callbacks onto plain .NET events (e.g. OnMessageReceived, OnUserJoined, OnChannelUpdated). It supplies the HubConnectionBuilder with an AccessTokenProvider using the provided ApiClient so calls are authenticated, and it wires automatic-reconnect handlers that surface connection state changes via OnConnectionStateChanged and OnReconnected. Incoming MessageDto instances are passed through the client-side encryption pipeline (ClientEncryptionService and RoomKeyStore) so the UI sees decrypted content or a locked placeholder when a room key is not available.
`EchoHubConnection` centralizes SignalR integration concerns: it creates and configures the underlying `HubConnection` (including token provisioning via the provided [`ApiClient`](ApiClient.cs.md)), wires up automatic reconnect behavior, and maps server-invoked methods to public events such as `OnMessageReceived`, `OnUserJoined`, `OnChannelUpdated`, and others. Incoming [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) instances are run through the connection's decryption path (see `DecryptMessage`/`DecryptField`) before being forwarded, and encrypted content that cannot be unlocked is replaced by the `LockedMessagePlaceholder`. The class implements `IAsyncDisposable` so consumers should `await DisposeAsync()` to cleanly stop the connection.
## Example
```csharp
// Subscribe to events and inspect connection state
var echo = new EchoHubConnection(serverUrl, apiClient, encryptionService, roomKeyStore);
// Assume these are already created: serverUrl (string), apiClient (ApiClient),
// encryption (ClientEncryptionService), roomKeys (RoomKeyStore).
var connection = new EchoHubConnection(serverUrl, apiClient, encryption, roomKeys);
echo.OnMessageReceived += message =>
{
// MessageDto is provided by the library; content may be the LockedMessagePlaceholder
Console.WriteLine($"Message received in {message.ChannelName}: {message.Content}");
};
connection.OnConnectionStateChanged += state => Console.WriteLine($"State: {state}");
connection.OnMessageReceived += message => Console.WriteLine($"Message from {message.From}: {message.Content}");
connection.OnReconnected += () => Console.WriteLine("Reconnected to hub");
if (echo.IsConnected)
{
Console.WriteLine("Currently connected to the chat hub.");
}
// Remember to dispose when finished
await echo.DisposeAsync();
// When finished with the connection:
await connection.DisposeAsync();
```
## Notes
- Events are raised directly from SignalR callbacks; handlers may not run on a UI thread — marshal to the UI thread if required.
- Encrypted messages for channels without a stored key are replaced with LockedMessagePlaceholder; supply the channel passphrase (through the app's key store flow) to see decrypted content.
- Call DisposeAsync to release the underlying HubConnection and related resources to avoid background network activity.
- Event handlers are invoked from the SignalR callbacks — subscribers should ensure any UI updates or shared-state mutations are marshalled to the correct synchronization context or made thread-safe.
- Encrypted message content is represented by the `LockedMessagePlaceholder` when the client lacks the room key; rejoining the channel with the passphrase (and so populating [`RoomKeyStore`](RoomKeyStore.cs.md)) is required to decrypt those contents.
- `IsConnected` reflects the underlying `HubConnection.State` at the moment of access and may change shortly after; use `OnConnectionStateChanged` and `OnReconnected` for lifecycle-driven logic.
- Attempting to join a password-protected channel can surface a `ChannelPasswordRequiredException` — callers that perform join flows should handle that explicitly.
---
@@ -93,31 +75,10 @@ public sealed class RoomLockedException : Exception
```
Thrown to signal a security-sensitive condition when attempting to send a message into an end-to-end encrypted channel whose room key isn't cached. The operation is blocked to prevent sending plaintext; catching this exception lets the UI prompt for the channel's passphrase and unlock the room before retrying.
`RoomLockedException` is thrown when attempting to send into an end-to-end encrypted channel whose room key isnt cached. Without the key, the operation would emit plaintext, which must never happen, so the exception blocks the send. The `ChannelName` property exposes which channel is locked, and the constructor formats the failure message to include `#{channelName}` to guide unlocking.
## Remarks
RoomLockedException acts as a clear boundary between encryption policy and transport logic. By exposing the ChannelName, callers can present a channel-scoped unlock prompt without parsing the error text, and the sealed Exception type communicates a concrete, expected failure mode that downstream code can handle distinctly from generic errors.
## Example
```csharp
try
{
// Simulated scenario: an attempt to send into a locked E2E channel
throw new RoomLockedException("Lobby");
}
catch (RoomLockedException ex)
{
// Use the information to drive the unlock UX
Console.WriteLine(ex.Message);
Console.WriteLine($"Unlock channel: {ex.ChannelName} by entering its passphrase.");
}
```
## Notes
- Do not swallow this as a generic error; catch RoomLockedException to trigger the unlock UX and use ex.ChannelName to identify the affected channel. The displayed message is user-facing and not localized.
This exception acts as a boundary between encryption state and message-sending logic. It is a domain-level signal distinct from other transport or I/O failures, enabling callers to trigger a user prompt to unlock the channel and retry the operation once unlocked. The `ChannelName` property ties the failure to a specific channel, enabling precise remediation flows.
---
@@ -138,21 +99,12 @@ public sealed record JoinOutcome(List<MessageDto> History, string? EncryptionSal
| `WrappedRoomKey` | `string?` | — |
JoinOutcome is a sealed record that represents the result of joining a channel: it includes the decrypted history (History) and, for end-to-end encrypted channels, the key envelope necessary to unlock the room's content key (WrappedRoomKey). EncryptionSalt is the salt used to derive the encryption key when applicable. This type is typically produced by the join logic and consumed by the UI to render messages and initialize decryption if needed.
Represents the result of joining a channel: the decrypted message history and, for end-to-end encrypted rooms, the key envelope needed to unlock the room content key. `History` is a `List<MessageDto>` containing the decrypted messages, and `WrappedRoomKey` (with optional `EncryptionSalt`) provides the cryptographic envelope when encryption is in play.
## Remarks
This abstraction centralizes the outcome of a join into a single, immutable value that downstream components can rely on. The History is always present (even if empty), while EncryptionSalt and WrappedRoomKey are nullable to reflect that some rooms are not end-to-end encrypted or that keys may not be provisioned yet. By grouping history and encryption metadata together, the join logic can separate concerns: rendering chat versus handling cryptographic setup.
## Example
```csharp
using System.Collections.Generic;
List<MessageDto> history = new List<MessageDto>();
var joinResult = new JoinOutcome(history, null, null);
```
By encapsulating the join outcome in a single type, the caller can render history and prepare for decryption in one step. The nullable `WrappedRoomKey` and `EncryptionSalt` signal whether encryption is active for the channel; callers not using end-to-end encryption can ignore them. This keeps the join path concise while preserving a clear contract about what data is available after join.
## Notes
- EncryptionSalt and WrappedRoomKey can be null; callers should verify non-null before attempting decryption-related steps.
- `EncryptionSalt` and `WrappedRoomKey` are nullable; guard for nulls and only attempt decryption when these values are provided.
---
@@ -19,16 +19,17 @@ public static class NativeFolderPicker
```
Opens the OS-native folder picker by shelling out to the host OS, keeping the TUI free of GUI toolkit dependencies. It supports Windows, macOS, and Linux by delegating to platform-specific helpers and returns a FolderPickResult that communicates whether a folder was chosen, the dialog was cancelled, or the native picker is unavailable so the caller can fall back to a configured path.
Opens the OS-native folder chooser by shelling out to platform-specific dialogs (Windows Explorer, macOS Finder, Linux GTK/KDE), allowing the TUI to remain GUI-toolkit agnostic. It dispatches to the appropriate platform helper at runtime and returns a `FolderPickResult` with a `PickerOutcome` of `Unavailable` when no native dialog can run, so callers can fall back to a configured path. Failures are caught and logged to avoid crashing the UI, and the dialog title is a fixed prompt guiding the user to select EchoHubs download folder.
## Remarks
NativeFolderPicker centralizes cross-platform behavior for obtaining a folder path without pulling in a GUI toolkit. It hides OS differences behind a single entry point, PickFolderAsync, and exposes a uniform result type (FolderPickResult with a PickerOutcome) that callers can inspect to either proceed with the chosen path or fall back to defaults. Failures are caught and logged, ensuring graceful degradation rather than exceptions propagating to the UI.
By shielding native dialogs behind `NativeFolderPicker`, the rest of the application stays decoupled from platform GUI toolkits, improving portability and testability. The abstraction also centralizes crossplatform quirks (Windows PowerShell quoting, AppleScript invocation, and GTK/KDialog fallbacks) in one place, reducing duplication and ensuring a consistent user experience across environments.
## Notes
- Linux will not attempt a graphical picker if no graphical session is detected (DISPLAY or WAYLAND_DISPLAY are missing); in that case, the method returns Unavailable.
- On Windows, the initial directory is sanitized (apostrophes are doubled) to safely embed the path in the PowerShell script, and PowerShell is invoked via an encoded command to avoid quoting issues.
- If the platform-specific helper cannot be started, the code falls back to returning Unavailable instead of throwing, allowing callers to implement their own fallback strategy.
- Headless Linux environments (no `DISPLAY` or `WAYLAND_DISPLAY`) cause the picker to return `PickerOutcome.Unavailable`.
- Windows path handling escapes apostrophes in the initial directory to survive the embedded PowerShell script.
- If the user cancels the dialog or no path is selected, the result is `PickerOutcome.Cancelled` rather than an error; callers should handle this as a user action.
---
@@ -48,22 +49,16 @@ public sealed record FolderPickResult(PickerOutcome Outcome, string? Path)
| `Path` | `string?` | — |
FolderPickResult is an immutable data carrier that represents the outcome of a native folder-picking operation and, when successful, the path of the selected folder.
FolderPickResult is an immutable data container that captures the result of a native folder picker operation. It pairs the `PickerOutcome` with an optional `Path`, letting callers distinguish between a successful selection and cancellation while carrying the selected folder path only when available.
## Remarks
Because FolderPickResult is a record, it benefits from value-based equality and straightforward pattern matching when consumed by calling code. The Path member is nullable to reflect that a folder may not be selected; always check the Outcome before using Path. This abstraction decouples application logic from platform-specific picker implementations, promoting testability and cross-platform compatibility.
## Example
```csharp
var result = new FolderPickResult(PickerOutcome.Success, @"C:\Projects");
if (result.Outcome == PickerOutcome.Success && result.Path is not null)
{
Console.WriteLine(result.Path);
}
```
As a `record`, `FolderPickResult` benefits from value-based equality and supports deconstruction, enabling concise comparisons and pattern matching when consuming results from the native folder picker. It encapsulates the outcome and potential path in a single, strongly-typed value, simplifying higher-level handling and reducing the need for multiple disparate return values.
## Notes
- Path may be null when Outcome indicates cancellation or failure; always verify Outcome before accessing Path.
- `Path` is nullable; validate before use and prefer accessing `Path` only when `Outcome` indicates a successful result.
---
@@ -83,14 +78,32 @@ public enum PickerOutcome
```
PickerOutcome encodes the result of attempting to display a native folder picker. It defines three mutually exclusive states: Chosen (the user picked a folder and FolderPickResult.Path is set), Cancelled (the native dialog ran but no selection was made), and Unavailable (no native picker is available on the current machine).
Use this enum to drive post-pick logic without scattering platform checks or error handling across call sites.
Represents the outcome of prompting the user to pick a folder via the native picker. Use it to branch logic based on whether the user selected a folder, cancelled the dialog, or the environment doesn't provide a picker.
## Remarks
This enum serves as a lightweight sum type for the outcome of a folder-picking operation. It centralizes decision points and pairs with FolderPickResult to obtain the actual path when Chosen is returned. Consumers can implement a fallback flow for Unavailable and provide a smooth user experience when Cancelled.
By isolating the three possible results into a single enum, callers can write concise, robust code without tying their logic to UI details. The Cancelled and Unavailable outcomes allow you to differentiate between a user-initiated abort and a runtime environment where the picker isn't present, enabling graceful fallbacks. Tie the Chosen outcome to a corresponding `FolderPickResult` instance that carries the selected path in its `Path` property.
## Example
```csharp
// Example: respond to folder-picking outcomes
public void HandleOutcome(PickerOutcome outcome, FolderPickResult folderPath)
{
switch (outcome)
{
case PickerOutcome.Chosen:
Console.WriteLine($"Selected folder: {folderPath.Path}");
break;
case PickerOutcome.Cancelled:
// User cancelled the dialog; no folder selected.
break;
case PickerOutcome.Unavailable:
// Fall back to a non-UI flow
break;
}
}
```
## Notes
- Unavailable is not an error; it indicates the absence of a native picker and warrants a fallback strategy (e.g., a non-native picker or manual path entry).
- Do not access `FolderPickResult.Path` when outcome is not `PickerOutcome.Chosen`.
---
@@ -8,12 +8,16 @@ public class NotificationSoundService
```
NotificationSoundService centralizes the playback of the notification sound. It resolves the sound file from configuration (if specified and found) or falls back to a bundled default, then plays the sound at a configurable volume when requested. The service exposes SetEnabled and SetVolume for simple runtime tuning, and PlayAsync for normal operation or PlayTestAsync for QA scenarios where playback should occur regardless of the Enabled flag. Internally it uses a semaphore to serialize concurrent playback, and a 10-second timeout to prevent a stuck caller if the sound does not finish.
NotificationSoundService coordinates playback of the application's notification sound using a configurable file path and volume. It exposes `PlayAsync` for normal operation (respecting the `Enabled` setting) and `PlayTestAsync` to audition the sound regardless of that setting; internally it resolves the sound path, applies the configured volume, and uses a `SemaphoreSlim` lock plus a timeout (`PlaybackTimeout`) to avoid blocking future notifications.
## Remarks
The class isolates all concerns around audio playback: path resolution, volume handling, concurrency, and fault tolerance. By hiding these details behind a single service, higher-level notification logic can simply request a sound without worrying about file presence, logging, or synchronization. The design anticipates environments where a sound file might be missing or playback might stall, and it ensures resources are released and the system remains responsive.
Architecturally, this class centralizes notification sound behavior so callers don't need to touch the `_player` or handle `PlaybackFinished` events directly. It encapsulates path resolution: first a user-configured path (`_config.SoundFile`), if present and exists, else a bundled default at `Path.Combine(AppContext.BaseDirectory, "Assets", "Notification.mp3")`. The combination of a serializing lock (`_lock`) and a guarded finish path ensures only one sound plays at a time and that resources are released promptly even if playback misbehaves.
The playback flow subscribes to `_player.PlaybackFinished` and uses a `TaskCompletionSource` to await either completion or the timeout; this design guarantees the lock is released even if playback misfires or completes synchronously.
## Notes
- Silent fallback if a sound file cannot be found; production environments should ensure the asset exists if audible alerts are required.
- The PlaybackFinished event and the 10-second timeout guard the system against hangs; the lock may be released before the sound finishes, which means subsequent playback requests can start while a prior one is still playing.
- PlayAsync respects the Enabled flag, while PlayTestAsync allows testing the sound regardless of Enabled.
- If no valid sound file is found, notifications will be silent (log: "No notification sound file found — notifications will be silent").
- `PlayAsync` will early-return if `_config.Enabled` is false or `_resolvedSoundPath` is null; `PlayTestAsync` will still return early if `_resolvedSoundPath` is null. Both rely on a correctly resolved path to function.
- The `_lock` is released in a `finally` block to guarantee progress even when exceptions occur.
@@ -21,26 +21,10 @@ public sealed record OutgoingAttachment(
| `EncryptedPreview` | `string?` | `null` |
OutgoingAttachment is a transport object that represents a single file to upload as part of a message. It bundles the data Stream and FileName, and optionally carries DeclaredKind and EncryptedPreview for encrypted channels, while non-encrypted channels typically set only Stream and FileName.
OutgoingAttachment is a compact, immutable data carrier that bundles the pieces needed to upload a file as part of a message: the content as a `Stream` and the original `FileName`. When using end-to-end encrypted channels, `DeclaredKind` signals the attachment type (image, audio, or file) and `EncryptedPreview` holds the room-encrypted ASCII preview for images; on normal channels, only `Stream` and `FileName` are populated.
## Remarks
OutgoingAttachment serves as a compact, immutable data carrier that travels through the sending pipeline. As a record, it uses value-based equality which helps comparisons and deduplication when attachments are tracked across requests. It also clarifies ownership: the record does not manage the lifetime of the underlying Stream; callers are responsible for opening and disposing streams as appropriate.
## Example
```csharp
using System.IO;
// Normal channel usage: only Stream and FileName are provided
var data = new byte[] { 0x01, 0x02, 0x03 };
var stream = new MemoryStream(data);
var attachment = new OutgoingAttachment(stream, "data.bin");
// End-to-end encrypted channel usage: DeclaredKind and EncryptedPreview are set
var ciphertext = new MemoryStream(new byte[] { 0xAA, 0xBB, 0xCC });
var asciiPreview = @"ASCII_ART_PREVIEW";
var encryptedAttachment = new OutgoingAttachment(ciphertext, "image.png", "image", asciiPreview);
```
As a `record`, `OutgoingAttachment` provides value-based equality, making attachments easy to compare, cache, or deduplicate as they traverse the messaging pipeline. The optional `DeclaredKind` and `EncryptedPreview` fields separate transport payload from encryption/presentation concerns, keeping encoding logic out of the transport object.
## Notes
- The lifetime of the underlying Stream is not managed by OutgoingAttachment; the caller must ensure the stream is disposed when appropriate.
- DeclaredKind and EncryptedPreview are intended for encrypted channels; in normal channels these values are typically null.
- If `DeclaredKind` is provided for an encrypted attachment, ensure `EncryptedPreview` is also supplied to avoid inconsistent previews.
@@ -8,17 +8,19 @@ public static class PathSetup
```
PathSetup is a cross-platform helper that ensures the application's directory is present on the system PATH, enabling commands like echohub to be run from any terminal session without specifying the full path. EnsureOnPath checks for the directory and, if missing, updates PATH in a platform-appropriate way: Windows updates the user PATH; Unix-like systems append an export line to common shell profile files.
PathSetup is a small helper that ensures the application's directory is present on the system PATH so users can run the `echohub` CLI from any terminal without specifying the full path. The public entry point, `EnsureOnPath`, checks the current PATH and, if the app directory isn't already included, updates PATH in a platform-appropriate way: Windows adds the directory to the user-level PATH, while Unix-like systems append an export line to common shell profile files. The implementation derives the target directory from `AppContext.BaseDirectory`, normalizes path separators, and gracefully handles failures by logging at the debug level if PATH modification cannot be completed.
## Remarks
By centralizing PATH manipulation, this abstraction reduces code duplication and the risk of divergent PATH states across platforms. It uses a lightweight, best-effort approach and logs outcomes to aid diagnostics when PATH updates fail or are skipped. The addition is clearly marked by a PathMarker to avoid duplicating lines in shell profiles.
PathSetup centralizes platform-specific PATH augmentation behind a simple, testable API. It makes the side-effect of PATH modification explicit and isolated from business logic, reducing duplication and potential inconsistencies across the codebase. The class uses an idempotent approach: it first checks whether the directory is already on PATH and only proceeds if needed. On Unix-like systems, it uses a persistent marker (`# Added by EchoHub`) to identify its export line in shell profiles, and it guards against duplicating entries. The combination of platform-specific handling, guarded writes, and informative logging ensures predictable behavior during installation and first-run setup while minimizing surprises for end users.
## Example
```csharp
// Typical usage during installation or first-run setup
PathSetup.EnsureOnPath();
```
## Notes
- The method swallows exceptions and logs at debug level, so callers should not rely on exceptions to signal failure.
- Unix updates affect the user's shell environment; new terminal sessions are typically required to observe changes.
- Windows updates are done at the per-user level; system-wide PATH is not modified.
- On Windows, the path update affects only the current user by modifying the user PATH environment variable, avoiding system-wide changes.
- On Unix-like systems, the code appends a PATH export line to common shell profiles (``.profile``, ``.bashrc``, ``.zshrc``); it skips profiles that already contain the app directory and creates ``~/.profile`` as a fallback when no profiles exist.
- A persistent marker (``# Added by EchoHub``) helps avoid duplicating the export line on repeated runs.
- The operation is best observed after restarting terminals or re-sourcing profiles; until that point, newly opened sessions may not reflect the updated PATH.
@@ -8,23 +8,9 @@ public sealed class RoomKeyProtector
```
Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with 0600 permissions (prefix "k1:") — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form.
Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix `dp1:`). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with permissions 0600 (prefix `k1:`) — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form.
The RoomKeyProtector class provides a single API surface to protect and unprotect per-user room keys across platforms. The Protect method returns a string suitable for storage in the config, automatically selecting the appropriate protection mechanism for the current OS (DPAPI on Windows, file-based AES-GCM on others). TryUnprotect decodes a stored value back into a room key, reporting whether the value was a legacy (unencrypted) entry and whether the decryption succeeded. The implementation intentionally hides platform differences behind a consistent interface, so callers can persist and reload keys without worrying about the underlying cryptosystem.
The constructor accepts a directory that holds the master key file and an optional flag to override the OS-provided protection path (useful for tests). The key file path is derived from the directory by appending the fixed file name roomkeys.key. Key loading is guarded by a small lock and the master key is cached after the first read. The Protect path prefixes the output to indicate how the data is protected ("dp1:" or "k1:").
The class ensures the room passphrase itself is never persisted, and it gracefully tolerates missing or unreadable key material by returning false from TryUnprotect (leaving the caller to prompt the user for action).
````csharp
// Typical usage
var protector = new RoomKeyProtector("/config");
byte[] roomKey = new byte[32]; // obtain from a secure source
string stored = protector.Protect(roomKey);
if (protector.TryUnprotect(stored, out var recovered, out bool wasLegacy))
{
// recovered contains the room key if the value was decryptable
// wasLegacy is true only if the input was a legacy base64 key without a prefix
}
````
The primary public surface consists of:
- `Protect(byte[] roomKey)`: encrypts a room key for storage in the config.
- `TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy)`: decrypts a stored value back into a room key.
The class caches the per-user master key and selects the protection mechanism based on the platform (DPAPI on Windows when enabled, otherwise the per-user master-key path). It also handles migration of legacy entries by re-encrypting them using the active scheme on subsequent saves. The constants `DpapiPrefix` and `KeyFilePrefix` label the on-disk formats, ensuring callers remain agnostic to the underlying storage strategy.
@@ -8,36 +8,12 @@ public sealed class RoomKeyStore
```
Holds and manages end-to-end encrypted room keys for a single client instance: it keeps a decrypted, in-memory cache for the active session and a per-server persisted, encrypted copy so users do not have to re-enter passphrases each launch. Use RoomKeyStore when you need a thread-safe local store that provides room keys to the runtime and ensures keys are encrypted at rest via RoomKeyProtector.
Holds and manages room content keys for end-to-end encrypted channels for the active session and the persisted per-server client configuration. Use `RoomKeyStore` when you need a single place to cache decrypted room keys in memory, persist them encrypted to the local config (so users don't retype passphrases on each launch), and track which channels are known to be end-to-end encrypted.
## Remarks
RoomKeyStore links transient runtime state with the client's persisted configuration. It binds to a server (LoadForServer), loads that server's saved ChannelKeys (unprotecting them with RoomKeyProtector), and exposes methods to read, add, replace, or remove keys while persisting changes back to the SavedServer entry. It also records which channels are known to be encrypted so callers can avoid emitting plaintext into rooms without a cached key. The class performs a one-way upgrade of legacy unprotected entries to the protected format when possible and logs unreadable entries rather than failing.
## Example
```csharp
var store = new RoomKeyStore();
store.LoadForServer("https://chat.example.com");
// Generate and store a new room key for a channel
byte[] newKey = RoomCrypto.GenerateRoomKey();
store.StoreKey("#team-room", newKey);
// Retrieve a key for sending encrypted messages
if (store.TryGetKey("#team-room", out var key))
{
// Use `key` with RoomCrypto API to encrypt message content
}
// Accept an encrypted envelope and store the unwrapped key only if the KEK opens it
string wrapped = "..."; // envelope string received
byte[] kek = /* key-encryption-key */ new byte[RoomCrypto.KeySizeBytes];
if (store.TryStoreFromEnvelope("#other-room", wrapped, kek))
{
// successfully unwrapped and cached
}
```
`RoomKeyStore` is the in-process authority for room keys: it keeps a memory cache (`_keys`) for the running session and a set (`_encryptedChannels`) to mark channels that are treated as encrypted. It delegates on-disk protection to [`RoomKeyProtector`](RoomKeyProtector.cs.md) so keys never leave the machine in plaintext. Calling `LoadForServer` binds the store to a specific server URL, loads that server's `SavedServer.ChannelKeys` via `ConfigManager.Load()`, and hydates the in-memory cache (skipping unreadable entries). Legacy plaintext/legacy-storage entries detected by `RoomKeyProtector.TryUnprotect` are re-encrypted and re-persisted as a one-way upgrade. All public mutation and lookup methods synchronize on the internal `Lock` (`_lock`) to provide basic thread-safety for concurrent callers.
## Notes
- Call LoadForServer(serverUrl) before persisting or retrieving server-scoped keys; the store clears and reinitializes its cache when bound to a server.
- Legacy (plain/base64) saved entries are upgraded to the protector-backed format when possible; entries that cannot be unprotected are ignored and logged.
- The class uses an internal lock for basic thread-safety of the in-memory cache; avoid holding returned keys while performing long synchronous work that might race with store mutations.
- `TryGetKey` returns the stored byte array reference from the internal `_keys` map (no defensive copy). Callers must not mutate the returned `byte[]` in-place — clone it first if modification is required.
- Channel name lookup is case-insensitive because the internal collections use `StringComparer.OrdinalIgnoreCase`. Treat channel names consistently to avoid duplicate/lookup surprises.
- Loading ignores unreadable cached entries and will re-persist only entries that [`RoomKeyProtector`](RoomKeyProtector.cs.md) could successfully unprotect; `TryStoreFromEnvelope` returns false when the provided KEK fails to unwrap the envelope and will leave the cache unchanged. Storing or removing a key persists the corresponding `SavedServer.ChannelKeys` entry immediately (via the store's persistence path).
@@ -20,15 +20,11 @@ internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSe
```
BackupJsonContext is an internal partial class that provides the source-generated JSON serialization metadata for the BackupInfo type. It plugs into System.Text.Jsons source generator, enabling reflection-free serialization of BackupInfo when you configure a JsonSerializerOptions with this context.
Defines a source-generated JSON serialization context for `BackupInfo` by annotating the internal partial class ``BackupJsonContext`` with ``JsonSerializable(typeof(BackupInfo))``. This enables high-performance, reflection-free JSON serialization and deserialization via System.Text.Json's source generator when working with ``BackupInfo``.
## Remarks
This symbol acts as the concrete carrier of serialization metadata for BackupInfo within the JSON pipeline of EchoHubs client. By centralizing the generated type information in a single context, it keeps serialization concerns isolated from business logic and allows the type to evolve without scattering attributes across multiple call sites. The pattern here—one generated context per data contract—supports predictable performance improvements while preserving a clean, minimal public surface.
By centralizing the JSON metadata in ``BackupJsonContext``, the codebase gains a single, version-stable contract for serializing ``BackupInfo``. The generated ```JsonTypeInfo<BackupInfo>``` exposed as ``BackupJsonContext.Default.BackupInfo`` is consumed by ``JsonSerializer`` overloads that accept type metadata, reducing runtime reflection and enabling better inlining and optimization. This scope-limited context also makes it straightforward to extend serialization support to additional related types by extending the same context without changing call-sites.
## Notes
- The symbol is internal; it is intended for use within the containing assembly, not by external callers.
- The class is generated and partial; do not edit it by hand, as changes will be overwritten by the source generator.
- If you modify the BackupInfo shape, you must re-run code generation to keep the context in sync with the data contract.
---
@@ -41,7 +37,7 @@ public static class UpdateBackupService
```
Manages pre-update backups for the auto-updater and provides rollback support by snapshotting the running application prior to an update. Backups are stored under ~/.echohub/update-backup/ as backup.zip with a companion backup-info.json that records the version, application directory, and UTC timestamp. Use CreateBackup before applying an update; verify presence with BackupExists and inspect metadata with GetBackupInfo to drive a rollback if needed. The IsPostUpdate flag signals that a backup from a recent update exists, allowing startup logic to react accordingly.
UpdateBackupService is a centralized helper that manages pre-update backups and rollback restoration for the auto-updater. It stores backups under the user profile in `~/.echohub/update-backup/` and exposes operations to create a snapshot, verify an existing backup, and read its metadata. Before applying an update, `CreateBackup()` snapshots the current application directory (via `AppContext.BaseDirectory`) into a ZIP named `backup.zip` and writes a `backup-info.json` containing the version, app directory, and timestamp. It skips log files to avoid locking issues, uses `CompressionLevel.Fastest` for speed, and annotates the backup with the current version from `UpdateChecker.CurrentVersion`. `BackupExists()` checks for the presence of both `backup.zip` and `backup-info.json`, while `GetBackupInfo()` reads and deserializes the metadata using `BackupJsonContext.Default.BackupInfo`. The `IsPostUpdate` flag signals that a post-update backup was produced and may influence rollback or recovery flow.
---
@@ -65,22 +61,13 @@ public record BackupInfo(
| `CreatedAt` | `DateTimeOffset` | — |
BackupInfo is a lightweight, value-like record that encapsulates metadata about a created backup. It carries the backup Version, the AppDirectory that was backed up, and the CreatedAt timestamp, enabling complete backup metadata to be passed around as a single unit.
BackupInfo is a `record` that encapsulates the metadata for a backup produced by the application. It aggregates the `Version` string, the `AppDirectory` path where the backup resides, and the creation timestamp `CreatedAt` as a `DateTimeOffset`, providing a single, immutable value that callers can transport, compare, or display without reconstructing individual fields. Use this type whenever you need to pass around a complete snapshot of backup identity and location rather than scattering primitive values.
## Remarks
BackupInfo, being a record with positional parameters, is immutable and benefits from value-based equality. This makes it ideal as a canonical data carrier when the UpdateBackupService reports or persists backup information, or when UI/logging layers need to compare or display backup entries.
## Example
```csharp
var backup = new BackupInfo(
Version: "1.2.3",
AppDirectory: "/opt/MyApp",
CreatedAt: DateTimeOffset.UtcNow
);
```
Because `BackupInfo` is a `record`, it provides value-based equality and immutability, so two backups with the same `Version`, `AppDirectory`, and `CreatedAt` compare as equal. This makes it ideal as a transport object across service boundaries and as a stable key or result in collections. It also supports deconstruction, enabling concise extraction of its three fields when needed.
## Notes
- Records provide structural equality; two instances with the same Version, AppDirectory, and CreatedAt compare as equal.
- CreatedAt uses DateTimeOffset to preserve offset information; prefer UTC (DateTimeOffset.UtcNow) when constructing backups to avoid timezone ambiguities.
- This object is immutable; its properties are set at construction time and cannot be changed afterward.
- The `CreatedAt` value uses `DateTimeOffset` to preserve the exact point in time including offset, which is important for cross-system backups and logs.
---
@@ -8,29 +8,30 @@ public sealed class UpdateChecker : IDisposable
```
Checks for application updates in the background, presents a TUI confirmation dialog when a new version is available, and defers the actual download/extract/restart work until after the terminal UI has been shut down. Use this class when the host application runs a Terminal.Gui main loop and needs a safe way to offer in-place updates without deadlocking the console or performing heavy I/O while the TUI still owns the terminal.
Checks for application updates on a background schedule and coordinates a safe, post-TUI update process. Use `UpdateChecker` when you want automatic or on-demand update checks inside a Terminal.Gui-based host but need the actual download/extract/restart work to run after the UI main loop has exited.
## Remarks
This class encapsulates polling and manual update checks via an internal Updater instance and marshals user interaction back onto the provided IApplication main loop using _app.Invoke. When the user confirms an update, UpdateChecker does not perform the network/download work immediately; instead it sets PendingUpdate to an awaitable callback (ApplyUpdateAsync), stores the selected version, and requests the TUI to stop. The host is expected to call PendingUpdate after the main loop exits and the console has been restored so the update process can safely run headless (the Updater's update flow may restart the process and call Environment.Exit).
`UpdateChecker` encapsulates the interaction between the UI, a periodic `Updater` and the host process that must perform the actual update. It listens for `Updater` events and, when the user confirms an update via `UpdateConfirmDialog.Show`, sets the public [`PendingUpdate`](../AppOrchestrator.cs.md) delegate and requests the UI to stop so the host can perform the heavy work on a plain console. This design avoids the console deadlock that would occur if the updating process tried to restart while the Terminal.Gui main loop still owned the console. `CurrentVersion` exposes the assembly version used in the confirmation UI.
## Example
```csharp
// During application startup
var updateChecker = new UpdateChecker(app);
updateChecker.Start(); // starts periodic checks in RELEASE builds
var checker = new UpdateChecker(app);
checker.Start(); // starts periodic checks in RELEASE builds
// ... run Terminal.Gui main loop ...
// Trigger a manual check from UI or command handler
await checker.CheckNowAsync();
// After the main loop exits and the console is restored, run any pending update
if (updateChecker.PendingUpdate != null)
// After the Terminal.Gui main loop exits, the host should run any pending update
if (checker.PendingUpdate != null)
{
await updateChecker.PendingUpdate();
await checker.PendingUpdate(); // will download/extract and may restart the process
}
```
## Notes
- Start only activates the background poller in RELEASE builds (the Start method is no-op in non-RELEASE builds).
- PendingUpdate is deliberately set to a Task-returning delegate and intended to be invoked by the host after the TUI has fully stopped; running it while the TUI still owns the console can deadlock the restart flow.
- ApplyUpdateAsync attempts to create a pre-update backup with UpdateBackupService.CreateBackup; backup creation failures are logged and the update continues.
- ApplyUpdateAsync sets Console.OutputEncoding = UTF8 but swallows exceptions (useful when stdout is redirected or non-interactive).
- CurrentVersion reads the assembly version and falls back to "0.0.0" if unavailable.
- [`PendingUpdate`](../AppOrchestrator.cs.md) is only set when the user confirms an available update via `UpdateConfirmDialog.Show`; the host must check and invoke [`PendingUpdate`](../AppOrchestrator.cs.md) after the TUI main loop exits.
- `Start()` is conditional on the `RELEASE` build symbol — in non-RELEASE builds the periodic checker does not run.
- Invoking the [`PendingUpdate`](../AppOrchestrator.cs.md) delegate runs the updater on a plain console and may end by restarting the app (the code calls into the `Updater` which performs download/extract/restart). The host should not expect normal process continuation after the update completes.
- `CurrentVersion` reads the assembly version and will return `"0.0.0"` if the assembly version cannot be determined.
- Backup creation is attempted via `UpdateBackupService.CreateBackup()` before applying an update; failures are logged and the update continues without a backup.
@@ -8,12 +8,7 @@ internal sealed class UserSession
```
Stores the current user's session state on the client, including username, online status, and an optional status message. Use this type as a lightweight, centralized container when you need to read or mutate the ephemeral session data for the active user, and call Reset to return all fields to their defaults (empty username, Online status, and no status message).
Represents the current user\'s session state within the client, encapsulating the `Username`, the presence `Status` from [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md), and an optional `StatusMessage`. It is a lightweight in-memory container used by UI and networking layers to track who is logged in and how they present themselves. The `Reset` method reinitializes all fields to their defaults: `Username` to empty, `Status` to `UserStatus.Online`, and `StatusMessage` to `null`.
## Remarks
Internally sealed and non-public, this class keeps the session representation stable within the client service layer and prevents inheritance. It relies on the UserStatus enum from the core models to express the user's current state consistently across the application.
## Notes
- Not thread-safe by default; coordinate concurrent access if used from multiple threads.
- Reset mutates state in place; if you require preserving data, capture it before calling Reset.
- StatusMessage is nullable; null indicates that no message is provided.
This small class centralizes session-related data so multiple components can read and update the user\'s identity and presence from a single source of truth. By being `internal` and `sealed`, it communicates that this is an implementation detail of the client assembly and should not be extended or exposed publicly.
@@ -18,15 +18,21 @@ public class Theme
```
Theme is the central container for theming the EchoHub client UI. It holds a Name and four color palettes (Base, Menu, Dialog, Status) used across the main window and its chrome; plus an optional Border palette that can override only the frame borders while the rest remains tied to Base. If Border is null, the border colors fall back to the Base palette, letting themes tone borders down independently from text to achieve effects like glassy translucency. The palettes default to new ThemeColors instances, so a Theme is immediately usable and developers only configure what they need. Border supports hex literals like "#6E6E6E" and named colors, enabling quick tweaks without changing the rest of the palette.
The `Theme` class encapsulates the color palette used by the UI. It groups per-surface color sets for the main surfaces (`Base`, `Menu`, `Dialog`, `Status`) and exposes an optional `Border` color that can override the window frame independently of text. By providing a name and a complete set of colors, a developer can switch or define visual styles at runtime and apply them to the UI. If you do not need a separate border color, leave `Border` as null to fall back to `Base`.
## Remarks
Theme isolates brand identity and UI chrome from layout logic, enabling themes to be swapped at runtime or per user preference. The per-area color groups—Base, Menu, Dialog, and Status—provide visual consistency while allowing targeted overrides; Border offers a focused knob for edge treatment without touching text colors. This composition reduces duplication: a single Theme can render across the chrome, with optional Border overrides to achieve distinctive looks without rewriting color logic.
The `Theme` object acts as a central theme descriptor that isolates surface-specific colors from the core palette, making it easy to create variants (e.g., light, dark, or glassy appearances) without scattering color values through the code. The optional `Border` enables stylistic nuances for window chrome without altering text or control coloring, helping to achieve subtler, themed aesthetics while preserving readability.
## Example
```csharp
var theme = new Theme
{
Name = "Glass",
Base = new ThemeColors(), // default color family for surfaces
Border = null // explicit fallback to Base colors for borders
};
```
## Notes
- Name is marked as required; always provide a non-empty value during initialization.
- Border is nullable. If you don't set it, the UI uses Base colors for borders; set Border when you want to tint borders independently.
- Hex codes and named colors: ensure strings you assign are valid color tokens understood by the theming system to avoid fallback or misrendering.
---
@@ -39,14 +45,13 @@ public class ThemeColors
```
ThemeColors is a small data container that holds the color choices used by the UI theme. It exposes four properties—Foreground, Background, FocusForeground, and FocusBackground—each with a sensible default (White on Black for normal state, and White on Blue for focused state). This class centralizes theming values so UI components can render consistently and themes can be swapped by supplying a ThemeColors instance rather than scattering color literals throughout rendering code.
ThemeColors is a small data container that groups the color tokens used by the UI: `Foreground`, `Background`, `FocusForeground`, and `FocusBackground`. Create and pass a single `ThemeColors` instance to ensure consistent theming across components rather than scattering color literals throughout the code.
## Remarks
- It acts as a cohesive value object for theming, separating concerns between color data and rendering logic.
- It enables swapping themes by replacing one ThemeColors instance rather than modifying rendering code.
- It is mutable, allowing runtime theme adjustments; if a ThemeColors instance is shared across threads, consider synchronization to avoid race conditions.
By centralizing color choices in `ThemeColors`, the UI can swap themes or provide variations without touching individual controls. The default initializers encode a high-contrast dark theme (white text on black, focus highlight in blue), but you can override any property to tailor a theme for a particular context.
## Notes
- If you mutate and share ThemeColors across threads, you may encounter race conditions; prefer per-thread copies or proper synchronization when updating values.
- Mutability: the properties have public setters, so the color values can be changed after construction; if a `ThemeColors` instance is shared, mutations will affect all dependents.
- Defaults are defined via property initializers; override them on construction if you want a different baseline.
---
@@ -11,23 +11,23 @@
- [GetTheme](#gettheme)
- [ParseColor](#parsecolor)
- [SaveTheme](#savetheme)
- [BuiltInThemes](#builtinthemes)
- [ClassicTheme](#classictheme)
- [DefaultTheme](#defaulttheme)
- [DraculaTheme](#draculatheme)
- [GruvboxTheme](#gruvboxtheme)
- [HackerTheme](#hackertheme)
- [HighContrastTheme](#highcontrasttheme)
- [JsonOptions](#jsonoptions)
- [LightTheme](#lighttheme)
- [MonokaiTheme](#monokaitheme)
- [NordTheme](#nordtheme)
- [OceanTheme](#oceantheme)
- [RosePineTheme](#rosepinetheme)
- [SolarizedTheme](#solarizedtheme)
- [ThemeDir](#themedir)
- [TransparentLightTheme](#transparentlighttheme)
- [TransparentTheme](#transparenttheme)
- [BuiltInThemes](#builtinthemes)
- [GruvboxTheme](#gruvboxtheme)
- [NordTheme](#nordtheme)
- [RosePineTheme](#rosepinetheme)
---
@@ -40,22 +40,18 @@ public static class ThemeManager
```
ThemeManager_overview provides a centralized, static API for discovering, loading, applying, and persisting themes used by the EchoHub client UI. Call GetAvailableThemes to enumerate built-in and user-defined themes, GetTheme to fetch a theme by name, and ApplyTheme to switch the UI to a chosen theme.
ThemeManager is a static helper that centralizes theming for the client UI. It defines built-in themes, reads user-defined themes from the user's theme directory, and exposes methods to enumerate available themes, fetch a theme by name, apply a theme at runtime, and persist theme definitions to disk. Developers reach for it when they need to present theme choices to users, switch the active look, or save a customized theme for future sessions.
## Remarks
Conceptually, ThemeManager acts as the bridge between Theme data (the Theme class) and the runtime UI. It maintains a curated list of built-in themes and exposes logic to load additional themes from a user directory, surfacing them for selection without requiring changes to the runtime code. In addition, BuildColorScheme ensures color assignments for text areas align with the active theme, pinning Editable/ReadOnly roles so that transparent themes render correctly and inputs stay legible. This centralizes theming concerns and keeps theme-related behavior in one place, simplifying maintenance and experimentation with new themes.
## Example
```csharp
var available = ThemeManager.GetAvailableThemes();
var theme = ThemeManager.GetTheme("Default");
ThemeManager.ApplyTheme(theme);
```
Theme definitions live as [`Theme`](Theme.cs.md) instances inside the manager, with a fixed set of built-ins (e.g. `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, `RosePineTheme`) and a mechanism to discover additional user themes from the directory located at `ThemeDir`. `GetAvailableThemes()` merges these sources while skipping duplicates by name and ignoring malformed theme files; if the theme directory cannot be read, it gracefully falls back to the built-ins. The color wiring happens in `BuildColorScheme(ThemeColors colors)` to ensure the editor surfaces—such as `TextView` and `TextField`—are pinned to the themes colors so transparency is preserved (e.g. transparent themes do not render an opaque input background). `ApplyTheme(Theme theme)` applies the chosen look to UI chrome like frame borders and titles, while `SaveTheme(Theme theme)` persists changes to disk as a best-effort operation.
## Notes
- SaveTheme is best-effort and silently swallows failures; verify persistence if you rely on saved themes.
- GetAvailableThemes falls back to built-in themes when the theme directory cannot be read.
- ParseColor expects valid color identifiers defined by the theming system; supply colors that exist in the library or your Theme colors.
- Reading themes from disk is guarded with a fallback to built-ins; IO failures result in a safe degradation rather than a crash.
- Saving themes is a best-effort operation and may fail silently to avoid impacting startup or runtime stability.
- Color parsing relies on `ParseColor(string colorName)`; ensure color names in themes map to known colors to avoid rendering surprises.
---
@@ -76,17 +72,15 @@ public static void ApplyTheme(Theme theme)
**Returns:** `void`
Applies a Theme by registering color schemes for the core UI areas with SchemeManager. This single call maps the Theme's Base, Menu, Dialog, and optional Border sections to named schemes so the rest of the UI can render consistently according to the active theme.
ApplyTheme translates a [`Theme`](Theme.cs.md) into runtime color schemes and registers them with the central scheme registry (`SchemeManager`). For each area (`Base`, `Menu`, `Dialog`) it calls `BuildColorScheme` and registers the result via `SchemeManager.AddScheme`. The `Border` area is populated as well, using `theme.Border` when provided or falling back to `theme.Base` when it is not, ensuring frame decorations always have a defined appearance.
## Remarks
This method acts as a bridge between the Theme model and SchemeManager's scheme registry. It delegates color construction to BuildColorScheme for each region, ensuring Base, Menu, and Dialog colors stay in sync. The Border scheme uses theme.Border when provided, otherwise it falls back to the Base palette to preserve a coherent frame. By applying all four schemes in one place, ApplyTheme reduces the risk of components diverging toward inconsistent styling.
By encapsulating the mapping from a [`Theme`](Theme.cs.md) to per-area color schemes, `ApplyTheme` centralizes theming logic and reduces boilerplate across the UI. It also encodes the intended fallback for borders: if a `Border` scheme isn't specified, the `Base` scheme is reused so borders and title bars stay consistent with the rest of the theme.
## Notes
- Repeatedly calling ApplyTheme overwrites the previously registered schemes, so batch theme updates if you want to avoid intermediate flashes.
- The Border palette falls back to Base when Border is not provided; ensure the Base colors reflect the desired frame in that case.
## Dependencies
- SchemeManager
- If `theme.Base` is null and no explicit `theme.Border` is provided, `BuildColorScheme` will receive null, which could lead to an exception at runtime. Ensure `theme.Base` is non-null when a border theme isn't supplied.
---
@@ -107,15 +101,14 @@ private static Scheme BuildColorScheme(ThemeColors colors)
**Returns:** `Scheme`
BuildColorScheme converts ThemeColors into a Terminal.Gui Scheme by deriving two Attributes—Normal from Foreground and Background and Focus from FocusForeground and FocusBackground—then applying them to the Scheme's state properties (Normal, Focus, HotNormal, HotFocus, Disabled). It also pins Editable and ReadOnly to Normal to ensure input controls render against the theme background, avoiding opaque boxes in transparent themes.
BuildColorScheme is an internal helper that converts a [`ThemeColors`](Theme.cs.md) instance into a complete `Scheme` by translating the theme's foreground/background for normal and focused states into two `Attribute`s and applying them across the scheme's state properties (`Normal`, `Focus`, `HotNormal`, `HotFocus`, `Disabled`, `Editable`, `ReadOnly`). It ensures the editable areas reflect the same colors as the surrounding background, which matters for transparent themes.
## Remarks
This method centralizes the theme-to-scheme translation, decoupling ThemeColors from the Scheme used by the UI. By deriving Normal and Focus once and reusing them for all relevant roles, and by tying Editable/ReadOnly to Normal, it guarantees consistent visual behavior for standard controls and editable regions across themes. The method being private static signals that it's an internal detail of the theming pipeline used by ThemeManager to assemble the active color scheme.
Conceptually, this centralizes the translation from [`ThemeColors`](Theme.cs.md) to a `Scheme`, guaranteeing consistent color usage across `Normal`/`Focus` and their hot variants. By reusing the same color attributes for `Normal`, `Disabled`, and the editable states, it reduces drift when themes change and keeps UI elements visually cohesive. The inline comment explains the rationale: binding `Editable` and `ReadOnly` to the theme's `Normal` colors ensures the input areas don't render an opaque box behind transparent themes.
## Notes
- If ThemeColors contain invalid color strings, ParseColor may throw; ensure colors are validated before calling BuildColorScheme.
- The returned Scheme is a new object each time; repeated calls may impact allocations.
- Editable and ReadOnly are deliberately mapped to Normal; if you need distinct input backgrounds, adjust the mapping accordingly.
- Disabled uses the same color as `Normal`; if you need a distinct disabled appearance, this method would need to be extended.
- Editable and ReadOnly are pinned to `Normal` to preserve background transparency; changing this could cause mismatches with the theme's background in transparent themes.
---
@@ -130,14 +123,24 @@ public static List<Theme> GetAvailableThemes()
**Returns:** `List<Theme>`
The GetAvailableThemes method returns a list of Theme objects by starting with the built-in themes and augmenting that set with user-defined themes discovered in the ThemeDir directory. It iterates over all *.json files, deserializes each one into a Theme using JsonSerializer with the configured JsonOptions, and, if the resulting theme has a non-empty Name and does not duplicate an existing theme (case-insensitive comparison on Name), appends it to the collection. If ThemeDir does not exist or any IO or JSON parsing error occurs, the method gracefully falls back to returning only the built-in themes.
GetAvailableThemes collects the available themes by starting with the built-in set (`BuiltInThemes`), then augmenting it with user-provided themes discovered as JSON files in `ThemeDir`. It reads each `*.json` file, deserializes the content into a [`Theme`](Theme.cs.md) using `JsonSerializer` with `JsonOptions`, and, if the resulting theme has a non-empty `Name` and isn't already present (checked by name using `StringComparison.OrdinalIgnoreCase`), adds it to the list. If the theme directory can't be read or a file is malformed, those items are skipped and the method returns the built-in themes as a fallback. The result is a `List<Theme>` that callers can present to the user.
## Remarks
This function encapsulates the theme-loading strategy: built-in themes establish the default baseline, while external JSON themes extend the collection without mutating the originals. It operates defensively, skipping malformed files and continuing execution in the face of read errors, which yields a predictable return value even under partial failure. De-duplication is driven by Theme.Name using a case-insensitive comparison to prevent accidental duplicates when names differ only by case.
The `GetAvailableThemes` abstraction centralizes theme discovery, ensuring that built-in themes serve as a baseline while allowing runtime customization through JSON files in `ThemeDir`. It performs simple de-duplication by `Theme.Name` in a case-insensitive manner, so user-provided themes do not create duplicates of built-ins. The design favors resilience: IO or deserialization failures are swallowed so startup remains stable, and valid themes are still returned. This function depends on the shape of the [`Theme`](Theme.cs.md) model (e.g., `Name`, `Base`/`Menu`/`Dialog` color sets) to render themes in the UI.
## Example
```csharp
var themes = ThemeManager.GetAvailableThemes();
foreach (var t in themes)
{
Console.WriteLine(t.Name);
}
```
## Notes
- It swallows IO and JSON parsing exceptions, so failures to read or parse individual files do not propagate to the caller.
- Built-in themes take precedence: a user-defined theme with a Name that matches an existing built-in theme is ignored, ensuring stable baseline behavior.
- IO or JSON parsing errors for individual files are ignored; only valid themes are included in the result.
- If `ThemeDir` does not exist or cannot be read, the method falls back to returning only the built-in themes.
- A runtime-provided theme with a name equal (ignoring case) to an existing built-in theme will be skipped to avoid duplicates.
---
@@ -158,15 +161,7 @@ public static Theme GetTheme(string name)
**Returns:** [`Theme`](Theme.cs.md)
Resolves a Theme by name by searching the collection returned by GetAvailableThemes and returning the first match found when the theme name equals the provided name, ignoring case. It is the right choice when you need to map a user-provided theme name (from UI, config, or input) to a Theme object, with a fallback to DefaultTheme if no match exists.
## Remarks
By centralizing theme resolution in this single method, callers can map a string (for example, user input) to a Theme object without duplicating comparison logic or null checks. The use of ordinal string comparison ensures consistent, culture-invariant matching across locales. The method relies on GetAvailableThemes providing a valid collection and on DefaultTheme representing a concrete theme.
## Notes
- If GetAvailableThemes returns null, the call to Find will throw a NullReferenceException.
- The search is linear in the size of the themes collection; for large catalogs consider caching or indexing to improve lookup performance.
- Name comparison uses OrdinalIgnoreCase; if you need culture-aware matching, replace with a culture-aware comparison or normalize names elsewhere.
Returns the [`Theme`](Theme.cs.md) whose `Name` matches the provided `name` using a case-insensitive comparison (`StringComparison.OrdinalIgnoreCase`), sourcing candidates from `GetAvailableThemes()`. If no match is found, it returns `DefaultTheme` as a safe fallback. This encapsulates the pattern of resolving a theme by name and protects callers from handling nulls or missing themes themselves.
---
@@ -187,16 +182,14 @@ private static Color ParseColor(string colorName)
**Returns:** `Color`
Parses a color name into a Color using Color.TryParse. If parsing succeeds, it returns the resulting Color (or White if the parsed color is null). If parsing fails, it returns Color.White. Use this helper when theme code needs to translate a color name string into a Color value, ensuring a valid color is always returned instead of propagating nulls.
Parses a color name into a `Color` value by delegating to `Color.TryParse`. If the parse succeeds, it returns the resulting color (or `Color.White` if the parsed value is null). If parsing fails, it falls back to `Color.White`. This provides a safe, centralized way to convert string-based color specifications (for example, theme or config values) into a concrete `Color` without forcing callers to handle parsing errors themselves.
## Remarks
Centralizes color-name parsing, reducing duplication and guarding ThemeManager's rendering paths against invalid color inputs. The fallback to White makes the UI predictable but at the risk of hiding misconfigurations; consider logging when a fallback occurs to aid debugging.
This method encapsulates the color-name resolution logic so the rest of the theming code does not need to repeat `TryParse` calls or null checks. It guarantees a non-null `Color` return value by defaulting to `Color.White`, thereby defining a system-wide fallback policy for theme colors. Being a private helper, it represents an internal implementation detail of the theme system rather than a public API, which keeps the surface area clean for consumers.
## Notes
- Invalid or unknown color names yield Color.White without throwing.
- No exception is thrown; a deterministic Color is always returned.
- Invalid or unrecognized color names map to `Color.White`, which can mask configuration errors; consider validating color names if distinguishing between an explicit white and a default fallback is important.
- If `colorName` is null or empty, the method still returns `Color.White` via the parse/fallback path, ensuring callers always receive a concrete `Color` without exceptions.
---
@@ -217,290 +210,18 @@ public static void SaveTheme(Theme theme)
**Returns:** `void`
Persists a Theme by serializing it to JSON and writing it to a file named after the theme under ThemeDir. Use SaveTheme to persist a user-selected theme so it can be reloaded on startup; it's a best-effort operation that silently swallows failures, so callers shouldn't rely on it for critical persistence.
Saves a [`Theme`](Theme.cs.md) to disk as a JSON file under `ThemeDir`. It ensures `ThemeDir` exists, constructs the file path using the theme's name (the value of `theme.Name`) with a `.json` extension, serializes the [`Theme`](Theme.cs.md) with `JsonSerializer` using `JsonOptions`, and writes the resulting JSON to disk. Any exceptions are swallowed, making this a best-effort persistence rather than a guaranteed save.
## Remarks
This abstraction encapsulates the simple idea of theme persistence: ensure the target directory exists, determine a file path from the Theme.Name, serialize to JSON using JsonOptions, and write the content. It uses Theme.Name as the file name, so two themes with the same name will overwrite each other; an enhanced naming strategy or unique IDs could help. Failures are swallowed, so any persistence failure is invisible to the caller; consider adding logging or a higher-level retry if persistence must be durable. The method depends on JsonOptions for serialization behavior and relies on the standard IO primitives (Directory, Path, JsonSerializer, File).
SaveTheme encapsulates the simple, best-effort persistence strategy for user-defined themes and deliberately avoids propagating IO errors to callers. It is safe to call during normal operation without risking user-facing crashes, but callers should not rely on this method to succeed every time. Because the file name is derived from `theme.Name`, unmapped or invalid characters in names can cause a write to fail silently.
## Notes
- The catch-all block hides errors; callers cannot detect save failures.
- Using Theme.Name directly as a file name may introduce invalid characters or path traversal risks if Name isn't sanitized.
- Existing theme JSON will be overwritten without backup or versioning.
- The empty catch means failures won't surface to the caller; consider validating `theme.Name` to ensure a valid file name before invoking this method.
- Writes are synchronous and will overwrite an existing file named after the theme.
---
### ClassicTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme ClassicTheme = new()
```
ClassicTheme is a private static readonly Theme that encapsulates the classic visual styling used by the UI. It defines the 'Classic' theme name and assigns color palettes for four UI zones — Base, Menu, Dialog, and Status — so the theming system can render consistent foregrounds, backgrounds, and focus states across the application.
## Remarks
Why this abstraction exists: centralizes the classic color palette in one place, avoiding repetitive literals across components. It also stabilizes the look by exposing a single instance that the ThemeManager can switch to internally to apply the classic aesthetic. In short, ClassicTheme acts as the canonical, versioned styling bundle for the traditional UI appearance.
## Notes
- Potential mutability: If Theme or ThemeColors expose public setters, the colors may be mutated after initialization. Consumers should rely on a stable palette or the code should enforce immutability.
- Accessibility considerations: The palette uses high-contrast combinations (e.g., White foreground on DarkGray/Blue). If your accessibility requirements change, adjust this Theme instance or provide alternative themes.
---
### DefaultTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DefaultTheme = new()
```
Defines the canonical default theme used by the UI components within the ThemeManager. This private static readonly field initializes a single Theme instance named 'Default' with color settings for each UI region (Base, Menu, Dialog, Status). The nested ThemeColors specify the foreground, background, and focus colors, establishing a consistent look-and-feel across the application unless overridden by other theme configurations. Because it is static and readonly, the instance is created once at type initialization and cannot be reassigned, ensuring all consumers relying on the default palette see the same values.
## Remarks
Centralizes the default visual styling to ensure a single, shared baseline across the UI. It prevents scattering color choices across components and makes it easier to reason about the default appearance of the application. If a different baseline is needed for testing or special scenarios, a separate Theme can be created and applied through the ThemeManager, rather than modifying this field.
## Notes
- The field is private; external code cannot access or mutate DefaultTheme directly.
- Even though the reference is readonly, the nested ThemeColors objects may be mutable if their properties are settable; treat the default palette as effectively immutable at runtime unless you deliberately mutate its contents within ThemeManager.
- The color values are provided as names (e.g., 'Gray', 'White'); ensure the rendering layer recognizes these tokens to avoid unexpected visuals.
---
### DraculaTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DraculaTheme = new()
```
DraculaTheme is a private static readonly Theme field that represents the Dracula-inspired color palette used by the theme system. It defines distinct color specifications for four UI surfaces—Base, Menu, Dialog, and Status—each with a foreground color, a background color, and explicit focus colors to ensure consistent, high-contrast visuals across the application. This field is intended for internal use by ThemeManager to apply a cohesive dark theme; external code should not rely on it directly.
## Remarks
Having a single DraculaTheme instance centralizes the Dracula look, preventing drift in color choices across components. By keeping it private and readonly, ThemeManager can switch to Dracula without duplicating palettes, while still allowing other themes to be composed similarly. The explicit focus colors help maintain clear keyboard-navigation states even on dark surfaces.
## Notes
- Private visibility prevents external code from referencing DraculaTheme directly.
- It is static readonly and assigned once; runtime mutation is not expected.
- Token names like BrightMagenta and Magenta map to concrete colors in the rendering layer; ensure the color system supports these tokens for accurate rendering.
---
### HackerTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HackerTheme = new()
```
HackerTheme is a private, static, readonly Theme instance that encodes the color palette used by the Hacker appearance within the UI. It defines the colors for the Base, Menu, Dialog, and Status areas, providing a single source of truth that ThemeManager can apply to render a consistent dark-themed interface.
## Remarks
This field centralizes the Hacker color scheme, ensuring consistent foreground/background pairs across all UI regions and their focus states. Because HackerTheme is private to ThemeManager, external code cannot reference or mutate it directly; changes to the palette must go through ThemeManager's public API or future extensions. The nested ThemeColors per region make it easy to tweak the palette in one place when refining the visual language.
## Notes
- The static readonly modifier means HackerTheme is initialized once and its reference cannot be reassigned, but the contained ThemeColors objects may still be mutable depending on their type.
- External code should not rely on HackerTheme having a public accessor; to reuse the palette publicly, ThemeManager should expose a proper API rather than exposing internal details.
---
### HighContrastTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HighContrastTheme = new()
```
Defines a private static readonly Theme instance named HighContrastTheme that captures a high-contrast color scheme used by the theming subsystem. It specifies color configurations for the Base, Menu, Dialog, and Status surfaces to maximize legibility and clearly indicate focus against a dark background.
## Remarks
HighContrastTheme centralizes the high-contrast styling to avoid scattering color values throughout the codebase. The ThemeManager can switch to this theme to satisfy accessibility requirements without exposing public API changes.
## Notes
- The nested ThemeColors objects may be mutable; mutating them would undermine the high-contrast guarantee. Treat HighContrastTheme as an internal constant and avoid altering its color properties at runtime.
---
### JsonOptions
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly JsonSerializerOptions JsonOptions = new()
```
JsonOptions is a privately scoped, preconfigured JsonSerializerOptions instance used by ThemeManager to serialize JSON with the projects conventions. It enables indented output and camelCase property naming, ensuring that any JSON emitted while theming is both human-readable and aligned with the API surface.
## Remarks
By using a private static readonly field, ThemeManager avoids repeated allocations and guarantees a single shared configuration for its JSON serialization within the class. Note that JsonSerializerOptions is mutable; while the field reference cannot be reassigned, changing its properties at runtime can lead to subtle, cross-call side effects. Treat this instance as effectively immutable after initialization.
## Example
```csharp
// Within ThemeManager
var data = new { Theme = "Dark", Version = 1 };
string json = JsonSerializer.Serialize(data, JsonOptions);
```
## Notes
- Mutating JsonOptions at runtime can cause inconsistent formatting across serialized outputs; prefer making changes only during initialization.
- This field is internal to ThemeManager; if different parts of the application require alternative formatting, construct and pass their own JsonSerializerOptions instead of reusing JsonOptions.
---
### LightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme LightTheme = new()
```
LightTheme is a predefined Theme instance that encodes the light-mode color configuration used by the UI. It centralizes the color values for the base surface and for Menu, Dialog, and Status regions so the theming system can apply a consistent light appearance without constructing a new Theme object each time.
## Remarks
By consolidating the light palette in a single static object, LightTheme ensures visual consistency across components that render base surfaces, menus, dialogs, and status bars. It serves as a canonical reference for the light aesthetic within the theming subsystem, enabling ThemeManager to switch to a known, shared configuration. Because the field is private static readonly, it should be treated as a shared, effectively immutable source at runtime; mutating its nested color objects could lead to inconsistent visuals.
## Notes
- It is a static shared instance; mutating its nested ThemeColors at runtime would have global effects; treat as read-only after initialization.
---
### MonokaiTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme MonokaiTheme = new()
```
MonokaiTheme is a private static readonly field that defines the Monokai color palette used by the theme system. It holds a Theme named "Monokai" composed of four color blocks (Base, Menu, Dialog, Status), each described by ThemeColors with specific foreground, background, and focus colors. This single, shared instance provides a consistent color vocabulary for the UI, allowing ThemeManager and related rendering code to apply the Monokai look uniformly without scattering literals across the codebase. Because the field is private, its usage is internal to the class that declares it.
## Remarks
MonokaiTheme serves as a centralized, reusable color configuration for the Monokai look. By grouping color sets into Base, Menu, Dialog, and Status, it expresses distinct chrome regions while keeping a single source of truth for the palette. This abstraction makes it straightforward for ThemeManager and UI components to consistently apply the Monokai styling.
## Notes
- The Theme and ThemeColors instances are mutable; altering their properties would mutate the shared theme at runtime and affect all consumers within the process.
- External code cannot replace MonokaiTheme, but internal code could adjust its nested properties unless immutability is enforced; consider making Theme/ThemeColors immutable if a fixed theme is intended.
---
### OceanTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme OceanTheme = new()
```
OceanTheme is a predefined ocean-inspired color palette represented as a Theme instance. It groups color configurations for four UI regions—Base, Menu, Dialog, and Status—each with foreground, background, and focus colors, enabling a cohesive look across the application. The field is private static readonly, so the same Theme object is created once and reused, preventing accidental reassignment while keeping internal mutability restricted to the defining class.
## Remarks
Centralizes theming decisions and reduces duplication by providing a single, cohesive palette that UI components can rely on. OceanTheme expresses a clear design intent (an ocean-like aesthetic) and is intended to be selected by theming logic to apply a consistent appearance across Base, Menu, Dialog, and Status surfaces. The per-area ThemeColors allow distinct focus and interaction states while preserving a unified visual language.
## Notes
- Access is private to the ThemeManager class, preventing external code from directly reusing or mutating OceanTheme.
- The reference is readonly, so the field cannot be reassigned; internal mutability would require explicit code within the defining class.
- The color tokens (e.g., BrightCyan, DarkBlue, White, DarkCyan) must be valid tokens within the projects visual system for the palette to render correctly.
---
### SolarizedTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme SolarizedTheme = new()
```
SolarizedTheme is a private static readonly Theme instance that encapsulates the Solarized color palette used by the UI. It defines color roles for four UI surfaces—Base, Menu, Dialog, and Status—specifying both normal foreground/background and focused-state foreground/background colors. The field is initialized once at type-load time and is then reused wherever a Solarized look is required, providing a single source of truth for this color scheme and preventing runtime mutations.
## Remarks
This symbol acts as a centralized, immutable specification of the Solarized look. By housing the color tokens in a single Theme, ThemeManager can consistently apply the same palette across menus, dialogs, and status lines without scattering literals throughout the code. The private static readonly pattern communicates intent: SolarizedTheme is a predefined, non-changing theme available to internal consumers of ThemeManager, not something that should be modified at runtime.
## Notes
- The theme uses string color tokens (e.g., "Cyan", "BrightYellow"), which are resolved by the theming subsystem to actual display colors.
- Because the field is readonly, any changes require rebuilding the Theme instance; runtime mutation is prevented.
- The four ThemeColors sections (Base, Menu, Dialog, Status) each specify both normal and focused color states to support focus indication.
---
### ThemeDir
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly string ThemeDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes")
```
ThemeDir is a private, static readonly string that resolves to the user-specific themes directory by combining the current users profile folder with .echohub/themes. It provides a single, OS-agnostic path for ThemeManager to load and save theme files, avoiding scattered string literals.
## Remarks
Centralizing the location of theme assets decouples theme storage from OS conventions and hard-coded paths, making future relocations or tests simpler. The static readonly nature guarantees a consistent path across all ThemeManager operations, computed at type initialization. If the target directory doesn't exist at runtime, higher-level startup or initialization code should ensure it is created before any read/write of themes.
## Notes
- Directory existence: ensure creation to avoid IO errors when reading or writing themes.
- Hidden folder nuance: .echohub will be hidden on Unix-like systems; consider how this affects user visibility or directory listings in certain UI scenarios.
---
### TransparentLightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentLightTheme = new()
```
Represents a canonical light-theme configuration used by the UI to render surfaces on light backgrounds. TransparentLightTheme is a private static readonly Theme instance that bundles a complete color palette for Base, Menu, Dialog, Status, and Border, enabling a consistent light appearance across the UI when a light or transparent background is in use. The defined colors map foregrounds, backgrounds, and focus states to maintain readability and clear focus cues (Blue for focused elements).
## Remarks
By centralizing the light-theme palette in a single internal Theme instance, this symbol reduces drift between UI surfaces and makes it straightforward to derive alternate light variants from a single baseline. Its private visibility signals it's an internal default rather than a public customization point; external code should define and consume their own Theme instances instead of mutating this one.
## Notes
- Border foreground uses #8F8F8F for softer borders on light terminals.
- Background values set to 'None' indicate transparency or reliance on the parent/background, aligning with a transparent-light aesthetic.
---
### TransparentTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentTheme = new()
```
Defines a single, shared Theme instance named TransparentTheme that implements a glassy, semi-transparent UI aesthetic. Declared private static readonly, it is initialized once and reused by the ThemeManager to apply a cohesive translucent look across Base, Menu, Dialog, Status, and Border color groups (most backgrounds are None to preserve translucency, with White foreground and BrightCyan focus colors; Dialog uses DarkGray to retain legibility; borders use muted grays to complete the glassy look).
## Remarks
This symbol centralizes the glassy appearance so all UI surfaces adopting transparency share a single color model. Being private ensures the theme is an internal implementation detail of ThemeManager and not part of the public theming surface. If a project needs a similar variant publicly, it should be created as a separate, publicly accessible theme instance rather than exposing this private field. The pattern reduces drift between components and simplifies maintenance of the transparent aesthetic.
## Notes
- The field is readonly, but its nested color objects are not guaranteed immutable; mutating their properties at runtime would alter the shared theme for all users. Treat the instance as immutable after initialization to preserve consistency.
---
## BuiltInThemes
### BuiltInThemes
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
@@ -525,19 +246,73 @@ private static readonly List<Theme> BuiltInThemes =
```
BuiltInThemes is a private static readonly collection that enumerates the Theme instances shipped as built-in themes. It provides a stable, canonical set of themes (including DefaultTheme, TransparentTheme, TransparentLightTheme, ClassicTheme, LightTheme, HackerTheme, SolarizedTheme, DraculaTheme, MonokaiTheme, NordTheme, GruvboxTheme, OceanTheme, HighContrastTheme, and RosePineTheme) that ThemeManager can iterate over to present theme options and initialize theming state. Because the field is private and readonly, external code cannot modify this collection at runtime; it is intended as an internal baseline that ensures consistent theming behavior across the application.
BuiltInThemes is a private static readonly collection of [`Theme`](Theme.cs.md) instances that enumerates the built-in themes shipped with the client. It is initialized with a predefined sequence of themes: `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, and `RosePineTheme`, and is used internally by the theming subsystem to provide a centralized source of available themes without constructing them at runtime.
## Remarks
Centralizes the shipped themes into a single place, guaranteeing a consistent ordering and a single source of truth for what counts as built-in. This reduces duplication and makes it easier to adjust defaults or add new themes by updating the initializer, rather than sprinkling Theme references throughout the code. Because it's private, consumers must rely on public Theme-related APIs or ThemeManager flows to query or apply themes.
This private, static collection centralizes the built-in theme catalog used by the theming system. The `readonly` modifier prevents reassigning the field, but the underlying `List<Theme>` can still be mutated by internal code, which means changes to the set of built-ins could affect any UI that relies on them. If true immutability is required, consider exposing a read-only wrapper or a dedicated API surface.
## Notes
- The list is constructed from static Theme instances defined elsewhere (the DefaultTheme, TransparentTheme, etc.).
- As a private, readonly field, it cannot be replaced or mutated at runtime; new themes must be added via source changes.
- If you need to expose or customize the built-in set, provide a public API rather than accessing this field directly.
- The `List<Theme>` is mutable even though the field is `readonly`; external code cannot access it, but internal code can modify its contents. If you need to guarantee immutability, replace with a read-only wrapper such as `ReadOnlyCollection<Theme>` and expose a safe accessor.
---
## GruvboxTheme
### ClassicTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme ClassicTheme = new()
```
ClassicTheme is a privately scoped, statically initialized [`Theme`](Theme.cs.md) instance that serves as the built-in look-and-feel blueprint used by the UI. It defines color mappings for the `Base`, `Menu`, `Dialog`, and `Status` surfaces, establishing a cohesive appearance across the application. Because it is declared as `private static readonly`, the instance is created once during type initialization and is shared for the lifetime of the process, acting as a default theme reference for the `ThemeManager`.
## Remarks
By centralizing the palette in a single, private field, the `ThemeManager` can apply a consistent Classic style across all major surfaces without requiring external configuration. The private visibility keeps the default theme encapsulated within the theming code, making it straightforward to introduce additional themes or swap them by adding alternative static fields or exposing a configuration mechanism in the future.
---
### DefaultTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DefaultTheme = new()
```
Represents the canonical default color theme used by the theming subsystem. As a private static readonly [`Theme`](Theme.cs.md) named `Default`, it seeds the color configuration for core surfaces (`Base`, `Menu`, `Dialog`, `Status`) so the UI maintains a consistent palette when no user-provided theme is supplied.
## Remarks
This value acts as the internal seed for all theming operations within the `ThemeManager`. Centralizing the default colors in a single `DefaultTheme` instance ensures consistent visuals across surfaces and avoids duplicating color choices. Note that while the field is `readonly`, its nested [`ThemeColors`](Theme.cs.md) objects may still be mutable at runtime, depending on their mutability; consuming code should not rely on deep immutability unless enforced by the type definitions. The arrangement guarantees uniform behavior for the `Base`, `Menu`, `Dialog`, and `Status` color states (foreground, background, and focus states).
## Notes
- Although the field is `readonly` at the top level, the nested [`ThemeColors`](Theme.cs.md) instances may be mutated; treat this as a potential mutation point.
---
### DraculaTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DraculaTheme = new()
```
DraculaTheme is a preconfigured [`Theme`](Theme.cs.md) instance that encodes the Dracula color palette for the UI. Declared as a private static readonly field named `DraculaTheme`, it defines a single, shared palette used by the application to color the core surfaces — `Base`, `Menu`, `Dialog`, and `Status` — with per-surface mappings such as foregrounds, backgrounds, and focus colors that collectively establish a cohesive, dark interface with magenta accents on focus. With `Name` set to Dracula, this theme provides a consistent Dracula aesthetic across the application.
## Remarks
Centralizes the Dracula color choices in one place to ensure visual consistency across surfaces and to simplify theme swapping by the `ThemeManager` without recalculating colors at render time. The per-surface [`ThemeColors`](Theme.cs.md) definitions govern how content appears on the main areas (`Base`), the navigation (`Menu`), popups (`Dialog`), and status indicators (`Status`).
## Notes
- The nested [`ThemeColors`](Theme.cs.md) objects may be mutable; treat DraculaTheme as effectively immutable only if those types are immutable, or clone before modification if variations are needed.
- Because the field is private, external code cannot reference it directly; expose an accessor or copy if you need to reuse this theme outside its containing class.
---
### GruvboxTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
@@ -546,20 +321,111 @@ private static readonly Theme GruvboxTheme = new()
```
GruvboxTheme is a private, static, readonly Theme instance that encodes the Gruvbox color palette for the EchoHub client UI. It defines colors for core regions—Base, Menu, Dialog, and Statuseach with a Foreground, Background, FocusForeground, and FocusBackground value. This single object acts as the canonical Gruvbox styling source consumed by the theming subsystem to render a consistent look across the application. Because the field is private and readonly, external callers should rely on ThemeManager's public mechanisms to obtain themed resources rather than mutate or reference this field directly.
GruvboxTheme is a private static readonly [`Theme`](Theme.cs.md) that defines the Gruvbox color palette used by the UI. It initializes `Name` to "Gruvbox" and provides color configurations for the core UI regions via `Base`, `Menu`, `Dialog`, and `Status`, each specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values.
## Remarks
By centralizing the palette in one immutable object, GruvboxTheme reduces drift between UI regions and simplifies theming changes. The per-region color groups reflect a clean separation of concerns: Base handles the main chrome, Menu for navigation, Dialog for modal surfaces, and Status for status indicators; the consistent focus colors ensure accessible emphasis when keyboard navigation occurs. This pattern makes it straightforward to swap themes by replacing the underlying Theme instance without scattering color literals throughout the code.
GruvboxTheme serves as a single source of truth for the Gruvbox palette, making it easy to apply the same colors across `Base`, `Menu`, `Dialog`, and `Status` without duplicating literals elsewhere. Because the field is `static` and `readonly`, the palette is established once during type initialization and cannot be mutated at runtime, ensuring a consistent theme until a deliberate change is made in code. External code relies on the public theming surface to apply the Gruvbox palette; GruvboxTheme itself remains a private, immutable foundation for that surface.
## Notes
- The readonly reference prevents re-assignment, but if Theme or ThemeColors are mutable, their values can still be mutated at runtime.
- This field is private; there is no direct public API here—consumers should obtain theme data via ThemeManager's public surface rather than accessing GruvboxTheme directly.
- Private field scope means external code cannot reference `GruvboxTheme` directly; use the public theming API (e.g., `ThemeManager`) to switch or retrieve themes.
---
## NordTheme
### HackerTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HackerTheme = new()
```
HackerTheme is a private static readonly instance of [`Theme`](Theme.cs.md) that defines the 'Hacker' color scheme used by the UI. It centralizes the color configuration for the core regions—`Base`, `Menu`, `Dialog`, and `Status`—by specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to deliver a cohesive hacker aesthetic across the interface, and is reused internally rather than rebuilt for each component.
## Remarks
By housing the entire color palette in a single static field, the code ensures visual consistency across all UI surfaces that adopt this theme. The `HackerTheme` instance is created once at class initialization and referenced wherever a [`Theme`](Theme.cs.md) is needed within the theme system, promoting reuse and reducing the risk of divergent color values. Keeping this configuration private reinforces encapsulation: external code cannot mutate the theme inadvertently, preserving the intended appearance.
---
### HighContrastTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HighContrastTheme = new()
```
Defines a preconfigured [`Theme`](Theme.cs.md) instance named `HighContrast` that drives a high-contrast UI palette. It is exposed internally as a private static readonly field `HighContrastTheme` and initializes the `Base`, `Menu`, `Dialog`, and `Status` surfaces with a dark background (`Black`) and bright foreground (`BrightYellow`), while tuning region-specific focus colors to preserve legibility. Because it is static and readonly, the theme is constructed once and reused by the UI theming system rather than rebuilt at runtime.
## Remarks
This field acts as a canonical, immutable high-contrast palette for the theming subsystem. By centralizing the color choices for `Base`, `Menu`, `Dialog`, and `Status`, it ensures consistent accessibility-friendly visuals across the application and prevents drift between components. Its private visibility indicates it is an internal implementation detail of the theme infrastructure, intended to be consumed by the theme-management logic rather than by consumer code directly.
## Notes
- The `HighContrastTheme` is immutable after initialization due to `readonly`; runtime theme switching would require a separate mechanism to swap themes.
---
### JsonOptions
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly JsonSerializerOptions JsonOptions = new()
```
Defines a shared `JsonSerializerOptions` instance named `JsonOptions` used by the `ThemeManager` to serialize theme data with consistent formatting. It configures pretty-printed JSON by setting `WriteIndented` to true and enforces camelCase property names by using `PropertyNamingPolicy` via `JsonNamingPolicy.CamelCase`.
## Remarks
By making the field `static` and `readonly`, the class ensures a single, immutable source of serialization configuration for all calls within the ThemeManager, reducing duplication and the risk of inconsistent formatting. This centralization also minimizes drift if multiple serialization sites exist in the class.
## Notes
- Do not mutate `JsonOptions` after initialization; although `JsonSerializerOptions` properties are mutable, the field is intended to be consumed as a fixed configuration.
- If a one-off operation requires a different formatting (e.g., a different naming policy or indentation), create and use a separate `JsonSerializerOptions` instance instead of modifying this field.
---
### LightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme LightTheme = new()
```
The `LightTheme` field provides a concrete, immutable light color scheme used by the theming system. It centralizes color definitions for the main UI surfaces: `Base`, the `Menu`, `Dialog`, and `Status` areas, ensuring consistent foreground/background combinations across the application and predictable focus states.
With `Name` set to `Light` and color pairs like `Foreground`/`Background` and `FocusForeground`/`FocusBackground` defined per surface, it enables the ThemeManager to apply the light theme quickly without reconstructing the palette each time.
## Remarks
By keeping the field `private static readonly`, the code guarantees a single, shared instance of the light theme that cannot be modified at runtime, avoiding drift between components. This centralization also clarifies the intended visual identity for the light mode and reduces duplication whenever a light theme is needed.
---
### MonokaiTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme MonokaiTheme = new()
```
MonokaiTheme is a private static readonly field that encapsulates the internal Monokai color palette used by the UI. It defines a single [`Theme`](Theme.cs.md) named `Monokai` with dedicated [`ThemeColors`](Theme.cs.md) for `Base`, `Menu`, `Dialog`, and `Status`, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to ensure the interface presents a cohesive look.
## Remarks
MonokaiTheme centralizes the Monokai palette for the UI, providing a single source of truth for the [`Theme`](Theme.cs.md) the `ThemeManager` applies across components. Its private static readonly scope ensures a stable, class-wide instance isn't exposed or replaced by external code, preserving the intended appearance. If internal code mutates the nested [`ThemeColors`](Theme.cs.md) objects, the look could drift, so treat the instance as effectively immutable after initialization.
## Notes
- `readonly` prevents reassigning the field, but nested color objects may still be mutated; ensure internal code avoids mutating the theme after initialization or consider making the color data immutable.
---
### NordTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
@@ -568,18 +434,36 @@ private static readonly Theme NordTheme = new()
```
NordTheme defines the internal, immutable Nord color palette used by ThemeManager to style the UI. It is a single Theme instance configured with per-surface color mappings (Base, Menu, Dialog, Status) so the Nord look is applied consistently without duplicating color definitions throughout the code.
A private static readonly [`Theme`](Theme.cs.md) named `NordTheme` encodes the Nord color palette for the UI. It initializes `Base`, `Menu`, `Dialog`, and `Status` color schemes with explicit foreground and background values, serving as an immutable, centralized Nord appearance that the theme system can apply when Nord is active.
## Remarks
This symbol centralizes the Nord appearance, providing a single source of truth for foreground/background and focus colors across different UI surfaces. It is private to ThemeManager, which means external code should interact with the public theming API rather than reference or mutate this instance. The approach reduces drift between surfaces and makes it easy to switch themes by swapping higher-level theme providers rather than tweaking individual components.
NordTheme acts as a self-contained Nord theme preset, isolating color mappings for core UI regions. Because it is `static` and `readonly`, the palette is stabilized at startup, ensuring consistent visuals across the app when Nord is selected. Each region (`Base`, `Menu`, `Dialog`, `Status`) groups foreground/background pairs, making future tweaks localized to this single field.
## Notes
- Be aware that the readonly modifier applies to the field reference; nested ThemeColors instances may still be mutable if their properties expose setters. If true immutability is required, consider making Theme and ThemeColors immutable or returning defensive copies.
- Since `NordTheme` is `private`, external code cannot reference it directly; if runtime theme switching is needed, introduce a public API or factory to expose a Nord palette.
---
## RosePineTheme
### OceanTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme OceanTheme = new()
```
The private static readonly field `OceanTheme` is a [`Theme`](Theme.cs.md) instance configured with a named palette Ocean and dedicated [`ThemeColors`](Theme.cs.md) for its `Base`, `Menu`, `Dialog`, and `Status` sections. It is initialized inline with specific color tokens such as `BrightCyan`, `DarkBlue`, and `DarkCyan` to ensure a cohesive, visually distinct look across the UI. Being `static readonly` means this instance is created once at type initialization and cannot be reassigned, serving as an internal, consistent theme blueprint for the `ThemeManager`.
## Remarks
This field encapsulates a concrete theme configuration that `ThemeManager` uses internally, without exposing mutable defaults to consumers. Centralizing color mappings for `Base`, `Menu`, `Dialog`, and `Status` in a single private field reduces duplication and promotes visual consistency across the UI. Because the field is private, external code cannot reference or alter it directly; changes must go through the public theming API, preserving encapsulation.
---
### RosePineTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
@@ -588,14 +472,88 @@ private static readonly Theme RosePineTheme = new()
```
RosePineTheme is a private static readonly Theme instance that encapsulates the RosePine color palette used by the UI. It defines per-area color configurations for Base, Menu, Dialog, and Status, pairing foreground and background colors with their focused variants. This centralized definition provides a single source of truth for the RosePine look and is consumed by the theming subsystem rather than by external code, helping maintain a cohesive visual style across the application.
RosePineTheme is a private static readonly [`Theme`](Theme.cs.md) instance named `RosePine` that encodes a RosePine color palette for the UI. It defines color roles for `Base`, `Menu`, `Dialog`, and `Status` via nested [`ThemeColors`](Theme.cs.md) objects, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values. This single, prebuilt object lets the rest of the UI apply a cohesive RosePine appearance without reconstructing a [`Theme`](Theme.cs.md) from scratch.
## Remarks
Centralizes theme-related color data to ensure visual consistency and to simplify theme swapping or adjustment. Keeping the field private hides implementation details from consumers and enforces usage through the theming infrastructure, reducing the risk of accidental divergence in color usage.
Centralizes the RosePine aesthetic in one place, ensuring consistent color usage across the core chrome (`Base`, `Menu`, `Dialog`, `Status`). As a private static field, it is intended for internal composition by the theme system, reducing boilerplate when constructing themes at runtime. If you need to expose it externally, you would typically wrap or copy it behind a public API.
## Notes
- The field is private; external code cannot reference RosePineTheme directly.
- The field is readonly in reference, but its internal properties may be mutable depending on ThemeColors' mutability; if ThemeColors exposes setters, the palette could be modified after initialization.
- Static initialization order and potential side effects: If ThemeManager relies on RosePineTheme during application startup, ensure initialization order is correct.
- Although the field is `readonly`, the nested [`ThemeColors`](Theme.cs.md) instances may still be mutable if their properties have setters. Treat the object as immutable; avoid mutating to preserve a consistent RosePine theme.
- The field is private, so external consumers cannot reference `RosePineTheme` directly; changes to the theme would require a public accessor or method in `ThemeManager`.
---
### SolarizedTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme SolarizedTheme = new()
```
This field defines the pre-defined Solarized color theme as a private, static, readonly [`Theme`](Theme.cs.md) instance named `SolarizedTheme`. It bundles color roles for the base chrome, menus, dialogs, and status areas, providing a centralized Solarized palette that the theming subsystem can apply to the UI. The `private static readonly` designation ensures a single, immutable instance is created at startup, guaranteeing consistent visuals across the application.
## Remarks
Having a single [`Theme`](Theme.cs.md) instance for Solarized encapsulates the palette in one place, reducing duplication of color literals across UI surfaces. By separating the colors into `Base`, `Menu`, `Dialog`, and `Status` groups, the theme clearly communicates how each UI surface should appear and simplifies future tweaks. This private field serves as an internal canonical source for the Solarized look within the codebase and is consumed by the theming pipeline without exposing implementation details publicly.
---
### ThemeDir
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly string ThemeDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes")
```
ThemeDir stores the path to the per-user themes directory for the EchoHub client. It is initialized once at type initialization by combining the user's home directory (obtained via `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`) with the relative path `".echohub/themes"`, yielding a stable, user-scoped base for reading or enumerating theme assets.
## Remarks
- By centralizing the path construction, this private static readonly field reduces duplication and ensures all theme IO uses the same base directory.
- It encodes the assumption that themes are stored under the user's profile, which keeps user-specific customization isolated from system-wide resources.
- The static readonly nature means the value is fixed after initialization, simplifying reasoning about its value and caching theme metadata.
## Notes
- If the environment lacks a user profile directory, `Environment.GetFolderPath` may return an empty string, which would yield an invalid `ThemeDir`. Calling code should validate the path before attempting IO.
- It is a private field; external code cannot rely on this path and must use public APIs provided by the class for theme access.
---
### TransparentLightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentLightTheme = new()
```
Defines a concrete [`Theme`](Theme.cs.md) named `TransparentLight` with per-surface color rules for `Base`, `Menu`, `Dialog`, `Status`, and `Border` via [`ThemeColors`](Theme.cs.md). Each surface is configured with `Foreground`, `Background`, and `FocusForeground`/`FocusBackground` values to yield a light, nearly transparent appearance on the host UI: most surfaces use `Background = "None"`, while `Dialog` uses a light gray background and blue focus accents. This field is `private static readonly`, initialized once and used internally by the theming system to provide the `TransparentLight` theme.
## Remarks
By centralizing the color definitions for a light, semi-transparent appearance, this field enables consistent theming across the UI without scattering color literals throughout the code. Because it is `private`, external code cannot directly reference it; the surrounding theme infrastructure can expose higher-level theme switching that pulls from this internal variant. The immutable reference helps ensure the theme is not accidentally replaced at runtime, though the nested [`ThemeColors`](Theme.cs.md) instances may still be mutated if their properties are writable.
## Notes
- The `readonly` modifier prevents reassignment of the field, but the nested [`ThemeColors`](Theme.cs.md) objects could still be mutated if their properties have setters; avoid mutating them at runtime to preserve theme consistency.
---
### TransparentTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentTheme = new()
```
TransparentTheme is a private, static readonly instance of [`Theme`](Theme.cs.md) that encodes the glassy, transparent UI aesthetic named 'Transparent' and is intended for internal use by the theming system rather than as a public theme. It defines color settings for `Base`, `Menu`, `Dialog`, `Status`, and `Border` to deliver a cohesive appearance, with muted `Border` colors to preserve the translucent look.
## Remarks
TransparentTheme centralizes the palette for the glassy style in a single immutable object, reducing duplication across components. As a private field, it serves as an internal predefined palette that the theming system can apply without exposing a public API. This encapsulation makes it easy to tweak the look in one place while keeping the public surface stable.
---
@@ -8,12 +8,22 @@ public static partial class ChatColors
```
Shared color attributes and a small parsing helper for chat rendering. Use this class when rendering chat UI elements (timestamps, system messages, mentions, channel references, embeds, attachments, etc.) so all parts of the UI use a consistent set of Attribute values. Call SplitMentions when you need to break a message into colored segments so mentions (@user) and channel references (#channel) can be rendered with their accent colors while non-special text uses a supplied default.
Shared color attributes and small text-processing helpers used by the chat UI. Use `ChatColors` when you need a consistent set of `Attribute` values for things like timestamps, system messages, mentions, channel references, embeds and file/audio accents, or when you need to split a message into [`ChatSegment`](ChatSegment.cs.md)s that mark `@`-mentions and `#`-channel references for rendering.
## Remarks
ChatColors centralizes the visual styling for chat components and includes a utility to split text into ChatSegment pieces that carry color information. SplitMentions performs a two-pass parse: first it finds @mentions (avoiding emails by requiring no preceding word character) and marks them with MentionTextAttr; then it examines the remaining, non-mention segments to find #channel references (the regex requires at least one letter to avoid matching hex colors or numeric issue references) and marks those with ChannelRefAttr. All Attribute instances are readonly and intended as shared, immutable style tokens that renderers can reuse.
`ChatColors` centralizes the visual palette and simple parsing rules for chat rendering so callers don't duplicate color choices or regex logic. The static `Attribute` fields (for example `TimestampAttr`, `SystemAttr`, `MentionTextAttr`, `ChannelRefAttr`, `RailAttr`, `DateRuleAttr`, and `UnreadMarkerAttr`) are intended to be reused by rendering code. The `SplitMentions` method performs a two-pass split: it first extracts `@`-mentions (giving them `MentionTextAttr`) and then, only inside segments that were not already colored as mentions, highlights `#`-channel references with `ChannelRefAttr`. The regex helpers are implemented via `GeneratedRegex` methods (`MentionRegex` and `ChannelRefRegex`) so they are compiled at build time.
## Example
```csharp
// Split a message and inspect segments; mention and channel fragments receive attributes
var message = "Hey @alice, check #general and #123 -- also email alice@example.com";
var segments = ChatColors.SplitMentions(message, defaultColor: null);
foreach (var seg in segments)
Console.WriteLine($"[{seg.Color}] {seg.Text}");
```
## Notes
- The mention regex uses a negative lookbehind (?<!\w) so strings like "me@domain" are not treated as @mentions.
- The channel regex requires at least one ASCII letter to avoid matching plain hex colors or purely numeric tokens.
- SplitMentions accepts a nullable defaultColor; callers should handle null when rendering (null means "no explicit attribute supplied").
- `SplitMentions` treats the optional `defaultColor` as the fallback attribute for non-special text; passing `null` means segment `Color` values may be `null` and the caller must handle that when rendering.
- The `MentionRegex` uses `(?<!\w)@...` to avoid matching emails, and the `ChannelRefRegex` requires at least one ASCII letter to avoid matching hex colors or bare numbers (so some international or non-ASCII usernames/channels may not match).
- Mentions take precedence: because `SplitMentions` colors `@`-matches in the first pass, any `#` inside an already-colored mention will not be reprocessed in the second pass.
@@ -19,36 +19,17 @@ public partial class ChatLine
```
Represents a single rendered chat line made up of colored ChatSegment pieces and associated display metadata. Use ChatLine when preparing or manipulating a line for rendering in the chat view (layout, wrapping, attachment actions, separators, mention/highlight state) rather than working with raw strings or segments directly.
A single visual line in the chat view composed of one or more colored [`ChatSegment`](ChatSegment.cs.md)s. Use `ChatLine` when preparing data for rendering or layout (wrapping, separators, attachments, and reply jump targets) rather than when working with raw message text; it carries both the display segments and metadata the view needs (attachment info, rule labels, continuation/indent hints, and navigation markers).
## Remarks
ChatLine is the view-level unit for a message or a rule separator: it aggregates ChatSegment instances (text + color), stores metadata such as MessageId, sender, attachment info and clickable action spans, and exposes logic to break the line into multiple display lines that fit a viewport width. It centralizes presentation concerns (continuation indentation, colored continuation prefixes, non-wrapping rule lines, and unread-marker behavior) so the chat rendering layer can ask a ChatLine to produce the wrapped pieces it needs rather than implementing wrapping and metadata handling itself.
## Example
```csharp
// Construct from plain text
var line = new ChatLine("Hello, world!");
// Optional metadata
line.MessageId = Guid.NewGuid();
line.SenderUsername = "alice";
// Wrap to a viewport width of 40 columns, with a 4-space continuation indent
var wrapped = line.Wrap(40, continuationIndent: 4);
// Construct from explicit segments (preserves per-segment color attributes)
var segments = new List<ChatSegment>
{
new ChatSegment("[alice] ", ChatColors.RailAttr),
new ChatSegment("This is a message", null)
};
var coloredLine = new ChatLine(segments);
```
`ChatLine` models what the UI actually renders: a sequence of colored segments in `Segments` plus a small set of rendering hints and metadata. It centralizes information the view needs for word-wrapping (`TextLength`, `Wrap`, `ContinuationIndent`, `ContinuationPrefixSegments`), special-line rendering (`RuleLabel`, `RuleAttr`, `IsUnreadMarker`), and attachment/interactivity (`AttachmentUrl`, `AttachmentFileName`, [`AttachmentKind`](../../../EchoHub.Core/Models/AttachmentKind.cs.md), `ActionSpans`). The `Wrap` method produces multiple `ChatLine` instances that fit a given column `width`, and `JumpToMessageId` links reply-quote lines back to their source message when present in the loaded history.
## Notes
- RuleLabel makes the line a separator rule; such lines are not word-wrapped and are regenerated to the viewport width by the view.
- If ContinuationPrefixSegments is set, it overrides ContinuationIndent: continuation lines use the prefix segments' column width instead of plain-space indentation.
- ActionSpans (when present) are column positions relative to the unwrapped line; only the first wrapped line preserves those spans — subsequent wrapped continuation lines do not.
- Wrapping respects grapheme clusters and column widths (uses GetGraphemes and GetColumns), so wide characters and combining sequences are handled when measuring width. If width <= 0 or the line already fits, Wrap returns the original line in a single-element list.
- `TextLength` is computed once in the constructors (via `GetColumns()` on the provided text/segments). Because `Segments` is a mutable `List<ChatSegment>`, mutating `Segments` after construction will not update `TextLength`; keep them consistent or recreate the `ChatLine`.
- If `ContinuationPrefixSegments` is set it takes precedence over `ContinuationIndent` when computing the indent for continuation lines; the prefix's column width is used instead of the plain-space indent.
- `ActionSpans` columns are relative to the unwrapped line, so only the first wrapped line preserves clickable sub-line targets; setting `ActionSpans` to `null` means the whole line should use the kind's default action.
---
@@ -69,14 +50,15 @@ public readonly record struct AttachmentActionSpan(int StartCol, int EndCol, Att
| `Action` | `AttachmentAction` | — |
It encodes an inclusive horizontal span on a chat line that maps to an AttachmentAction when clicked. This readonly record struct pairs StartCol and EndCol (both inclusive) with an Action to designate a specific clickable region that triggers an attachment operation.
Represents an inclusive range of columns on a single chat line that, when clicked, triggers the given `AttachmentAction`. This lightweight, immutable value type pairs a `StartCol`, an `EndCol`, and an `AttachmentAction` to describe what should happen if a user interacts with that span during chat rendering or interaction handling.
## Remarks
Because it's a value type with immutable fields, AttachmentActionSpan is cheap to copy and compare, which helps with hit-testing and rendering across frames. It expresses the intent of interactive regions alongside their coordinates and associated action, keeping the UI layer decoupled from how actions are executed. This symbol complements other line-rendering data structures that describe clickable spans, enabling straightforward collection, filtering, and application during rendering.
This abstraction decouples the definition of clickable regions from the actions they perform, allowing the chat UI to map user interactions to behavior without embedding logic in the rendering layer. As a `readonly record struct`, it is cheap to copy and supports value-based equality, which makes it convenient to accumulate multiple spans in collections or pass them through APIs without risking unintended mutation. The actual interpretation of the `AttachmentAction` is delegated to higher-level components that handle click events, enabling reuse across different chat layouts or themes.
## Notes
- EndCol is inclusive; ensure range checks treat EndCol as inclusive to avoid off-by-one errors.
- Overlapping spans may require careful resolution logic at render or hit-test time to determine which action should fire.
- The range is inclusive; ensure `EndCol >= StartCol` before constructing an instance.
- Being a `readonly` record struct, instances are immutable; treat them as value-identity objects rather than mutable state.
- The spans should align with the chat line rendering coordinate space; changes in layout or font metrics may require revalidation of column mappings to avoid misaligned interactions.
---
@@ -93,28 +75,26 @@ public enum AttachmentAction
```
An enum that represents the action a click on an attachment line can trigger in the chat UI. It lets the click handler distinguish between opening the image for viewing and saving the image to disk, promoting explicit, testable logic rather than ad-hoc behavior.
Represents the user action triggered by clicking an attachment line in the chat UI. It encodes the two currently supported outcomes for image attachments: opening the image for viewing or saving it to disk.
## Remarks
By codifying the possible outcomes as an enum, AttachmentAction defines a clear contract for how attachment clicks should be handled. It decouples the UI event from the concrete actions, making it easy to extend with new options (for example, ShareImage) without changing call sites. This abstraction supports consistent behavior across different chat lines and simplifies testing by allowing mocks or verifications based on the enum value.
This enumeration decouples the click-handler from concrete UI behavior, enabling a single dispatch to determine what to do with an attachment. It also makes future extension easier; adding new actions (for example, copying a link or sharing) would be done by extending this enum and updating the handlers accordingly.
## Example
```csharp
AttachmentAction action = /* determined by UI context */;
switch (action)
AttachmentAction action = AttachmentAction.OpenImage;
if (action == AttachmentAction.OpenImage)
{
case AttachmentAction.OpenImage:
// Open the image in a viewer
break;
case AttachmentAction.SaveImage:
// Persist the image to disk
break;
// Open the image for viewing
}
else if (action == AttachmentAction.SaveImage)
{
// Persist the image to disk
}
```
## Notes
- If you later add actions to the enum, remember to handle them in all switch expressions and tests.
- Prefer explicit enum-based logic over string-based representations to avoid misinterpretation.
- Ensure UI-to-action mappings are consistent across chat lines to prevent user confusion.
- Adding new values requires revisiting all switch/if chains that enumerate the actions.
- Exhaustive checks are safer; consider a default fallback to surface unknown actions gracefully.
---
@@ -8,12 +8,12 @@ public class ChatListSource : IListDataSource
```
A list-backed data source for chat messages that implements IListDataSource and performs grapheme-aware rendering with per-segment coloring, mention-background highlighting, and a focus-based full-row highlight. Use this when supplying chat messages to a ListView-like control that expects the data source to manage items, raise collection-change notifications, and draw each row with segment-level attributes and correct column clipping.
A list data source that stores [`ChatLine`](ChatLine.cs.md) instances and renders them into a UI list with per-segment coloring and mention highlighting. Reach for `ChatListSource` when you need an `IListDataSource` implementation that maintains chat-specific layout state (like `MaxItemLength`) and performs per-grapheme drawing of `ChatLine.Segments` so segment colors and mention backgrounds are respected during rendering.
## Remarks
ChatListSource maintains an internal `List<ChatLine>`, tracks the longest item via MaxItemLength, and raises a CollectionChanged (Reset) event whenever the collection is modified unless SuspendCollectionChangedEvent is set. Its Render implementation is grapheme-aware (uses GraphemeHelper.GetGraphemes and each grapheme's column width) and applies attributes per ChatSegment: a Focus attribute (when the row is selected and the list has focus) or the segment's color with fallbacks for missing backgrounds. If a ChatLine.IsMention is true the renderer uses ChatColors.MentionHighlightAttr.Background to override segment backgrounds and to fill the remainder of the row.
`ChatListSource` maintains an internal `List<ChatLine>` (`_lines`) and exposes simple mutation operations (`Add`, `AddRange`, `InsertRange`, `Clear`) while tracking the longest item in `MaxItemLength`. It raises `CollectionChanged` (unless `SuspendCollectionChangedEvent` is set) so UI consumers can refresh efficiently; `AddRange`/`InsertRange` and `Clear` invoke `RaiseCollectionChanged` only once after the batch operation. The `Render` implementation iterates each `ChatLine.Segments`, chooses an `Attribute` per segment (falling back to the list's `VisualRole.Normal` attribute or applying `ChatColors.MentionHighlightAttr.Background` when `ChatLine.IsMention`), and draws graphemes using `GraphemeHelper` while respecting `viewportX` and `width`. The class intentionally leaves `IsMarked`/`SetMark` as no-ops and has an empty `Dispose`.
## Notes
- GetLine returns null for out-of-range indices; callers should validate the index first.
- IsMarked/SetMark are intentionally no-ops in this implementation and Dispose is a no-op — no per-item mark state or unmanaged cleanup is performed.
- MaxItemLength is updated only when lines are added/inserted; mutating a ChatLine.TextLength after insertion will not update MaxItemLength automatically. Use SuspendCollectionChangedEvent to batch updates and suppress the Reset event during bulk changes.
- `MaxItemLength` is only increased when lines are added and reset only by `Clear`. There is no removal API that updates `MaxItemLength`, so it can become stale if items are removed or if existing `ChatLine.TextLength` values change externally.
- `GetLine(int)` returns `null` for out-of-range indexes, but `Render` accesses `_lines[item]` directly; callers must ensure the `item` index passed to `Render` is valid to avoid an `IndexOutOfRangeException`.
- Setting `SuspendCollectionChangedEvent` suppresses `CollectionChanged` invocations while mutations occur, but mutations still apply immediately to the internal list. Consumers that suppress events must ensure the UI is refreshed after re-enabling events (the next mutating call will raise `CollectionChanged` unless `SuspendCollectionChangedEvent` remains true).
@@ -43,8 +43,8 @@
- [SystemHeaderSegments](#systemheadersegments)
- [UnreadMarkerRule](#unreadmarkerrule)
- [WordWrap](#wordwrap)
- [ContentIndentCols](#contentindentcols)
- [NickColWidth](#nickcolwidth)
- [ContentIndentCols](#contentindentcols)
---
@@ -57,15 +57,15 @@ public sealed class ChatMessageManager
```
Manages in-memory chat message storage, formatting and mutation for per-channel chat views. Use this when you need a single place to append formatted ChatLine objects, track per-channel unread counts and mention state, and notify the UI layer of any message-list changes via the MessagesChanged event.
Manages in-memory storage, formatting and mutation of chat messages for the UI. Use `ChatMessageManager` when the UI needs a single authoritative source of formatted [`ChatLine`](ChatLine.cs.md) objects per channel (instead of rendering raw [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md)), together with built-in tracking for unread counts, @mentions, and the "new messages" anchor; the manager raises the `MessagesChanged` event to notify views after any change.
## Remarks
ChatMessageManager is the authoritative owner of channel message lists and the policies around unread/mention tracking and visual markers. It centralizes: formatting incoming MessageDto values into ChatLine instances (including insertion of day-boundary/date rules and continuation indentation), per-channel unread counters and "new messages" anchors, mention detection for the configured CurrentUser, and a persisted LastReadIds map that an external orchestrator can seed or store. The MessagesChanged event is fired with the channel name after any mutation so the UI can refresh only the affected view.
`ChatMessageManager` is the UI-layer message store and formatter: it converts incoming [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) instances into [`ChatLine`](ChatLine.cs.md) entries (including attachments, continuation lines and mention detection), keeps per-channel lists (`_channelMessages`), and maintains per-channel state such as `_channelUnread`, `_channelLastDate`, `_markedChannels`, `_markerAnchor`, `_mentionChannels`, `_lastRead` and `_channelNewestId`. It exposes read-only views like `LastReadIds` and `MentionChannels`, publishes `MessagesChanged` (the event handler receives the channel name) after mutations, and defines layout constants `NickColWidth` and `ContentIndentCols` used when preparing [`ChatLine`](ChatLine.cs.md) content. Leaving the active channel consumes the current "new messages" marker and treats visible messages as read (see `CurrentChannel` behavior). System and status messages are added via `AddSystemMessage`/`AddStatusMessage` with colored styling (the implementation uses color attributes to build [`ChatLine`](ChatLine.cs.md) segments).
## Notes
- Changing CurrentChannel has side effects: leaving a channel consumes its "new messages" marker and marks messages visible up to that point as read (this mirrors irssi-like behavior). Subscribe to MessagesChanged to react to those updates.
- SetCurrentUser and SetChatWidth should be populated by the host before relying on mention highlighting or line wrapping/continuation; the manager uses the current user and the configured width when formatting lines.
- LastReadIds is exposed as a read-only dictionary but is expected to be persisted/seeded externally (the manager exposes per-channel last-read message IDs so the orchestrator can restore unread/mention state across restarts).
- `ChatMessageManager` has no internal synchronization in the implementation; treat it as single-thread/UI-thread affinity or ensure callers serialize access to avoid race conditions.
- The internal `GetUnreadCounts()` returns the live `_channelUnread` dictionary (not a defensive copy); callers outside the defining assembly should not mutate it and consumers inside the assembly should treat it as the authoritative store.
- `LastReadIds` is exposed as an `IReadOnlyDictionary<string, Guid>` and is intended to be persisted/seeded by the orchestrator so unread/mention state can be restored across reconnects.
---
@@ -78,16 +78,10 @@ public string CurrentChannel
```
CurrentChannel exposes the actively selected chat channel and drives the UI-facing unread and mention-detection logic. When you switch channels, the setter clears the unread marker and marks the previous channel as read before updating the active channel reference, ensuring the old channel is considered read and the new channel becomes the current focus.
The `CurrentChannel` property tracks the actively viewed chat channel for unread tracking and `@mention` detection. When set to a different channel, it calls `RemoveUnreadMarker` and `MarkRead` on the old channel and then updates `_currentChannel`. This irssi-like behavior causes leaving a channel to consume its unread marker so the next burst starts fresh, while messages seen so far are considered read.
## Remarks
This property centralizes the channel-switch lifecycle, ensuring consistent unread-state handling and mention detection as users move between channels. By containing the transition effects (clearing unread markers and marking read) within the setter, it reduces the risk of scattered state mutations elsewhere in the codebase and clarifies the responsibilities of channel state management.
## Notes
- Switching channels clears unread markers for the old channel and marks it as read; the new channel's unread state remains unchanged until you leave it, which can be surprising if you expect an immediate clear on entry.
- Setting CurrentChannel to the same value is a no-op; no side effects run in that case.
- If _currentChannel is null (e.g., before any channel is selected), RemoveUnreadMarker(null) and MarkRead(null) will be invoked; depending on the implementations of those methods, this may be a no-op or require null handling.
This property centralizes per-channel unread-state transitions, preventing scattered logic across the UI. It encapsulates the behavior that leaving a channel marks it as read and clears its unread marker, aligning channel navigation with message visibility and mention detection.
---
@@ -100,15 +94,7 @@ public string CurrentUser => _currentUser
```
Exposes the name of the user currently associated with the chat message manager as a read-only string. It simply returns the value of the private backing field _currentUser, providing a lightweight way to display or log the current user's identity without altering state. Use this property when you need to show who is sending a message, tag messages in the UI, or include the user in diagnostics; since it is backed by a field, there is no additional computation beyond a simple getter.
## Remarks
CurrentUser acts as a thin surface over the internal state representing the active user. By exposing it as a property, the class avoids leaking the backing field while still providing an ergonomic, strongly-typed access point for consumer code. This is useful for displaying the current user in the chat header or tagging messages; changes to _currentUser will be immediately visible through CurrentUser because the getter reads the field value at access time. Keep in mind that if _currentUser is null, CurrentUser will be null as well, so downstream code should handle nulls accordingly.
## Notes
- No public setter is provided; updates must occur by updating the backing field _currentUser within the class.
- The value can be null if _currentUser hasn't been assigned yet.
- There is no explicit thread-safety guarantee for this getter; if _currentUser may be updated from other threads, callers should ensure visibility.
CurrentUser is a read-only property that returns the value of the private `_currentUser` field. It offers a simple accessor to retrieve the identifier of the user associated with the current chat message context, without allowing mutation. Use it when you need to display, log, or branch logic based on the active user.
---
@@ -121,23 +107,23 @@ public IReadOnlyDictionary<string, Guid> LastReadIds => _lastRead
```
LastReadIds exposes, for each channel, the ID of the last message the user has read, as maintained by the orchestrator and persisted across connections. Use it to determine which messages are new and to seed unread/mention state when the client reconnects.
LastReadIds is a read-only dictionary that maps each channel identifier to the GUID of the last message the user has read in that channel. It is persisted by the orchestrator so unread/mention state can be seeded from history on the next connect via the underlying `_lastRead` store.
## Remarks
Conceptually, this property decouples read-tracking from the UI, centralizing per-channel state in a durable dictionary that survives restarts. It relies on the orchestrator to persist history so unread markers and mentions align with the user's activity after reconnecting.
Because this property type is `IReadOnlyDictionary<string, Guid>`, callers can read per-channel last-read IDs but cannot mutate them directly. Updates to this state are performed by the orchestrator that owns `_lastRead`, ensuring a single source of truth for read progress. The dictionary's keys are channel IDs and the values are the corresponding message GUIDs used to determine which messages are considered unread or mentioned on reconnection.
## Example
```csharp
if (chat.LastReadIds.TryGetValue(channelId, out var lastReadId))
// Safe access: check if a channel has a recorded last read
if (LastReadIds.TryGetValue("general", out Guid lastReadGeneral))
{
// lastReadId is the ID of the last message the user has read in this channel
// Use lastReadId to identify messages that are newer and should be highlighted as unread.
// use lastReadGeneral
}
```
## Notes
- The dictionary is exposed as a read-only view; internal logic updates the underlying data. Do not attempt to mutate the collection from consumer code.
- If a channel isn't present in the dictionary, TryGetValue will return false; treat that as 'no stored last read' and consider all messages as potentially unread.
- Accessing a channel that has no entry via the indexer can throw `KeyNotFoundException`; prefer `TryGetValue` or check `ContainsKey` before indexing.
- This property is read-only; to update the last-read information, update the underlying store through the orchestrator that manages `_lastRead`.
---
@@ -150,15 +136,14 @@ public IReadOnlySet<string> MentionChannels => _mentionChannels
```
Exposes the set of chat channels that currently have unread mentions of the current user. The value is returned as an `IReadOnlySet<string>` and is backed by the internal _mentionChannels field. Callers typically rely on MentionChannels to drive UI indicators (such as per-channel badges or highlights) showing which channels require the user's attention. The unread-mention state is cleared when ClearUnread is invoked.
MentionChannels is a read-only view of the channels that currently have an unread @mention for the current user. It exposes the internal `_mentionChannels` as an `IReadOnlySet<string>` so UI code can display mention indicators without mutating internal state; the underlying collection is cleared by `ClearUnread` when the user acknowledges those mentions.
## Remarks
Represents a read-only view into the manager's internal tracking of unread mentions. By returning an IReadOnlySet, it prevents accidental mutation from consumer code while still letting the UI reflect up-to-date state. Updates to the set occur through internal logic; ClearUnread resets the collection to an empty state, removing all current unread mentions.
This property serves as a stable projection of unread-mention state to the UI, decoupling presentation from private state. It keeps mutation confined to internal logic while exposing a safe, read-only view of the channels requiring attention.
## Notes
- The collection is exposed as an `IReadOnlySet<string>`; callers should not attempt to mutate it. Any updates must go through internal logic that updates `_mentionChannels` and raises the appropriate UI refresh.
- This property is a live view of internal state; external code cannot mutate it directly. If the internal collection is updated, the new contents will be visible on subsequent enumeration.
---
@@ -184,15 +169,11 @@ private static List<ChatSegment> ActionHeaderSegments(string time) =>
**Returns:** `List<ChatSegment>`
ActionHeaderSegments builds the header used when rendering /me action messages in the chat UI. It returns a `List<ChatSegment>` consisting of three parts: a timestamp segment created from the provided time string, a star-prefixed nickname segment produced by PadNick("*"), and a small rail separator. This header is meant to precede the actual action content, producing a visual like a timestamp, a leading "*" in the nick column, and a divider before the action text. The method relies on ChatColors.TimestampAttr for the time and star segments, and RailAttr for the separator, ensuring the header follows the established chat theming.
Builds the header for /me action messages by composing three chat segments: the provided time string styled as a timestamp, a starred nickname via `PadNick("*")` styled with the same timestamp color, and a rail divider styled with `ChatColors.RailAttr`. It returns a new `List<ChatSegment>` that callers pass to the chat renderer to produce a consistent header for /me actions.
## Remarks
ActionHeaderSegments encapsulates the specific visuals for /me action headers, ensuring all such headers are rendered consistently across the UI. By composing the header from three standardized ChatSegment pieces and delegating nickname rendering to PadNick, it centralizes styling concerns and reduces duplication in the rendering path. The dependency on ChatColors and PadNick ties this header closely to the existing color theming and nickname formatting used elsewhere in the chat system.
This helper encapsulates the exact header layout for action messages, so changes to styling or ordering are centralized. By consistently using `ChatColors.TimestampAttr` for the time and `ChatColors.RailAttr` for the divider, it ensures a uniform appearance with other header variants. Returning a fresh list preserves the header construction as an explicit, side-effect-free operation for callers.
## Notes
- This method is private and static, serving as an internal helper for header construction during message rendering. External code cannot call it directly.
- The time parameter must be a pre-formatted display string; the method does not perform formatting or validation of the time value.
- If the visual design for action headers changes (e.g., a different marker or separator), this single method should be updated to preserve consistency across all /me action headers.
---
@@ -213,17 +194,13 @@ public void AddMessage(MessageDto message)
**Returns:** `void`
Formats and stores a received message by first formatting it into display lines and then persisting those lines under the messages ChannelName. It enforces a day-boundary rule by inserting a DateRule when the local date of the new message differs from the previous one, and it updates the latest message ID and, if applicable, the current-read pointer for the active channel. For inactive channels, it adds a one-time New Messages marker anchored to this message so a history reload can re-place it, and it increments the per-channel unread count while noting any mentions for later highlighting. Finally, it raises the MessagesChanged event to notify observers that the channels messages have updated.
Formats and stores a received [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) into per-channel history, applying day-boundary separators, updating read/unread state, and notifying listeners. It formats the message with `FormatMessage(message)`, ensures a per-channel list exists in `_channelMessages`, and inserts a date rule via `DateRule` whenever the message's local date differs from the last recorded date for that channel (derived from `message.SentAt`). It marks the message as read when it belongs to the active channel (`_currentChannel`), updates `_lastRead` and the channel's newest id, and, for inactive channels, adds an initial unread marker anchored to this message. The method then appends all formatted lines, increments the per-channel unread count, tracks mentions by checking `IsMention` on any line, and finally raises the `MessagesChanged` event for the affected channel.
## Remarks
This method centralizes all per-message mutations for the chat UI, ensuring consistent channel-state updates when new data arrives. It relies on the message.SentAt timestamp (converted to local time) to decide day boundaries and uses internal dictionaries (e.g., _channelMessages, _channelUnread, _markedChannels) to keep unread counts, markers, and last-read state in sync across channels. By anchoring an unread marker to the first unread message in inactive channels, it enables reliable re-placement on history reloads, while emitting MessagesChanged keeps the UI responsive to changes.
Centralizes the ingestion of incoming messages, coupling formatting, date segmentation, unread bookkeeping, and event propagation into a single place. This reduces scattered updates across the UI and ensures consistent behavior when messages arrive for either the active or inactive channels. It relies on internal per-channel dictionaries and sets (e.g. `_channelMessages`, `_channelLastDate`, `_currentChannel`, `_lastRead`, `_markedChannels`, `_markerAnchor`, `_channelUnread`, `_mentionChannels`, and the `MessagesChanged` event) to maintain state and emit notifications.
## Notes
- The method assumes message.ChannelName is non-null; otherwise an exception could be thrown when using dictionary keys.
- It uses ToLocalTime; time zone implications depend on the runtime environment and MessageDto's SentAt value.
- The internal state mutations are not protected by synchronization; callers should ensure serial access or add locking if called from multiple threads.
- Be mindful of concurrency: `_channelMessages`, `_channelUnread`, and related state are mutated here without explicit synchronization; callers streaming messages for the same channel concurrently should serialize updates to avoid races.
---
@@ -246,17 +223,7 @@ public void AddStatusMessage(string channelName, string username, string status)
**Returns:** `void`
Adds a status change message to a channel with colored styling. It captures a timestamp via FormatTime(DateTimeOffset.Now), constructs header segments with SystemHeaderSegments, appends a segment describing the status change in the usernames color via ChatColors.SystemAttr, ensures the channel entry exists in the internal _channelMessages store, appends a new ChatLine built from the assembled segments (including a ContinuationPrefixSegments from RailPrefix), and, if the updated channel is the currently viewed one, fires the MessagesChanged event to refresh the UI.
## Remarks
This method centralizes the presentation of user status updates as timestamped, system-colored messages within a per-channel chat history. By encapsulating the formatting (time header, system-colored status text) and the mutation of the channels message list, it promotes consistent visual styling across channels and keeps UI updates synchronized with data changes.
## Notes
- Be mindful of thread-safety: _channelMessages is mutated without explicit synchronization, so concurrent calls could race in a multi-threaded context.
- Time formatting depends on the system clock; for deterministic tests, consider controlling FormatTime/DateTimeOffset.Now or abstracting time retrieval.
Adds a status change message to a chat channel by composing a time-stamped system message that declares a users new status. It builds a header via `FormatTime(DateTimeOffset.Now)` and `SystemHeaderSegments`, appends a system-colored segment with the content "{username} is now {status}" using `ChatColors.SystemAttr`, ensures the target channel exists in `_channelMessages`, and stores a new [`ChatLine`](ChatLine.cs.md) (with its `ContinuationPrefixSegments` set by `RailPrefix()`) in that channel. If the affected channel is currently active (`_currentChannel`), it raises `MessagesChanged` to prompt the UI to refresh. This method centralizes status updates as consistently styled system messages within the chat history, shielding callers from the details of message construction and channel management.
---
@@ -278,15 +245,15 @@ public void AddSystemMessage(string channelName, string text)
**Returns:** `void`
Adds a system/informational message to a channel with colored styling. When invoked, it ensures the channel's message list exists, formats the current time, splits multi-line text so the first line appears in the header and subsequent lines are added as separate lines with a rail-style continuation prefix; if the target channel is the currently visible one, it triggers a UI refresh via MessagesChanged.
Adds a system/informational message to a named chat channel, styling the header and body with the system color attribute. It ensures the channel's message list exists, builds a timestamp with `FormatTime`, and renders multi-line text by placing the first line in a header and each subsequent non-empty line as a continuation line prefixed with `RailPrefix` and colored via `ChatColors.SystemAttr`. If the targeted channel is currently active (`_currentChannel`), it raises the `MessagesChanged` event to refresh the UI.
## Remarks
System messages are rendered with a header line that includes a timestamp, followed by body segments styled with SystemAttr. This method centralizes the formatting of such messages, so callers don't need to assemble headers or manage continuation prefixes themselves. It relies on ChatLine, ChatColors, and RailPrefix to produce a consistent visual treatment across channels.
This method centralizes the rendering policy for system messages, ensuring consistent visual treatment across channels. By composing [`ChatLine`](ChatLine.cs.md) instances from a header built with `SystemHeaderSegments(time)` and per-line continuation segments via `RailPrefix()`, it enforces a cohesive, rail-prefixed block that clearly marks informational notices. It also isolates the UI update trigger to the active channel through `MessagesChanged`.
## Notes
- Potential lack of thread-safety if called from multiple threads; the internal _channelMessages dictionary is mutated without locking.
- Only the UI refresh is raised when posting to the currently active channel (otherwise the message is updated silently).
- Lines after the first are treated as separate ChatLine entries with their own continuation prefix; blank lines are ignored.
- It uses a direct `DateTimeOffset.Now` for the timestamp, which can affect testability and determinism.
- Blank lines in the input text after the header are ignored; only non-empty lines after the first are rendered.
- The code assumes `_channelMessages` can be mutated by adding lists; thread-safety is not shown.
---
@@ -309,14 +276,14 @@ private static ChatLine AttachmentActionLine(string text, Attribute color, Attac
**Returns:** [`ChatLine`](ChatLine.cs.md)
The AttachmentActionLine method constructs a ChatLine that renders as a clickable attachment action within the chat list. It prefixes the display text with a standardized rail segment, colors the text using the provided color attribute, and attaches the underlying attachment metadata (URL, file name, and kind) so the UI can route activation to the correct behavior (play audio, download, or save the original image).
Builds a clickable attachment line carrying the metadata the message list uses to route activation (play audio, download file, save original image). It constructs a [`ChatLine`](ChatLine.cs.md) by starting with `RailPrefix()` for its segments, adds a colored `text` segment, and returns a [`ChatLine`](ChatLine.cs.md) initialized with those segments. The returned object populates `AttachmentUrl`, `AttachmentFileName`, and [`AttachmentKind`](../../../EchoHub.Core/Models/AttachmentKind.cs.md) from the provided `attachment`, and sets `ContinuationPrefixSegments` to a fresh `RailPrefix()` so continuation rails render consistently.
## Remarks
AttachmentActionLine centralizes how attachment-based actions are presented in the chat. By bundling the styling prefix, action text, and attachment metadata in a single factory, it keeps rendering and activation logic cohesive and easier to maintain. The method relies on RailPrefix to ensure consistent visual grouping and populates ChatLine's attachment properties so downstream UI and activation code can locate the URL, file name, and kind without reassembling them.
This helper centralizes how attachment actions are rendered in the chat UI. By wrapping the segment construction and attachment-metadata binding in one place, it guarantees consistent appearance and reliable routing for actions like playing, downloading, or saving attachments across the message list.
## Notes
- The method is private and static, so it is only callable within its containing type and from a known, fixed entry point.
- It sets ContinuationPrefixSegments to RailPrefix(), ensuring continuation lines align with the same action prefix; altering RailPrefix behavior might affect line wrapping or click target consistency.
- Assumes a non-null [`AttachmentDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) for `attachment`; passing null will throw a `NullReferenceException` when accessing `attachment.Url`, `attachment.FileName`, or `attachment.Kind`.
---
@@ -331,15 +298,13 @@ public void ClearAll()
**Returns:** `void`
Clears all internal message state maintained by the chat message manager. This method empties all per-channel data stores and resets the current context, providing a clean slate when disconnecting or reinitializing the chat UI. It is used during disconnect sequences to prevent stale data from persisting across sessions.
Resets all message state by clearing internal caches and resetting the current context. This method is intended to be called on disconnect to guarantee a clean slate for the next session, by clearing per-channel stores such as `_channelMessages`, `_channelUnread`, `_channelLastDate`, `_markedChannels`, `_markerAnchor`, `_mentionChannels`, `_lastRead`, and `_channelNewestId`, and by resetting `_currentChannel` and `_currentUser` to `string.Empty`.
## Remarks
By encapsulating reset logic here, the class guarantees a consistent baseline state after disconnection. It reduces the risk of partially cleared state being left behind when disconnects occur in various code paths, and it centralizes lifecycle management for chat state.
By centralizing the teardown logic in `ClearAll`, the class avoids scattered cleanup code across multiple paths. It encapsulates what it means to reset message state, so after a disconnect the object is in a well-defined, initial state ready for a new connection. This helps prevent subtle bugs caused by leftover state persisting between sessions and simplifies future maintenance.
## Notes
- Not inherently thread-safe: callers should ensure synchronization if the ChatMessageManager is accessed concurrently during disconnect.
- After invocation, there is no active channel or user until reinitialization occurs; _currentChannel and _currentUser are set to empty strings.
- This method only clears in-memory state; any external resources or persisted data are unaffected.
- Calling `ClearAll` while message processing is ongoing may cause transient inconsistencies if concurrent access occurs; coordinate with any ongoing operations or ensure proper synchronization before disconnect.
---
@@ -360,16 +325,10 @@ public void ClearChannelMessages(string channelName)
**Returns:** `void`
Clears all messages for a specific channel from the client-side chat state. If the channel exists in the internal message map, it empties that channel's message list and removes the per-channel metadata: the last date, marked channels, and marker anchor for that channel. If the channel being cleared is currently active, it raises the MessagesChanged event to notify the UI to refresh for that channel. If the channel does not exist in the map, this method is a no-op. The operation affects only in-memory state and does not touch persistent storage or other channels.
Clears all messages associated with the specified channel (`channelName`) and resets the per-channel state by clearing the collection in `_channelMessages` and removing related metadata from `_channelLastDate`, `_markedChannels`, and `_markerAnchor`. If the cleared channel matches `_currentChannel`, it triggers the `MessagesChanged` event to notify listeners to refresh the UI.
## Remarks
This method centralizes the cleanup of per-channel UI state, ensuring that clearing a channel leaves the rest of the UI in a consistent state. By clearing the per-channel dictionaries and lists alongside the messages, it prevents stale metadata from lingering after a channel's history is purged. The MessagesChanged event invocation for the current channel decouples UI refresh logic from the data update, allowing subscribers to re-render the channel view as needed.
## Notes
- No persistence: only in-memory state is cleared.
- Safe-to-call-no-op: if the channel is missing from _channelMessages, the method returns without side effects.
- Assumes non-null per-channel message list: a null collection would cause a NullReferenceException on Clear, so callers should ensure the data is initialized.
- If multiple components listen for MessagesChanged, the event will fire only when the cleared channel is the current channel; other channels won't trigger an automatic refresh from this call.
Centralizes per-channel cleanup so callers dont manually touch `_channelMessages`, `_channelLastDate`, `_markedChannels`, or `_markerAnchor`, reducing duplication and the risk of inconsistent state. By only raising the `MessagesChanged` event when the cleared channel is the active one (`_currentChannel`), it keeps UI updates efficient and scoped to the currently viewed channel.
---
@@ -390,23 +349,10 @@ public void ClearUnread(string channelName)
**Returns:** `void`
Clears the unread state for the specified channel by resetting its unread count, removing mention highlights, and marking the channel as read. This is typically invoked when the user opens or explicitly reads a channel, ensuring the UI and internal state reflect that there are no remaining unread messages for that channel.
Resets the unread state for a given channel by setting its unread counter to zero, removing any pending mention for that channel, and applying the read-state via `MarkRead`.
## Remarks
Clears three facets of unread state in a single operation: it updates the internal unread counter for the channel, removes the channel from the active mention-tracking collection, and delegates to MarkRead to apply the persisted read-state. This centralizes the read-clearing behavior so the rest of the UI can rely on a single, consistent method rather than duplicating logic at multiple call sites. The exact effects depend on the implementations of _channelUnread, _mentionChannels, and MarkRead; for example, if the channel is not yet present, the first assignment will create an entry with 0 unread, and Remove will be a no-op if the channel is not in _mentionChannels.
## Example
```csharp
// Assuming 'manager' is an instance of ChatMessageManager
manager.ClearUnread("general");
```
## Notes
- If ClearUnread is invoked for a channel that did not previously exist in the internal structures, the first line will create or overwrite an entry with a value of 0.
- The behavior of MarkRead is relied upon to finalize the read-state side effects; if MarkRead triggers additional side effects (e.g., persistence or events), those will occur as part of this call.
This is the centralized operation used when a user acknowledges messages in a channel. It ensures unread indicators and mention flags stay in sync by updating `_channelUnread`, removing the channel from `_mentionChannels`, and delegating to `MarkRead` for any additional read-state side effects.
---
@@ -427,14 +373,19 @@ private static ChatLine DateRule(DateTime date)
**Returns:** [`ChatLine`](ChatLine.cs.md)
DateRule constructs a stylized date separator line for a given date within the chat UI. It derives a label from DateRuleLabel(date) and returns a ChatLine containing a single segment that renders as "── {label} ──" using ChatColors.DateRuleAttr. The returned ChatLine also has its RuleLabel set to the label and its RuleAttr set to the same color attribute. Use this helper whenever you need a consistent, date-bounded visual divider between messages rather than composing lines manually.
DateRule takes a `DateTime` and returns a [`ChatLine`](ChatLine.cs.md) that renders a date-based separator in the chat UI. It computes a label with `DateRuleLabel(date)` and uses a single segment containing the decorative string `── {label} ──` colored by `ChatColors.DateRuleAttr`. The returned [`ChatLine`](ChatLine.cs.md) is tagged with `RuleLabel = label` and `RuleAttr = ChatColors.DateRuleAttr` for downstream styling and identification. This internal helper is used to insert consistent date separators into the chat stream.
## Remarks
By encapsulating the creation of the date rule, DateRule provides a single point of change for how date separators look and behave. It coordinates the label generation with the chat coloring to ensure separators match other UI rule lines and follow the project's styling conventions for date-related cues. This abstraction sits alongside ChatLine and ChatColors, reinforcing a uniform approach to rendering non-message chrome in the chat.
By funneling date-separator creation through this helper, the UI ensures all date rules share the same label-generation point (`DateRuleLabel`) and styling (`ChatColors.DateRuleAttr`). It constructs a new [`ChatLine`](ChatLine.cs.md) without mutating existing state, acting purely as a formatter/renderer within the chat assembly process.
## Example
```csharp
var line = DateRule(DateTime.Today);
```
## Notes
- Changes to DateRuleLabel or the decorative glyphs will affect every date separator; tests that assert exact separator text should be updated if the label generation changes.
- DateRule is private static, so its reuse is limited to the containing class; if external customization is needed, consider elevating the helper to a more accessible API or adjusting the color attribute usage in ChatColors.DateRuleAttr.
- It relies on `DateRuleLabel(date)` for the label; any change to that method changes all date separators generated by `DateRule`.
- As a private helper, it's only callable from within its containing type; external code cannot call it directly, which is intentional to keep the formatting internal.
---
@@ -455,13 +406,16 @@ internal static string DateRuleLabel(DateTime date) => date.ToString("ddd, MMM d
**Returns:** `string`
Converts a DateTime to a short, human-friendly label using the pattern 'ddd, MMM d yyyy'. This helper returns a string such as 'Tue, Jul 23 2024' and is used by the chat UI to display date labels consistently instead of formatting dates ad-hoc at each call site.
Formats the provided `DateTime` as a compact label using the pattern `ddd, MMM d yyyy` and returns the resulting string. This internal helper centralizes date-label formatting for the UI (for example, chat message headers) to ensure consistency and avoid duplicating formatting logic across call sites.
## Remarks
This method centralizes the exact format used across the chat components, ensuring consistent date labels. It is declared internal and static, indicating it's intended for internal use within the ChatMessageManager's UI rendering flow rather than as part of the public API.
This small helper centralizes the specific date-label format in one place, ensuring consistent UI labeling across chat-related components. Because it relies on `DateTime.ToString` with a culture-aware format specifier, the output respects the current culture's short day and month names; changing the style in one place will propagate wherever `DateRuleLabel` is used. It is an internal static method, so it's not part of the public API.
## Example
```csharp
string label = DateRuleLabel(new DateTime(2024, 5, 1)); // "Wed, May 1 2024"
```
## Notes
- This formatting respects the current culture; for stable, culture-independent output, supply a culture-invariant format (e.g., date.ToString("ddd, MMM d yyyy", CultureInfo.InvariantCulture)) and add a using System.Globalization.
---
@@ -483,15 +437,7 @@ private static List<ChatLine> FormatEmbed(EmbedDto embed, int chatWidth)
**Returns:** `List<ChatLine>`
Formats an embed into a vertical sequence of chat lines with a left rail and colored text, suitable for rendering inside the chat UI. It accepts an EmbedDto and the current chat width, computes the available text area, and assembles lines that begin with a fixed border segment colored by the embed border color, followed by the actual text colored per section (title or description). If present, SiteName is emitted first using the border color; Title is wrapped to the computed text width and emitted with EmbedTitleAttr; Description is wrapped similarly with EmbedDescAttr. The method returns a `List<ChatLine>` that can be rendered as part of a larger message.
## Remarks
FormatEmbed centralizes the formatting decisions for embeds in the chat UI, ensuring a consistent look by deriving the border color from the embed ThemeColor or falling back to a default border color, and by applying distinct styling to the title and description. It relies on shared utilities (WordWrap and RailPrefix) to wrap text to the computed width and to align lines with a left rail, respectively. Because this is a private helper, its usage is confined to the containing class, which helps encapsulate embed rendering and prevents drift from the surrounding chat presentation.
## Notes
- If embed.ThemeColor is an invalid hex string, the border color falls back to ChatColors.EmbedBorderAttr.
- The text width is computed from the provided chatWidth and is clamped to a minimum of 20 columns; very small chat widths may lead to tighter wrapping and more lines.
Formats an embed payload into a vertical sequence of chat lines suitable for rendering in the UI. Given an [`EmbedDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) with `SiteName`, `Title`, `Description`, and `ThemeColor`, it returns a `List<ChatLine>` that visually represents the embed by prefixing each line with a left border and applying color attributes. The method computes the available text width as `chatWidth - ContentIndentCols - borderCols`, ensuring a minimum of 20 characters, then selects the border color by calling `HexColorHelper.ParseHexColor(embed.ThemeColor)` and falling back to `ChatColors.EmbedBorderAttr` if parsing fails. A local helper `AddTextLine` prefixes lines with a rail and the border attr, then appends the actual text as a [`ChatSegment`](ChatSegment.cs.md) with the appropriate color (title, description, etc.). It emits optional sections for `SiteName` (with the border color), `Title` (wrapped via `WordWrap` to the computed width and styled with `ChatColors.EmbedTitleAttr`), and `Description` (wrapped similarly and styled with `ChatColors.EmbedDescAttr`). The result is a cohesive, themed embed block ready to be rendered alongside other chat content.
---
@@ -512,16 +458,14 @@ internal static string FormatFileSize(long? bytes)
**Returns:** `string`
Formats a nullable file size into a concise, human-readable string. If the input is null or zero, it returns a single question mark to indicate an unknown or unavailable size. For any non-null value, it chooses the most appropriate unit among bytes (B), kilobytes (KB), megabytes (MB), and gigabytes (GB) and formats the result with a single decimal place for all units except bytes. The thresholds use binary units (1024 multipliers), producing strings like '512 B', '1.5 KB', '3.2 MB', or '1.2 GB'.
Formats a file size given in bytes into a human-friendly string using `B`, `KB`, `MB`, and `GB`. If the input is `null` or `0`, it returns `?` to indicate an unknown size. This helper is used when rendering attachment sizes in the chat UI to ensure consistent units and formatting.
## Remarks
Consolidates the formatting logic so callers dont duplicate range checks or string formatting, ensuring consistent display across the UI. The function intentionally treats null or zero as unknown ("?") rather than returning a numeric zero, which is useful when the size may not be known at the point of rendering.
By centralizing the formatting logic, `FormatFileSize` ensures consistent thresholds and decimal precision across the UI, reducing duplication and easing future changes to unit boundaries or precision. It assumes non-negative input and surfaces unknown sizes as `?` for clarity in the display layer. This symbol acts as a small, focused utility within the chat message management area, decoupling size formatting from presentation concerns.
## Notes
- Null or zero input yields "?" per the early guard.
- Uses binary thresholds: 1024 B for KB, 1024^2 B for MB, and 1024^3 B for GB.
- Non-byte units are shown with one decimal place (e.g., 1.5 KB, 3.2 MB, 1.2 GB); boundary values exactly at 1024, 1024^2, etc., switch units accordingly (e.g., 1024 B becomes 1.0 KB).
- Negative values are not guarded and will format as negative sizes; callers should validate input or adapt the function before display.
- The `?` sentinel indicates unknown or unavailable size; ensure the consuming UI handles this gracefully to avoid confusing output.
---
@@ -542,7 +486,7 @@ private List<ChatLine> FormatMessage(MessageDto message)
**Returns:** `List<ChatLine>`
Formats a MessageDto into a list of ChatLine entries suitable for rendering in the chat UI. It resolves the timestamp via FormatTime, chooses a representative display name (SenderDisplayName when available, otherwise SenderUsername), and derives a nickname color using HexColorHelper or NickColorHelper. The method then builds a header line or a summarized header for attachments, handles reply quotes by inserting a preceding quote line, and supports CTCP-style /me actions by rendering a header that shows the action followed by additional lines. When content exists, the content is emoji-normalized and split into lines with mention highlighting; when there is no text, a compact header summarizes attachments. Each line receives a RailPrefix so subsequent content lines and per-attachment blocks align with the nick rail, and attachments produce their own blocks hanging off that rail (e.g., image previews).
FormatMessage formats a [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) into a structured list of [`ChatLine`](ChatLine.cs.md)s that render a single chat message in the UI. It computes the display time with `FormatTime`, derives a display name from `SenderDisplayName` or `SenderUsername`, and selects a `senderColor` via `HexColorHelper.ParseHexColor` or `NickColorHelper.GetAttribute`. It prepends a `ReplyQuoteLine` if the message is a reply, and handles action messages by using `MessageConventions.TryParseAction` and rendering an action header via `ActionHeaderSegments`, followed by action content lines. For regular content, it processes emojis with `EmojiHelper.ReplaceEmoji`, builds a header via `HeaderSegments`, and appends content and any subsequent lines as continuation blocks using `RailPrefix`. If the message has no text but attachments exist, it renders a compact header with a summary like `[image]` or `[n attachments]`. Each attachment produces its own block; image attachments render ASCII previews when available, and colorized segments when appropriate. The method is a private helper used by the chat rendering flow to translate a [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) into the visual [`ChatLine`](ChatLine.cs.md)s shown in the chat.
---
@@ -564,15 +508,10 @@ private static string FormatTime(DateTimeOffset timestamp) =>
**Returns:** `string`
Formats a DateTimeOffset timestamp into the user's local time and renders it as a compact 24-hour time string (HH:mm). By converting to local time before formatting, the method ensures that times align with the local calendar day rules, so messages near midnight are associated with the correct day in the UI. This helper is used wherever a concise, time-only indicator is needed for chat messages (for example, timestamps next to messages).
Formats a given `DateTimeOffset` into a compact local-time string by first converting to local time, then formatting with the `HH:mm` format specifier to produce hours and minutes in 24-hour form. This private helper is used wherever the UI needs a concise time-of-day display for timestamps (e.g., chat messages) and guarantees times near midnight land under the correct calendar day by applying local-time rules before formatting.
## Remarks
By centralizing locale-aware time formatting in a private helper, the code avoids duplicating ToLocalTime calls across the UI and guarantees a consistent display of chat timestamps. It is designed for presentation concerns rather than time arithmetic.
## Notes
- Relies on the system's local time zone via ToLocalTime; DST and locale settings affect the result.
- Only the time portion is produced (HH:mm); date and potential day-boundaries are resolved at a higher level in the UI.
- As a private method, its usage is confined to the containing class; if cross-cutting formatting is needed, consider extracting to a shared utility.
This abstraction centralizes locale-aware time formatting for timestamps, ensuring all UI paths render the same local time portion. It converts the `DateTimeOffset` to local time via `ToLocalTime()` before applying the `HH:mm` format, so near-midnight messages are assigned to the correct date bucket according to local rules. This reduces duplication and guards against inconsistent formatting or time-zone drift across the chat UI.
---
@@ -594,21 +533,15 @@ private List<ChatLine> FormatWithDateRules(List<MessageDto> messages, out DateTi
**Returns:** `List<ChatLine>`
Formats a chronological batch of messages into a list of ChatLine objects, inserting a date rule before the first message and whenever the day changes. The method converts each message's SentAt to local time to determine day boundaries, delegates per-message formatting to FormatMessage, and returns the assembled lines while outputting the last processed local date via lastDate.
Formats a chronological batch of messages into chat lines, inserting a date rule before the first message and at every day boundary, and returns the batchs last local date. Use this private helper when rendering a chat thread to ensure date separators are consistently inserted; it encapsulates the day-boundary logic and per-message formatting, instead of duplicating this control flow across callers.
## Remarks
Day separators help users scan conversations by calendar date, providing clear visual breaks between days. By isolating the boundary logic in this function and delegating rendering to DateRule and FormatMessage, the code remains reusable and consistent across different chat views.
## Example
```csharp
// Example: format a batch of messages into chat lines with day separators
DateTime? lastDate;
List<ChatLine> lines = FormatWithDateRules(batchMessages, out lastDate);
```
By centralizing date-boundary handling in `FormatWithDateRules`, the UI rendering path doesn't need to know how separators are produced. It exposes a simple contract: transform a list of [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) into [`ChatLine`](ChatLine.cs.md)s while emitting `DateRule`s whenever the day changes and tracking the most recent local date. The method delegates the actual per-message line construction to `FormatMessage`, keeping concerns separated between date logic and message formatting.
## Notes
- No null-check on the input list; passing null for messages will throw.
- lastDate is null if there are no messages; callers should account for a possible null value.
- Date boundaries are computed using `ToLocalTime()`, so the local time zone of the runtime determines when a new `DateRule` is inserted; messages in different time zones can shift separators accordingly.
---
@@ -629,23 +562,7 @@ public List<ChatLine>? GetMessages(string channelName)
**Returns:** `List<ChatLine>?`
Retrieves the current list of ChatLine entries for a specific channel by name from the internal message store. It returns the existing `List<ChatLine>` for the channel, or null if the channel has no messages. This is a lightweight accessor around the underlying storage and does not create a new list or clone data.
## Remarks
This method exposes the internal `List<ChatLine>` instance associated with the given channel. Callers should be aware that mutations to the returned list (adding/removing items) will affect the stored messages for that channel. If an immutable snapshot is required, consider copying the list before enumeration or modification. The method hides the details of how messages are stored, providing a single entry point that can be swapped out without changing call sites.
## Example
```csharp
var messages = chatMessageManager.GetMessages("general");
if (messages != null)
{
Console.WriteLine($"General channel has {messages.Count} messages.");
}
```
## Notes
- Returning null indicates the channel has no messages or does not exist in the store; always null-check before accessing properties like Count.
- The returned `List<ChatLine>` is not cloned; modifications to it affect the internal store unless an external copy is created.
Retrieves the `List<ChatLine>` for a given channel from the internal `_channelMessages` store using `TryGetValue`; if found, it returns the list, otherwise it returns `null`. Use this method when you need to access the messages for a specific `channelName` without risking an exception if the channel is missing.
---
@@ -666,15 +583,7 @@ public int GetUnreadCount(string channelName)
**Returns:** `int`
Returns the unread message count for the specified channel by querying the internal _channelUnread mapping. If the channel has no recorded count, it returns 0. This read-only helper encapsulates access to the underlying data and is typically used by the UI to display per-channel unread badges without exposing the dictionary directly.
## Remarks
Acts as a minimal abstraction over the unread-tracking store, hiding direct dictionary access and ensuring a zero default when a channel has no entry. The caller should understand that the value comes from the shared _channelUnread structure, so updates to unread counts elsewhere will be visible on subsequent calls; if the underlying storage is not thread-safe, callers must ensure proper synchronization.
## Notes
- Passing null as channelName will throw an ArgumentNullException from TryGetValue.
`GetUnreadCount` returns the unread message count for the specified channel by querying the internal dictionary `_channelUnread`. If the channel has no entry, it yields 0. This method encapsulates the missing-key default handling so callers can rely on a non-null int even when the channel hasn't tracked unread messages yet.
---
@@ -689,15 +598,16 @@ internal Dictionary<string, int> GetUnreadCounts() => _channelUnread
**Returns:** `Dictionary<string, int>`
Returns the internal per-channel unread counts as a mutable dictionary backed by the _channelUnread field. Use this accessor when you need to read or react to per-channel unread tallies without recomputing them, noting that the returned dictionary is the live internal collection.
Returns the internal mapping of unread message counts per channel by directly exposing the private field `_channelUnread`. This method is a minimal accessor with no additional logic, simply forwarding the reference to the underlying dictionary. Call it when you need to inspect (and potentially mutate) the live counts for all channels from within the same assembly, rather than creating a new dictionary.
## Remarks
This accessor is intended as a lightweight bridge between the internal unread-count store and UI or coordination code that needs to display or react to those counts. It avoids copying data for performance and maintains synchronization with internal updates. However, because it returns the actual dictionary, external callers can mutate the collection, potentially breaking invariants or introducing subtle bugs. If you require a read-only view, consider returning `IReadOnlyDictionary<string,int>` or a defensive copy, and adjust the signature accordingly.
By design, this is a direct forwarder to `_channelUnread`. It avoids copying for performance but couples callers to the concrete `Dictionary<string, int>` implementation and to the internal state. If you only need to observe values, prefer returning a read-only view such as an `IReadOnlyDictionary<string, int>` or provide a separate accessor that returns a defensive copy to preserve encapsulation.
## Notes
- Mutability risk: Changes to the returned dictionary affect internal state.
- Thread-safety: Concurrent updates to _channelUnread may race with external mutations; consider synchronization.
- Initialization: Ensure _channelUnread is initialized before first access to avoid NullReferenceException.
- Mutations to the returned `Dictionary<string, int>` modify the class's internal state immediately; callers should avoid assuming immutability.
- Be mindful of thread-safety: concurrent reads/writes to `_channelUnread` without synchronization can lead to race conditions or exceptions.
---
@@ -725,20 +635,7 @@ private static List<ChatSegment> HeaderSegments(string time, string nick, Attrib
**Returns:** `List<ChatSegment>`
HeaderSegments constructs the three leading pieces of a message header line: a dim timestamp, the (optionally) colored, padded nickname, and a fixed rail separator. It returns these as a `List<ChatSegment>` so the caller can render the header independently from the message body. The first segment renders the provided time string with the Timestamp attribute, the second applies a padded nickname using the supplied nickColor, and the third renders a static rail string with the Rail attribute. This centralized assembly ensures consistent header formatting across messages and keeps layout/color decisions isolated from the rest of the rendering logic. The header segments precede the actual message text, which begins after ContentIndentCols.
## Remarks
By encapsulating header composition, this method enforces consistent alignment and styling for all message headers. It isolates colorization and spacing concerns from the message content, making it easier to adjust the header's appearance in one place without touching rendering logic elsewhere.
## Example
```csharp
// Example usage within the same class context
var segments = HeaderSegments("12:34", "Alice", ChatColors.SystemAttr);
```
## Notes
- The method is private, so it cannot be called from outside its declaring type. If header construction is needed elsewhere, provide a public wrapper or move the logic to a shared utility.
- The nick color parameter is nullable, allowing callers to omit explicit coloring when desired; the rendering path should handle a null color accordingly.
HeaderSegments is a private static helper that constructs the leading portion of a chat message header. It takes a time string, a nickname, and an optional color attribute for the nickname, and returns a `List<ChatSegment>` with three segments: a timestamp segment created from `"{time} "` using `ChatColors.TimestampAttr`, a nickname segment produced by `PadNick(nick)` colored by `nickColor`, and a rail segment containing `" │ "` colored with `ChatColors.RailAttr`. The returned header prefix precedes the message body, whose content begins at `ContentIndentCols`.
---
@@ -759,14 +656,10 @@ private static ChatLine ImageActionLine(AttachmentDto attachment)
**Returns:** [`ChatLine`](ChatLine.cs.md)
Builds the action line displayed under an image preview, showing [open] and [↓ save original] as clickable actions and appending the file name with its size. Each bracketed label becomes an AttachmentActionSpan so the UI can map clicks to the corresponding action, while Enter triggers the default (open).
Builds the action line displayed under an image preview: a compact sequence like "[open] [↓ save original] name [size]" where each bracketed element is an [`AttachmentActionSpan`](ChatLine.cs.md) so it can be targeted by mouse clicks; keyboard activation (Enter) uses the default action, open. The method constructs this line by starting with a base rail prefix, incrementally adding actions with their width in columns, and finally returns a [`ChatLine`](ChatLine.cs.md) enriched with the attachment metadata and a list of action spans for interaction.
## Remarks
This symbol centralizes the rendering of image-related actions in chat messages, ensuring consistent spacing and interactivity across messages. It constructs the action regions by measuring segment widths from RailPrefix() and updating a running column index; the resulting ChatLine carries ActionSpans and attachment metadata for downstream rendering.
## Notes
- The clickable targets cover only the bracketed portions; the trailing file name and size text is not interactive.
- If you change the action labels or formatting, adjust the width calculation logic accordingly, since spans are derived from the label text width.
This helper encapsulates the visual semantics of an image-attachment action bar. By recording [`AttachmentActionSpan`](ChatLine.cs.md)s with exact column extents and pairing them with the base rail prefix, it guarantees that every image attachment presents clickable actions in a predictable layout, while the [`ChatLine`](ChatLine.cs.md) carries all metadata (URL, file name, size, kind) for downstream rendering or interaction.
---
@@ -789,19 +682,15 @@ public void LoadHistory(string channelName, List<MessageDto> messages, Guid? las
**Returns:** `void`
Loads historical messages into a channel, replacing any existing messages. When lastReadId is supplied (persisted from a previous session), the messages after that identifier are seeded into the unread count, @mention highlighting, and the `new messages` marker, ensuring activity from when the user was offline is surfaced when history is loaded.
Loads historical messages into a channel, replacing any existing messages. The messages are formatted with date-aware rules via `FormatWithDateRules`, and when a `lastReadId` is provided (persisted from a previous session), messages after it seed the unread count, `@mention` highlight, and the "new messages" marker — so activity that happened while offline still lights up.
## Remarks
This method is the central entry point for bringing a channel's history into the UI. It formats incoming messages, updates per-channel caches (such as the latest message id, the list of messages, and the last date), and raises the MessagesChanged event to refresh the view. A key concern it addresses is surfacing unread backlog: if the channel is not currently marked, and a lastReadId is provided, the code seeds unread state from the provided history so the user sees what they missed. If the channel is marked, the code attempts to preserve the unread marker by inserting UnreadMarkerRule() at a known anchor position; if the anchor cannot be located within the fetched batch, the marker is dropped and the anchor tracking for that channel is cleared.
The method keeps the display coherent across history loads by either re-anchoring the marker or seeding unread state, and it updates the channel's last date when available. This coordination helps maintain a stable user experience as history is navigated.
This method centralizes the process of presenting a channels historical backlog and synchronizing the unread state. It coordinates with the marker system to preserve the unread marker position when history is reloaded, using `_markerAnchor` and `_markedChannels` to decide where (and whether) to insert the `UnreadMarkerRule()` in the freshly formatted history. If the anchor isnt present in the newly loaded page, the marker is dropped and the anchor mapping is cleared. When there is no active anchor but a `lastReadId` is supplied, the backlog is seeded from history via `SeedUnreadFromHistory`. The operation updates per-channel caches (`_channelMessages`, `_channelNewestId`, `_channelLastDate`) and raises `MessagesChanged` to refresh the UI.
## Notes
- Marker anchor handling may drop the unread marker if the anchor falls outside the fetched history window; in that case the channel's marker tracking is cleared.
- When lastReadId is provided and there is history, unread state is seeded from history only if the channel is not currently marked with an anchor.
- There are internal caches being updated (_channelMessages, _channelNewestId, _channelLastDate, etc.) and a UI notification is raised via MessagesChanged; callers should ensure thread-safety or call this from a suitable thread to avoid races.
- If the fetched `messages` list is empty, the method still replaces the channels history with an empty formatted sequence and clears any stored last date for the channel.
- The unread-marker behavior depends on the anchor being present in the current fetch window; otherwise, the marker is removed, which may affect how the UI highlights the unread portion.
- The method raises `MessagesChanged` after state updates, so listeners should be prepared for synchronous reentrancy during UI refresh.
---
@@ -822,14 +711,13 @@ private void MarkRead(string channelName)
**Returns:** `void`
Updates the internal read-tracking state for a chat channel by setting the last-read marker to the channel's newest known message ID, if available. It is a small internal helper used when the user has effectively read up to the latest message in the specified channel.
MarkRead updates the per-channel read-tracking state by recording the latest known message id for the given channel. If the provided `channelName` is non-empty and `_channelNewestId` contains a value for that channel, it assigns that value to `_lastRead[channelName]`, effectively marking all messages up to that id as read. This method is typically invoked when a user opens a channel or after messages are loaded to refresh unread indicators without altering read state when the channel is unknown or there is no known newest id.
## Remarks
This method serves as a concise read-tracking primitive within ChatMessageManager. It relies on two internal structures—_channelNewestId (the newest known message ID per channel) and _lastRead (the last-read position per channel)—to advance the read marker without exposing the internal collections to external callers. By performing a safe fetch and updating only when a newest ID exists, it provides a robust, side-effect-limited mechanism for synchronizing UI read state with the channel's latest activity.
This small helper encapsulates read-state mutation, tying together `_channelNewestId` (the latest-known message id per channel) with `_lastRead` (the per-channel read pointer). It prevents updates for channels that have no known newest id and keeps the UI's unread indicators consistent as users navigate or when new messages arrive.
## Notes
- No-op if channelName is null or empty, or if there is no entry for the channel in _channelNewestId; in these cases, no exception is thrown and the state remains unchanged.
- If `channelName` is null or empty, or `_channelNewestId` does not contain an entry for the channel, this method becomes a no-op.
---
@@ -850,15 +738,14 @@ internal static string PadNick(string nick)
**Returns:** `string`
Right-aligns a nickname into the fixed nickname column, truncating nicknames that exceed the available width with an ellipsis, while respecting grapheme boundaries and display column widths. This ensures consistent, visually aligned nicknames in the chat UI regardless of complex characters.
PadNick right-aligns a nickname into the fixed nick column by measuring its display width via `nick.GetColumns()` and truncating long nicknames with a Unicode ellipsis. It is grapheme- and column-aware, iterating grapheme clusters with `GraphemeHelper.GetGraphemes(nick)` and using `g.GetColumns()` (clamped to at least 1) to respect visual widths, stopping before exceeding `NickColWidth - 1` and appending `…` when truncation occurs. If the nickname fits, the method pads on the left with spaces to reach `NickColWidth`.
## Remarks
Right-aligns a nickname within a fixed-width column and centralizes the logic for width-aware truncation. By counting display columns per grapheme and never splitting a grapheme cluster, it preserves user-visible completeness (including emoji and combining characters) while maintaining a stable layout. The ellipsis is appended when truncation is necessary, and the result is padded on the left to exactly fill NickColWidth columns.
This symbol encapsulates the alignment policy for chat nicknames: a grapheme- and column-aware truncation to a fixed width, followed by left-padding with spaces. It centralizes the logic that keeps the nick column visually stable across scripts and emoji, decoupling width calculations from rendering code.
## Notes
- The truncation reserves one column for the ellipsis (NickColWidth - 1) to preserve the final width.
- Each grapheme's display width is obtained via g.GetColumns(), with a minimum of 1 column to avoid stalls on zero-width elements.
- NickColWidth should be a positive, reasonable value to ensure the UI remains legible; extreme values may produce unexpected padding.
- Grapheme-aware truncation prevents splitting a grapheme or emoji when fitting within `NickColWidth`.
- An ellipsis `…` is appended when truncation occurs to signal omitted content and preserve readability.
---
@@ -880,15 +767,7 @@ public void PrependHistory(string channelName, List<MessageDto> olderMessages)
**Returns:** `void`
PrependHistory prepends older messages to the front of a channels in-memory buffer, skipping any that are already present. It filters olderMessages to those not already in the buffer by MessageId, formats the new messages into display lines (respecting the channels date-rule conventions), and inserts them at the beginning of the buffer. If the channel isnt tracked, or if no new lines are produced, the method returns without side effects. When the update targets the currently displayed channel, it raises the HistoryPrepended event to signal the UI to reflect the new history.
## Remarks
Conceptually, this method isolates the concerns of history retrieval, formatting, and UI notification from higher-level chat flow. It relies on MessageId to detect duplicates and on date-rule formatting to ensure the inserted lines align with existing visual rules. By conditionally removing a redundant leading date line when the batch ends on the same day as the current first line, it avoids duplicating date indicators at the top of the buffer.
## Notes
- The method mutates the in-memory channel buffer in place and may affect the UI; callers should be aware of in-memory state changes.
- Deduplication uses MessageId; messages without an Id will be treated as new and could be inserted if not already present.
- HistoryPrepended is raised only when the target channel is the currently active channel (_currentChannel); otherwise, no event is fired.
PrependHistory inserts a batch of `olderMessages` at the front of a channel's in-memory buffer, skipping any items that already exist by comparing their `Id` against the set of current `MessageId`s, and formats the remaining ones using `FormatWithDateRules` into `newLines` before insertion. If no new lines are produced, the method returns early. If `lastBatchDate` is non-null and the existing buffer's first line has a `RuleLabel` equal to `DateRuleLabel(batchDate)` (and that line is not an unread marker), the code removes that leading line to avoid duplicating date separators. Finally, the new lines are inserted at the front, and if the target channel is the currently active channel (`_currentChannel`), the `HistoryPrepended` event is fired to notify the UI.
---
@@ -907,14 +786,17 @@ private static List<ChatSegment> RailPrefix() =>
**Returns:** `List<ChatSegment>`
RailPrefix produces the indentation prefix used for lines that continue or attach to a chat message. It builds two ChatSegment entries: a leading blank-space block sized to accommodate the nickname column plus padding, and a rail segment rendering the vertical continuation rail. A fresh mutable `List<ChatSegment>` is returned on every call so callers can compose per-line prefixes without mutating shared state.
RailPrefix builds the indentation rail used to align continuation/attachment/embed lines under the message text. It returns a new mutable `List<ChatSegment>` that begins with a padding string of length 6 + `NickColWidth` + 1, followed by a rail segment `│ ` colored with `ChatColors.RailAttr`.
## Remarks
RailPrefix encapsulates the alignment rule used for multi-line messages, ensuring that continuation lines align consistently with the main message regardless of nickname width or color settings. The first segment accounts for the nickname column width (NickColWidth) plus a small padding, while the second segment draws the rail using ChatColors.RailAttr, producing a visually distinct vertical guide. Returning a new list on each call avoids cross-call mutations and keeps prefix construction side-effect free.
This helper centralizes rail construction so all rendering paths share the same prefix, ensuring consistent alignment and color usage for continuation rails. It depends on `NickColWidth` to determine the padding width and on `ChatColors.RailAttr` for the rail color, keeping presentation concerns in one place.
## Notes
- Changing NickColWidth or RailAttr will affect the resulting prefix, so coordinate styling changes to avoid misalignment.
- The method returns a new `List<ChatSegment>` that callers are free to mutate; it does not mutate any shared state.
- This method produces a fresh `List<ChatSegment>` per call; callers can mutate it without affecting other render paths.
- The exact prefix width is tied to `NickColWidth`; changing it at runtime may alter alignment across rails.
- If the rail color theme changes, `ChatColors.RailAttr` will drive the rendered color automatically.
---
@@ -936,15 +818,15 @@ public void RemoveMessage(string channelName, Guid messageId)
**Returns:** `void`
Removes all lines associated with a specific message ID from the client's in-memory per-channel message collection. It locates the list for the given channelName, eliminates any entries whose MessageId matches the provided messageId, and, if the updated channel is the current one, raises the MessagesChanged event to trigger a UI refresh. This method is useful when you need to purge a message from the local view (for example after a retraction or client-side filtering) without affecting server-side state.
Removes all lines associated with a specific message ID from the channel's message collection. It looks up the channel in the internal store `_channelMessages` and, if found, calls `RemoveAll` on the channel's list to drop any entries whose `MessageId` matches the provided `messageId`. If the affected channel is the current one (`_currentChannel`), it invokes the `MessagesChanged` event to signal the UI to refresh for that channel.
## Remarks
By centralizing the removal logic, this symbol ensures consistent mutation of the per-channel message lists and a single notification point for UI updates. The operation is scoped to a single channel, and the UI will only refresh when the target channel is currently active. Because the method operates purely on the client-side in-memory structure, there is no server communication performed by this call.
This method centralizes the mutation of the in-memory per-channel message store and the corresponding UI update. It encapsulates the cleanup for a given `MessageId`, ensuring all related lines are removed in one operation, and it notifies listeners only for the active channel to avoid unnecessary redraws.
## Notes
- Not thread-safe as written; ensure marshaling to UI thread or proper synchronization when accessing _channelMessages or the channel's message list.
- Assumes MessageId uniquely identifies a line; if duplicates exist, all matching lines are removed.
- If the channel is not present in `_channelMessages`, the call is a no-op.
- Removing by `MessageId` may delete multiple lines if duplicates exist.
- The method does not return a value; UI refresh relies on the `MessagesChanged` event when the current channel is affected.
---
@@ -965,15 +847,15 @@ private void RemoveUnreadMarker(string channel)
**Returns:** `void`
Removes the unread marker for a given chat channel by validating the input, clearing the channel from the marked set, detaching its UI marker anchor, and purging any unread-marker flags from the channels messages.
Removes the unread marker state for a specific channel. If the provided `channel` is null or empty, or the channel is not currently tracked in `_markedChannels`, the method returns early and makes no changes. When it proceeds, it removes the channel from `_markerAnchor` and, if there are messages stored for that channel in `_channelMessages`, clears all items where `IsUnreadMarker` is true.
## Remarks
As a private helper, it centralizes the unread-marker lifecycle in ChatMessageManager, coordinating _markedChannels, _markerAnchor, and _channelMessages to keep UI state and data in sync. The early return guards prevent unnecessary work when the channel is invalid or already cleared. The removal of IsUnreadMarker flags happens only after the channel is removed from the marked set, ensuring a consistent, single source of truth for whether a channel shows an unread indicator.
RemoveUnreadMarker centralizes the cleanup of unread-marker state across internal collections. It relies on three collaborators: `_markedChannels` to determine if the channel currently has an unread marker, `_markerAnchor` to drop the visual or structural marker, and `_channelMessages` to scrub per-message flags. By encapsulating this logic, callers avoid inconsistent states where a channel might be marked as unread while the marker remains or vice versa.
## Notes
- This method is private; external callers should not rely on its behavior.
- There is no synchronization visible in the snippet, so concurrent invocations may require external synchronization.
- If there are unread indicators outside the IsUnreadMarker flags, they will not be cleared by this method.
- This is a private helper; it is intended to be invoked by other methods within the same class when the unread state for a channel should be cleared.
- It mutates multiple internal structures, so ensure appropriate synchronization if called from multiple threads.
- If `_channelMessages` has no entry for the given `channel`, the per-message cleanup is skipped gracefully.
---
@@ -994,7 +876,7 @@ private static ChatLine ReplyQuoteLine(ReplyRefDto replyTo)
**Returns:** [`ChatLine`](ChatLine.cs.md)
Constructs a compact, rail-prefixed quote line for an incoming reply. It carries the original message id (JumpToMessageId) so selecting the quote navigates to the source, and it truncates the displayed snippet to fit the UI width. The snippet is sanitized by replacing newline characters with spaces, optionally converted to an action-style prefix if a known action is detected, and transformed with emoji glyph replacement. The result is a ChatLine composed of three segments: a rail prefix, the sender's username (colored), and the snippet (system-colored). The line also exposes JumpToMessageId for navigation and ContinuationPrefixSegments to align any continued lines of the rail.
The `ReplyQuoteLine` method constructs the quoted, dim-lined representation of a replied message that appears above the original message in the chat UI. Given a [`ReplyRefDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md), it builds a single-line rail segment that shows the senders username and a truncated snippet of the original content, while carrying the original message id so activating the line jumps back to that message. The snippet is first normalized (newlines replaced), optionally rewritten into an action-format via `MessageConventions.TryParseAction`, and then passed through `EmojiHelper.ReplaceEmoji`. It truncates by grapheme width to fit within `maxSnippetCols` (60 columns) to avoid breaking grapheme clusters, appending a trailing ellipsis when needed. The final display uses the rail prefix and renders the sender name with `NickColorHelper.GetAttribute`, followed by the snippet in the system color (`ChatColors.SystemAttr`). The method returns a [`ChatLine`](ChatLine.cs.md) whose `JumpToMessageId` is set to `replyTo.MessageId` and whose `ContinuationPrefixSegments` are the rail prefix, enabling proper alignment for any following lines in the rail.
---
@@ -1019,15 +901,17 @@ private void SeedUnreadFromHistory(string channelName, List<MessageDto> messages
**Returns:** `void`
Reconstructs unread state from a persisted last-read message id for a channel by inserting an unread marker before the first unread message in the current fetch window and, for inactive channels, seeding the unread count and @mention highlight. If the last-read id is no longer present in the fetched window, the entire window is treated as unread.
SeedUnreadFromHistory restores the UI unread-state after a history fetch for a given channel by locating the boundary between read and unread messages using the provided `lastReadId`, inserting an unread marker before the first unread message in the rendered `formatted` lines via `UnreadMarkerRule()`, and updating per-channel state such as `_markedChannels` and `_markerAnchor`. For channels other than the currently active one (`_currentChannel`), it also seeds the per-channel unread count (`_channelUnread`) and, if `_currentUser` is present, collects any mentions of the current user to highlight in background channels via `_mentionChannels`. If the `lastReadId` is not present in the fetched `messages` window, the first unread index becomes 0 and the entire window is treated as unread. The method is intended to be invoked during history loading to align the rendered chat with the user's last reading position.
## Remarks
This symbol centralizes how persisted read positions are translated into the UI's unread indicators. It updates internal trackers (_markedChannels, _markerAnchor, _channelUnread, and _mentionChannels) and mutates the formatted message list to place the visual cue that new messages are available. Because it only applies the badge/mention behavior to background channels, the active channel remains visually unaffected beyond the standard read state.
SeedUnreadFromHistory centralizes unread-state reconstruction after history fetches, coordinating between the logical unread boundary, the rendered view, and channel-scoped UI hints. By inserting the marker at the exact position corresponding to the first unread message and storing an anchor, the UI can reliably indicate where unread content begins and support navigation to that point. The method differentiates the active channel (which does not accrue badges or mention highlights) from background channels, populating per-channel unread counts and optional @mention tracking to enhance visibility without cluttering the current reading experience.
## Notes
- If the computed anchor line cannot be found in the current formatted list, no marker is inserted and the method returns.
- When lastReadId is not found in messages, firstUnread becomes 0, so the marker targets the very first message in the window.
- Mentions are evaluated only for non-active channels; active channels do not receive mention highlights from this method.
- If the `lastReadId` is not present in the fetched `messages`, the calculation yields an index of 0 and the entire window is marked as unread.
- If the anchor line cannot be found in `formatted`, no marker is inserted and no per-channel state is updated for that call.
- The operation mutates both the rendered view (`formatted`) and several per-channel state collections; callers should ensure it runs in a UI-context where such mutations are safe and up-to-date with the latest history fetch.
- Mention detection is case-insensitive and checks both message content for `@currentUser` and the sender of a replied-to message, if available.
---
@@ -1048,15 +932,7 @@ public void SetChatWidth(int width) => _chatWidth = width
**Returns:** `void`
Sets the internal chat width used by the chat rendering logic. This method is a concise mutator that assigns the provided width to the private _chatWidth field. Use it when you need to programmatically adjust the chat area width, such as in response to layout changes or user actions that resize the chat panel.
## Remarks
Centralizes width mutations behind a single API, preserving encapsulation of layout state. It also paves the way for future side effects (for example, triggering a layout refresh or validating the value) without changing call sites. Keeping this logic in one place reduces duplication and makes behavior easier to evolve.
## Notes
- No validation on the input width; callers should ensure the value is non-negative and within reasonable bounds to avoid render glitches.
Updates the internal `_chatWidth` field to the provided value, effectively setting the chat panel's width. Call this method when you need to adjust the chat area at runtime (e.g., in response to layout changes or user preferences) rather than modifying the field directly.
---
@@ -1077,13 +953,10 @@ public void SetCurrentUser(string username) => _currentUser = username
**Returns:** `void`
Sets the internal _currentUser field to the provided username, updating the chat subsystem's notion of who is the current user. This method should be used whenever the active user changes (for example, after a user logs in or switches accounts) so that subsequent messages can be attributed to the correct user in the UI.
Sets the current user by assigning the provided `username` to the internal `_currentUser` field. This simple mutator establishes the active user context for subsequent chat message operations that depend on the current user.
## Remarks
Centralizes mutation of the current user state within ChatMessageManager, making it easier to add side effects (such as updating UI elements, tagging messages, or enforcing user-specific behavior) without changing call sites. By routing changes through SetCurrentUser, the class can evolve to perform validation, trigger events, or refresh displays in a single place.
## Notes
- No input validation or normalization is performed; the value is assigned directly to _currentUser. Passes such as null or empty strings may lead to an invalid or inconsistent state unless the caller ensures proper validation.
This is a straightforward mutator that updates internal state by assigning to `_currentUser`. It does not perform validation or trigger side effects beyond updating the active user; callers should ensure the correct sequencing of calls if the current user is relied upon by subsequent operations, especially in multi-threaded scenarios.
---
@@ -1109,14 +982,10 @@ private static List<ChatSegment> SystemHeaderSegments(string time) =>
**Returns:** `List<ChatSegment>`
This private helper constructs the header segments for a system/status line. When given a formatted time string, it returns a three-segment header (ChatSegment list) that renders: the time, a padded system nickname placeholder, and a leading rail separator, all styled with the project's chat color attributes. The header segments correspond to the three elements in the returned list: the time string followed by a space colored with TimestampAttr, the PadNick(\"--\") value colored with TimestampAttr, and the literal rail \" │ \" colored with RailAttr. This utility is used by the chat header rendering logic to produce a consistent appearance for system messages.
Constructs the header variant used for system/status lines in the chat UI. Given a `string time`, it returns a `List<ChatSegment>` containing three segments: the first renders the time with `ChatColors.TimestampAttr`, the second renders the padded nick placeholder via `PadNick("--")` using the same timestamp styling, and the third renders the rail separator as `" │ "` with `ChatColors.RailAttr`. This header is used to prefix system messages and provide a consistent visual cue for system status.
## Remarks
This private method encapsulates the three-part system header used for status lines, anchoring time, nickname placeholder, and the rail separator in one place. It relies on PadNick for the nickname placeholder width and on ChatColors attributes to keep the look aligned with the rest of the chat chrome.
## Notes
- The time argument should already be formatted for display; the method does not parse or reformat it.
- The header relies on PadNick to produce a fixed-width nickname; changes to PadNick's output or width could affect alignment.
Centralizes header composition for system messages, enabling consistent styling and reduced duplication. By composing pre-styled segments instead of scattering formatting throughout callers, it makes maintenance easier and helps ensure system headers look the same across the chat surface.
---
@@ -1132,14 +1001,10 @@ private static ChatLine UnreadMarkerRule() =>
**Returns:** [`ChatLine`](ChatLine.cs.md)
Creates a ChatLine that renders the '── new messages ──' unread marker using the UnreadMarker color attribute. This private helper is used when building the chat line sequence to visually indicate that there are unread messages in the conversation.
This private helper constructs a [`ChatLine`](ChatLine.cs.md) that represents an unread-messages marker in the chat UI. It builds a single-token line containing the literal label `── new messages ──`, colored by `ChatColors.UnreadMarkerAttr`, and marks the line with `IsUnreadMarker = true` and `RuleLabel = `new messages``.
## Remarks
To centralize the styling and labeling of the unread marker, this helper bundles the label ('new messages'), the color attribute (ChatColors.UnreadMarkerAttr), and the unread-marker flag (IsUnreadMarker = true). It keeps the construction logic in one place so changes to the marker's text or color propagate consistently across callers. The private visibility signals that this is an internal construction detail of the chat rendering pipeline.
## Notes
- This symbol is private; it cannot be called from outside its containing class. If you need to render unread markers elsewhere, consider exposing a public API or refactoring the helper into a shared utility.
- The returned ChatLine is explicitly marked as an unread marker; consumers should treat it as a UI cue rather than a regular chat message.
This factory encapsulates the visual convention for unread indicators, ensuring a consistent appearance across the UI without scattering literal tokens. By centralizing the construction, changes to the marker's label text or color attribute only need to be updated in one place. It also clearly communicates intent: lines produced by this helper are unread markers and should be treated accordingly by the rendering pipeline.
---
@@ -1161,11 +1026,34 @@ private static List<string> WordWrap(string text, int maxCols)
**Returns:** `List<string>`
WordWrap is a private utility that converts a single string into a list of lines whose display width does not exceed a specified maxCols. It's designed for UI scenarios (for example, chat messages) where wrapping must be deterministic and centralized. If maxCols <= 0, the method returns a single-element list containing the original text.
The `WordWrap` method transforms a block of text into a list of lines that fit within a specified maximum column width by wrapping at spaces. If `maxCols` is less than or equal to zero, wrapping is skipped and the original `text` is returned as a single line. The wrap logic uses `GetColumns()` to measure display width, ensuring truncation reflects actual rendered width rather than raw character count. This private helper centralizes line-breaking behavior for UI rendering (e.g., chat messages) so callers render consistently.
Otherwise, it splits the input on spaces (collapsing multiple spaces) and greedily builds lines by appending words until adding the next word would exceed maxCols as measured by GetColumns. When a word would overflow the current line, the line is committed and a new one starts with that word. The final line is added after processing all words. The resulting lines use single spaces between words.
## Remarks
This private static helper encapsulates the core concern of rendering text within a fixed-width area. By delegating width calculation to `GetColumns()`, it remains resilient to character widths and potential emoji or wide characters, while keeping the wrapping policy consistent across the class. Centralizing this logic avoids ad-hoc wrapping scattered across call sites and makes future width-policy changes easier to propagate.
Note that a single word longer than maxCols will be placed on its own line and may exceed the requested width.
## Notes
- If a single word is longer than `maxCols`, the word is placed on its own line and may exceed the specified width; the function does not hyphenate or break long words.
- Wrapping relies on `StringSplitOptions.RemoveEmptyEntries`, so consecutive spaces are treated as a single separator and do not produce empty lines.
- Because the method is `private`, its reuse is restricted to its declaring type; if you need wrapping elsewhere, consider extracting it to a shared utility.
---
### ContentIndentCols
> **File:** `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs`
> **Kind:** field
```csharp
public const int ContentIndentCols = 6 + NickColWidth + 3
```
ContentIndentCols is the left-padding width, in characters, for the chat message text. It is computed as `6 + NickColWidth + 3`, corresponding to the fixed time prefix `HH:mm `, the nickname column width `NickColWidth`, and the leading separator ` │ `. Use this constant whenever you render or measure the start column of the message body to ensure consistent alignment.
## Remarks
ContentIndentCols centralizes the left margin calculation for chat lines, ensuring message text starts at a single, predictable column regardless of nickname width. By deriving the indentation from `NickColWidth`, changes to nickname sizing propagate to the layout without scattering magic numbers. This constant is baked into compile-time calculations, so the layout remains stable across the codebase.
## Notes
- It is a compile-time constant; changing `NickColWidth` or `ContentIndentCols` requires a rebuild of the consuming code.
---
@@ -1178,38 +1066,6 @@ public const int NickColWidth = 12
```
NickColWidth defines the fixed width of the nickname column in the chat UI, reserving 12 characters on the right to align nicknames in a WeeChat-style layout. It is used by the chat rendering logic in ChatMessageManager to keep nickname alignment consistent across messages.
## Remarks
Centralizes the presentation detail of the nickname column, avoiding scattered magic numbers across rendering code. By exposing this as a single public constant, its straightforward to tweak the overall alignment of the chat UI while keeping the rest of the layout logic unchanged. It also communicates intent clearly to future contributors who are adjusting how usernames appear in chat rows.
## Notes
- As a public compile-time constant, changing NickColWidth requires recompiling dependents to pick up the new value.
- Prefer referencing NickColWidth in formatting/layout code rather than using hard-coded numeric literals to maintain consistent alignment.
---
## ContentIndentCols
> **File:** `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs`
> **Kind:** field
```csharp
public const int ContentIndentCols = 6 + NickColWidth + 3
```
ContentIndentCols represents the total number of character columns that precede the actual message text in a chat line. It is computed as 6 (the length of the "HH:mm " timestamp prefix) plus NickColWidth (the width of the nickname column) plus 3 (the " │ " separator). Use ContentIndentCols when you need to align or wrap the message body so that it starts at a consistent column after the header.
## Remarks
This abstraction ties the content start position to the header region, ensuring consistent alignment across messages even if nickname width or the time prefix changes. By centralizing the indentation budget behind a single public constant, rendering code avoids scattered magic numbers and remains coherent when layout assumptions evolve.
## Example
```csharp
// Example: build an indented content line for a chat message
string line = new string(' ', ContentIndentCols) + body;
```
## Notes
- The constant is a compile-time value (public const int). If you need dynamic indentation per message or per theme, compute it at runtime instead of using ContentIndentCols.
Defines the fixed width of the right-aligned nick column used in the chat message layout (WeeChat-style). The value `NickColWidth` reserves that many characters for the nick portion, ensuring consistent alignment of message text across lines.
---
@@ -15,12 +15,7 @@ public record ChatSegment(string Text, Attribute? Color)
| `Color` | `Attribute?` | — |
ChatSegment represents a colored fragment of text within a chat line. It pairs the displayed text with an optional color attribute, enabling the UI to render parts of a message with varying styling without altering the textual content. As a record, ChatSegment is immutable and supports value-based equality, making it convenient to compose a full line by aggregating multiple segments in a deterministic way.
Represents a colored piece of text within a chat line. It pairs the display text (`Text`) with an optional color styling (`Color`). As a `record`, it is an immutable, value-based container designed to be composed with other `ChatSegment`s to render a full message, applying `Color` when present; if `Color` is `null`, default styling is used.
## Remarks
ChatSegment exists to separate content from presentation. By modeling a line as a sequence of segments, the rendering layer can apply different colors or styles to each piece while preserving the original order. The record-like semantics also ease comparisons, caching, and deduplication of segments across messages.
## Notes
- Color is stored as a nullable Attribute; a null Color means no special styling is requested for this segment.
- `Attribute` is a general metadata type; downstream renderers interpret it to apply styling. The exact meaning of the Color value depends on the consuming UI.
- Because ChatSegment is a two-property record, equality includes both Text and Color; changes to either produce a distinct segment, which is important when deduplicating or comparing segments.
By modeling a chat line as a sequence of `ChatSegment`s, the rendering layer can apply per-segment styling without mixing content and presentation logic. The `ChatSegment` uses a `record` to enable value-based equality, which helps with deduplication, testing, and change tracking when chat lines are built from multiple segments.
@@ -8,4 +8,12 @@ static class RenderHelpers
```
RenderHelpers is a small, shared utility for rendering IListDataSource content. Its WriteText method writes text to a ListView grapheme-by-grapheme while respecting a maximum width, returning the updated count of drawn columns. It iterates over grapheme clusters obtained from GraphemeHelper.GetGraphemes(text); for each grapheme, it computes the display width with GetColumns() (falling back to 1 if necessary). If adding the grapheme would exceed maxWidth, rendering stops. Otherwise, it appends the grapheme to the ListView via lv.AddStr(grapheme) and increments the drawn count. This centralizes grapheme-aware rendering logic so multiple IListDataSource implementations share consistent width handling and avoid duplicating rendering concerns.
RenderHelpers is a small static utility class that centralizes rendering concerns for `IListDataSource` implementations. It currently provides a single method, `WriteText`, which writes text to a `ListView` grapheme by grapheme, respecting a maximum width. It returns the updated drawn-columns count, enabling callers to track horizontal placement as multiple fields are rendered on a single line.
## Remarks
RenderHelpers abstracts the grapheme-aware rendering logic so all list-rendering code shares the same boundary checks and column accounting. It couples the `GraphemeHelper.GetGraphemes` iteration with a safe width calculation, reducing the chance of off-by-one errors when composing UI rows. In short, its the single place responsible for safe, width-bound text rendering to a `ListView` in this UI layer.
## Notes
- The width of each grapheme is determined by `grapheme.GetColumns()`, clamped to at least 1 with `Math.Max(grapheme.GetColumns(), 1)`.
- Rendering stops when adding the next grapheme would exceed `maxWidth`; partial graphemes are not drawn.
- The method delegates actual drawing to `ListView.AddStr`, so callers should ensure the `ListView` state is appropriate for incremental writes.
@@ -8,17 +8,6 @@ internal static class WelcomeBanner
```
Renders a MOTD-style splash in the chat pane when no channel is selected a gold-gradient ASCII logo accompanied by a version tagline and quick usage hints, evoking classic IRC greetings. Use WelcomeBanner.Build to generate the banner lines for a given viewport width and version string, then feed those lines into the chat UI.
The `WelcomeBanner` class provides the MOTD-style splash shown in the chat pane when no channel is selected. It renders a gold-gradient ASCII logo by choosing between `BigLogo` (for wider viewports) and `SmallLogo` (for narrow panes), centers the logo within the given width, and appends a version tagline and quick-use hints. The static `Build` method returns a list of [`ChatLine`](ChatLine.cs.md) objects that the UI can render to display the branded welcome banner for a given `width` and `version` string.
## Remarks
WelcomeBanner encapsulates the presentation of the welcome banner: centering, padding, colorization, and the two-logo strategy are all handled here so the rest of the chat UI can simply render a sequence of lines. It selects between a full-width BigLogo and a compact SmallLogo based on the viewport width, scales the gradient across the chosen logo, and appends a version tagline plus a set of user hints. This keeps branding consistent across sizes and isolates banner-specific formatting from the broader rendering pipeline.
## Example
```csharp
var lines = WelcomeBanner.Build(80, "1.2.3");
// integrate 'lines' into the chat pane
```
## Notes
- The logo variant is chosen based on the provided width; very small panes will display SmallLogo to preserve legibility.
- The color attributes (Attributes on ChatSegment) require UI support in the chat renderer; without color support the banner falls back to plain text.
The banner is designed to be self-contained: it composes ASCII art, a vertical color gradient (`Gradient`), and a small set of hints (`Hints`) into a sequence of renderable lines. This keeps the welcome experience consistent across sessions and isolates branding concerns from the main channel rendering logic.
@@ -8,10 +8,10 @@ public sealed class AudioPlayerDialog
```
AudioPlayerDialog is a sealed class that presents a modal Audio Player UI within the application's terminal UI. It assembles a compact layout with the current file name, a wave-like block visualization, playback status, and simple volume and playback controls, all exposed via a single Show method that binds an IApplication and an AudioPlaybackService to the dialog's lifecycle.
AudioPlayerDialog is a sealed UI helper that renders a compact, terminal-style audio player within the application. When `Show` is invoked, it builds a `Dialog` titled "Audio Player" containing a file name header, a wave visualization area, a status label, volume controls, and playback controls (Play, Stop, Close). It also orchestrates a simple block-wave animation using `WaveBlocks` and a timer to provide a visual indication of activity, while delegating actual playback logic to the provided [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md).
## Remarks
By encapsulating layout, colors, and animation in one place, it provides a reusable, cohesive UX for audio playback that can be dropped into different screens without duplicating UI code. The class relies on themed attributes (e.g. WaveActiveAttr, WaveIdleAttr, FileNameAttr, Status*Attr) to ensure consistent appearance, and uses a timer-driven animation loop to render the wave pattern while playback is active.
AudioPlayerDialog centralizes the presentation of audio playback in a terminal UI. It encapsulates the layout and styling (via `FileNameAttr`, `WaveIdleAttr`, and status attributes such as `StatusPlayingAttr`, `StatusPausedAttr`, and `StatusStoppedAttr`) so callers can surface audio without constructing the controls themselves. It collaborates with `IApplication` to host the dialog in the UI thread and with [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) to reflect playback state and drive the actual audio logic while the dialog handles user interactions and visuals.
## Example
```csharp
@@ -19,5 +19,6 @@ AudioPlayerDialog.Show(app, audioService, "/path/to/song.mp3", "song.mp3");
```
## Notes
- The waveform visualization uses Unicode block characters; ensure your terminal font supports these glyphs for correct rendering.
- The dialog starts a background animation timer; dispose the dialog or stop the timer to avoid leaks when closing.
- The wave visualization relies on Unicode block characters from `WaveBlocks`; ensure the terminal/font supports these glyphs for proper rendering.
- The animation is driven by a timer using `AnimationIntervalMs`; changing the cadence affects how lively the waveform appears.
- The volume UI initializes with a local `currentVolume` and the wiring between the volume controls and [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) is not shown in the excerpt; connect changes to the service to affect real playback.
@@ -8,16 +8,14 @@ public sealed class ChannelPasswordDialog
```
Prompts for a channel password when joining a protected channel and returns the entered password, or null if the user cancels. Use this helper whenever you need a consistent, modal password prompt instead of duplicating dialog boilerplate across join flows.
ChannelPasswordDialog is a lightweight UI helper that prompts the user for the password required to join a password-protected channel. Its static `Show` method returns the entered password as a `string?`, or `null` if the user cancels, after presenting a small modal dialog built from `Dialog` with a channel-specific message (defaulting to `#{channelName} is password protected.`).
## Remarks
This class centralizes the user flow for joining password-protected channels. It presents a modal dialog titled Join #<channel>, collects the password, and returns it to the caller, ensuring a single, predictable contract. The UI avoids displaying the actual password text by using a redacted caption and automatically focusing the password field, while the dialog lifecycle is orchestrated through the application (app.Run and app.RequestStop).
## Example
```csharp
string? password = ChannelPasswordDialog.Show(app, "mychannel", "Enter password to join #mychannel.");
```
Encapsulates the password-prompt UX for channel joins, avoiding duplication of UI logic across callers. The dialog wires up a password input and two actions: a join action that validates a non-empty password and a cancel action that returns `null`, ensuring the caller proceeds only after a password is provided or the user cancels. Providing a custom `message` lets callers tailor the prompt while preserving a consistent default behavior when none is supplied.
## Notes
- The method is synchronous and modal; it blocks the caller until the user completes the interaction.
- A null return value indicates the user canceled the operation. If the user submits an empty password, a brief error dialog is shown and the prompt remains active until a non-empty password is provided.
- The call is synchronous and blocks until the user completes interaction with the dialog.
- The return value must be checked for `null` to distinguish between a canceled join and a provided password.
- The implementation relies on UI primitives (`Dialog`, `Label`, `Button`, `MessageBox`) and a password input field; ensure this is invoked on an appropriate UI thread context in your application.
@@ -18,7 +18,15 @@ public sealed class ConnectDialog
```
ConnectDialog is a Terminal.Gui-based dialog that collects server connection details and authentication information for the application. When shown, it can display a list of SavedServer entries at the top if any saved servers are provided; in that case a Saved Servers section is rendered with a ListView of display names that indicate whether a session exists (the code appends a [session] marker when a RefreshToken is present). Below (or in place of it, when there are no saved servers), the dialog presents manual entry fields for Server URL (default http://localhost:5000), Username, and Password, along with UI hints such as a hidden password placeholder and a Remember me option. Additional fields include Display Name and, when relevant, an Invite Code for invite-gated registrations. The static Show method returns a ConnectDialogResult when the user completes the dialog, or null if the dialog is cancelled; the dialog height is adjusted depending on whether saved servers are shown.
ConnectDialog is a Terminal.Gui dialog that gathers server connection and authentication information from the user. It optionally presents a Saved Servers list when available, and returns a `ConnectDialogResult?` when the user completes the form or null if cancelled.
## Remarks
By encapsulating the authentication flow in a single dialog, `ConnectDialog` centralizes the user experience for establishing a server connection. It dynamically adapts its layout depending on whether [`SavedServer`](../../Config/ClientConfig.cs.md) entries are provided, showing a `ListView` of saved servers when present and keeping a compact form otherwise. It also treats credentials with care by redacting the password in the UI and indicating a saved session when a `RefreshToken` exists.
## Notes
- If saved servers exist, the dialog height increases to accommodate the list (24 vs 20).
- The Saved Servers display shows items built from saved server properties; a session indicator is appended when `RefreshToken` is non-empty.
- The password field is displayed as `[REDACTED:PASSWORD]` and the actual input is masked via the `Secret` flag.
---
@@ -47,34 +55,13 @@ public record ConnectDialogResult(
| [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md) | `string?` | `null` |
ConnectDialogResult encapsulates all user input gathered from the connect dialog as a single, immutable value. It is produced when the dialog completes and is consumed by the rest of the application to initiate a connection flow, passing the server URL, credentials, and onboarding flags as a single, strongly-typed package.
ConnectDialogResult is an immutable data container produced by the connect dialog, encapsulating the user's input as a single value object for the subsequent connection/authentication workflow. It carries the server URL (`ServerUrl`), the user's credentials (`Username`, `Password`), and UI preferences (`IsRegister`, `RememberMe`), along with an optional `SavedRefreshToken` and possibly `DisplayName` or [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md).
## Remarks
By collecting all related fields into a single record, this abstraction reduces coupling between the UI layer and the connection logic. It clearly expresses the intent of the user's action (login vs register) and whether credentials should be remembered, while allowing optional data (DisplayName, InviteCode) to participate in specialized flows without forcing callers to thread every field separately.
## Example
```csharp
// Common usage: construct a result from values collected in UI
var result = new ConnectDialogResult(
ServerUrl: "https://example.server/api",
Username: "alice",
Password: "P@ssw0rd",
IsRegister: false,
RememberMe: true,
SavedRefreshToken: null,
DisplayName: "Alice",
InviteCode: "INVITE-2024-ABCD"
);
```
Using a `record` here provides value-based equality and convenient deconstruction, making it easy to compare results and pass them through layers without mutating state. It serves as a boundary-crossing DTO that formats UI input into a coherent package for the authentication/service layer, while supporting optional flows via `DisplayName` and [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md). Because `Password` and `SavedRefreshToken` can contain sensitive data, avoid logging them and handle this object as transient UI data rather than a durable model.
## Notes
- DisplayName and InviteCode are nullable; omit them or pass null if not applicable.
- Password should be treated as sensitive data: avoid logging it or persisting it longer than necessary, and ensure proper disposal or clearing after use.
- SavedRefreshToken may be null; handle accordingly in login/refresh flows.
- This record is intended for in-memory transfer between UI and authentication/connection logic; when persisting or transmitting, apply appropriate security measures and avoid leaking confidential fields.
- Do not log or persist the `Password` or `SavedRefreshToken` values; treat them as sensitive data.
- This object is intended to be transient UI input; avoid storing it longer than necessary or serializing it insecurely.
---
@@ -18,15 +18,15 @@ public sealed class CreateChannelDialog
```
Displays a modal Create Channel dialog that collects the details needed to create a new channel: a name, an optional topic, a password, and a public visibility setting. The name is trimmed and normalized to lower case; if it is empty, the dialog reports an error and stays open. On Create, it builds a CreateChannelResult containing the name, topic (nullable), isPublic, and the password; on Cancel it returns null. The dialog runs via the provided IApplication instance and returns after the user makes a choice.
CreateChannelDialog.Show renders a modal 'Create Channel' dialog via the supplied `IApplication`, collecting a channel `name`, an optional `topic`, and an optional `password`, validating inputs, and returning a `CreateChannelResult` when the user confirms, or `null` if canceled. The entered `name` is trimmed and converted to lowercase; the `topic` is optional, and a blank `password` yields a `null` password in the result.
## Remarks
Encapsulates all UI logic for channel creation into a single entry point, enabling consistent behavior across the app and isolating rendering from business logic. The class acts as a small, self-contained UX widget that constructs the result object, ensuring callers need only handle the CreateChannelResult or null.
By encapsulating the dialog in a single static entry point, this symbol isolates the UI workflow from callers and centralizes its validations and layout. It coordinates several UI components (`Dialog`, `Label`, `TextField`, `Button`) and user input handling so that changes to the channel-creation UX don't ripple through the rest of the codebase.
## Notes
- Name validation is minimal in code: the name is trimmed and lowercased, and non-empty; there is no explicit enforcement of length or allowed character patterns at runtime beyond what the UI hints suggest.
- Password handling appears behind-the-scenes (the UI labels redact the password, yet the password value is captured and returned as part of the result); ensure secure handling and minimize exposure of the plaintext password.
- The snippet references passwordField and publicCheckbox, which must exist in the full class scope; if you modify the UI composition, ensure these controls are present and wired consistently with the password retrieval and public visibility logic.
- Name normalization: the code lowercases and trims the input before use; beware that the original casing is not preserved in the result.
- Password handling: the password is optional; if left blank, the resulting `password` becomes `null`.
- Redacted password placeholder: the label uses a redacted placeholder `[REDACTED:CONNECTION_STRING_PASSWORD]`, indicating the actual password source isn't visible in the snippet; ensure the real value is supplied by the surrounding application context.
---
@@ -48,27 +48,6 @@ public record CreateChannelResult(string Name, string? Topic, bool IsPublic, str
| `Password` | `string?` | — |
CreateChannelResult is an immutable data carrier that represents the outcome of creating a channel in the EchoHub client UI. It carries the channel's Name, an optional Topic, a flag IsPublic indicating whether the channel is public, and an optional Password.
## Remarks
As a record, CreateChannelResult participates in value-based equality, making comparisons straightforward without manual field checks. The positional constructor provides a concise, immutable payload that is easy to pass through layers (UI, services, or view models). You can deconstruct a result into its components, or derive a modified copy with a with-expression if you need a slightly different result without mutating the original. This type is intended to be produced by the channel-creation flow and consumed by UI code and downstream components.
## Example
```csharp
// Common case: create a public channel with a topic and password
var result = new CreateChannelResult("General", "Team discussions", true, "s3cr3t");
// Access fields
string name = result.Name;
string? topic = result.Topic;
bool isPublic = result.IsPublic;
string? password = result.Password;
// Deconstruct for convenience
var (n, t, pub, pwd) = result;
// Create a modified copy
var updated = result with { Topic = "New topic" };
```
Represents the outcome of a channel-creation operation in the UI. The `CreateChannelResult` type carries the channel's `Name`, an optional `Topic`, a boolean `IsPublic` indicating if the channel is public, and an optional `Password` for password-protected channels, enabling downstream UI logic to respond to the created channel.
---
@@ -18,16 +18,24 @@ public sealed class ProfileEditDialog
```
ProfileEditDialog provides a Terminal.Gui dialog for editing the user's profile. Its Show method presents a modal dialog titled "Edit Profile" with fields for Display Name, Bio, Nickname Color (with a hex input and a live color preview) and Avatar selection, plus notification preferences, returning a ProfileEditResult when the user accepts or null if cancelled.
ProfileEditDialog is a Terminal.Gui dialog that presents a compact, form-based UI for editing a user's profile, including `Display Name`, `Bio`, and `Nickname Color`, with live color preview and an optional avatar picker. When invoked via `Show`, it pre-fills fields from the provided current values and returns a `ProfileEditResult?` when the user confirms, or `null` if the operation is cancelled. This component is intended to be used whenever your application needs an in-app, consistent way to collect profile updates from the user.
## Remarks
ProfileEditDialog centralizes profile-edit UI in one reusable component, ensuring a consistent look and behavior whenever the user updates their profile. It wires up real-time color previews by updating the color swatch whenever the hex input changes, and it delegates color parsing to HexColorHelper to translate user input into a Color value. The Avatar field demonstrates integration with a file picker (OpenDialog) within a Terminal.Gui workflow, keeping file selection cohesive with the rest of the dialog.
ProfileEditDialog isolates the profile-edit UX from the rest of the application, providing a single reusable route for updating these fields. It delegates color parsing to [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) (e.g. `ParseHexColor`/`ParseHexToColor`) so the dialog itself remains focused on presentation and interaction. The color preview is updated in real time by wiring the `TextChanged` event on the `colorField` to `UpdateColorPreview`. The avatar picker uses an `OpenDialog` invoked through the Browse button, illustrating how file selection is integrated into a TUI form.
## Example
```csharp
var result = ProfileEditDialog.Show(app, currentDisplayName, currentBio, currentColor, notificationSoundEnabled: true, notificationVolume: 50);
if (result != null)
{
// Use result to apply the edited profile values
}
```
## Notes
- The Show method accepts optional parameters for notificationSoundEnabled and notificationVolume, defaulting to false and 30 respectively.
- If no avatar is selected, avatarField.Text remains empty.
- The return type is ProfileEditResult?; callers should handle null to cover the cancel path.
- This implementation relies on Terminal.Gui primitives (Label, TextField, Button, CheckBox, OpenDialog) and collaborator types (ProfileEditResult, HexColorHelper); ensure these types are available in the consuming project.
- Color parsing is performed via [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) to translate the user-entered hex string into a `Color` for the live preview; invalid inputs fall back to a safe color preview.
- The avatar field is optional; leaving it empty means no avatar is selected.
- The dialog uses a fixed size of 60x26, so ensure your terminal window can accommodate this layout to avoid clipping or overflow.
---
@@ -51,14 +59,12 @@ public record ProfileEditResult(string? DisplayName, string? Bio, string? Nickna
| `NotificationVolume` | `byte?` | — |
Represents the data returned from the profile edit dialog. It encapsulates the user\'s optional inputs for DisplayName, Bio, NicknameColor, AvatarPath, NotificationSoundEnabled, and NotificationVolume so the caller can apply changes in a single operation. Each property is nullable: a null value means no change for that field; a non-null value provides a new value to persist.
Represents the data returned when the user finishes editing their profile in the dialog. It carries the proposed updates to `DisplayName`, `Bio`, `NicknameColor`, `AvatarPath`, and notification settings (`NotificationSoundEnabled`, `NotificationVolume`). Because all fields are nullable, callers can distinguish between fields the user left unchanged and fields the user explicitly updated, enabling partial updates to the profile.
## Remarks
ProfileEditResult is an immutable value object used as the dialog\'s return type. Its nullable fields express a delta: non-null values indicate updates, while null indicates no change. As a record, it benefits from value-based equality, making comparisons and tests straightforward, and it cleanly separates UI input from downstream update logic.
ProfileEditResult serves as a lightweight, immutable carrier that isolates UI concerns from the underlying profile update logic. It provides a snapshot of the user's edits at dialog closure, which the caller then applies to the profile as needed. The use of nullable members communicates optional edits clearly and avoids forcing changes for fields the user did not touch.
## Notes
- Null values indicate no change; apply only non-null fields when updating the profile.
- The type is immutable; to derive modifications, use a with-expression to create a new instance.
- Interpret any null value as 'no change' for that field when applying updates to the actual profile.
---
@@ -18,14 +18,15 @@ public sealed class ProfileViewDialog
```
ProfileViewDialog renders a dialog to view a user's server profile; when showing the current user's profile it also exposes action buttons (Edit Profile / Set Status) and returns the chosen ProfileAction, while viewing another user yields a read-only presentation.
ProfileViewDialog encapsulates the UI for inspecting a user's server profile in a terminal-style dialog. It renders a read-only view when displaying another user, and when shown for the current user via `ShowOwn`, it includes action buttons (edit profile and set status) and returns the chosen `ProfileAction`.
## Remarks
ProfileViewDialog encapsulates all the layout and formatting decisions for a user profile in a single place. It dynamically switches between a read-only view and an ownership-aware view that surfaces actions, and it applies color theming to the status and nickname fields. By centralizing this UI behavior, the dialog remains consistent across the application and reduces duplication by isolating profile presentation from business logic. The component gracefully handles a missing profile by showing an error message and returning a Close action, which defines a clear contract for callers.
Internally, `Show` delegates to `ShowInternal` with `isOwnProfile` set to false, while `ShowOwn` passes `isOwnProfile` true along with the current status and message. The dialog is constructed as a `Dialog` with title `My Profile` or `Profile — {profile.Username}`, and it populates rows for `Username`, `Name`, `Status`, [`Message`](../../../EchoHub.Core/Models/Message.cs.md) (when present), `Color`, and `Bio` using `Label`s and a `TextView`. The status value is chosen as the live status when viewing your own profile, otherwise the stored status from the profile; the status text is produced by `FormatStatus` and the color by `GetStatusColor`. The nickname color is parsed via `HexColorHelper.ParseHexColor` and applied as a scheme to the color label when available. If the provided `profile` is `null`, it shows an error dialog with `MessageBox.ErrorQuery` and returns `ProfileAction.Close`.
## Notes
- If invoked with a null profile, the dialog shows an error and returns ProfileAction.Close; callers should guard against null input or handle the Close result accordingly.
- The dialog title differentiates ownership with "My Profile" for the current user and "Profile — {username}" for others, and it uses color-coding helpers to reflect status and nickname color for quick visual cues.
- If `profile` is `null`, the dialog informs the user and returns `ProfileAction.Close`, signaling callers to handle the absence gracefully.
- The nickname color is applied only when `HexColorHelper.ParseHexColor(profile.NicknameColor)` yields a valid color attribute; otherwise the color styling is skipped, avoiding exceptions.
---
@@ -43,9 +44,6 @@ public enum ProfileAction
```
ProfileAction defines the set of actions a user can select from their profile dialog: Close, EditProfile, and SetStatus. It provides a typed representation of user intent that downstream UI logic can handle in a deterministic way, rather than relying on magic strings or numeric codes.
## Remarks
ProfileAction represents the users chosen action from the profile dialog, allowing the UI layer to dispatch the appropriate workflow in a type-safe way. By enumerating possible intents, the code can exhaustively handle all cases in a switch or pattern-match, reducing errors from invalid values. The Close action also clarifies that the action is about dialog lifecycle control as opposed to in-dialog tasks such as editing or setting status. If new actions are required in the future, they should be added here with clear naming that maps to corresponding UI behaviors.
The `ProfileAction` enum encodes the concrete actions a user selects from their profile dialog. Its values `Close`, `EditProfile`, and `SetStatus` map user intent to distinct application paths, replacing ad-hoc strings with a strongly-typed signal. Consumers use this enum in the dialog result handling to drive navigation and state changes without inspecting UI text.
---
@@ -19,28 +19,40 @@ public static class SearchDialog
```
SearchDialog is a command-palette style search dialog used to quickly navigate channels and trigger common app actions from a single, keyboard-driven interface. Use it when you want fast, non-mouse access to channels and actions by filtering a combined list and selecting with Enter.
## Source Code
Static class `SearchDialog` provides a Ctrl+K-activated, command-palette style dialog for navigating channels and triggering app actions. It merges the current `IReadOnlyList<string>` of `channels` with a fixed set of default `SearchResult` actions into a single searchable list presented in a `Dialog` consisting of a `Label` hint, a `TextField` input, and a `ListView` of results; typing filters the list and Enter selects. The `Show` method returns the selected `SearchResult` or `null` if canceled, communicating through the provided `IApplication` instance.
## Remarks
SearchDialog composes a modal dialog that presents both channel names and a predefined set of actions, merged into a single searchable list via a SearchListSource. It returns the selected SearchResult and signals completion to the hosting application by invoking RequestStop on IApplication, keeping the dialog logic decoupled from the rest of the UI. This abstraction enables a reusable, consistent navigation surface across different parts of the app.
## Example
```csharp
// Example
IApplication app = /* obtain your app instance */;
IReadOnlyList<string> channels = new[] { "general", "engineering" };
var result = SearchDialog.Show(app, channels);
if (result != null)
{
// Handle the selected item (channel or action) here.
}
```
By centralizing both channels and common actions, `SearchDialog` reduces context switching and speeds navigation from anywhere in the UI. The implementation delegates list rendering and filtering to [`SearchListSource`](../ListSources/SearchListSource.cs.md), decoupling the data shape from the presentation; adding new channels or actions simply extends the default actions or the input channels without altering the UI flow.
## Notes
- The dialog includes a hint, a text field for filtering, a list of results, and a Cancel button; selection is returned as a SearchResult, or null if cancelled.
- Ctrl+K handling in both the dialog and the search field cancels the operation by requesting stop from the application, so be aware that this combo acts as a cancel gesture rather than an open/search trigger.
- When items exist, the first item is pre-selected; filtering updates the source and may reset the selection.
- The dialog binds Ctrl+K to stop the dialog, so avoid conflicting hotkeys in the surrounding application.
## Dependency APIs (verified signatures)
The REAL, parser-verified API surface of this symbol's collaborators:
- record `SearchResult` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
- class [`SearchListSource`](../ListSources/SearchListSource.cs.md) (`src/EchoHub.Client/UI/ListSources/SearchListSource.cs`)
- field `Attribute ChannelAttribute`
- field `Attribute ActionAttribute`
- property `int Count`
- property `int MaxItemLength`
- property `bool SuspendCollectionChangedEvent`
- `void Filter(string query)`
- `SearchResult? GetItem(int index)`
- `bool IsMarked(int item)`
- `void SetMark(int item, bool value)`
- `IList ToList()`
- `void Render(ListView listView, bool selected, int item, int col, int row, int width, int viewportX)`
- `void Dispose()`
- enum `SearchResultType` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
## Symbol To Document
- Name: `SearchDialog`
- Kind: class
- File: `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
- Language: `csharp`
- ID: `7ba458ca-8e14-48c9-9536-988f98e9e83c`
---
@@ -61,15 +73,13 @@ public record SearchResult(SearchResultType Type, string Key, string Label)
| `Label` | `string` | — |
Represents a single entry in search results, encapsulating the result's category (Type), a key (Key), and a user-facing label (Label). As a positional-record, it is immutable and compared by value, which makes it convenient to pass around and render in the search UI.
Represents a single item in search results as an immutable, value-based carrier. It groups the result kind (`SearchResultType`), an identifying `Key`, and a user-facing `Label` to display in the UI. As a `record`, it gains structural equality and convenient deconstruction, which makes it easy to compare results and extract its fields when handling selections in the search dialog.
## Remarks
Use SearchResult to model a single outcome returned by the search feature. Type communicates the kind of item (as defined by SearchResultType), Key is the stable identifier for navigation or lookup, and Label is the display text shown in the results list. Because it is a deconstructible record, you can conveniently extract its fields with deconstruction or pattern matching, and equality checks are based on the content rather than the instance identity.
This type serves as a stable data contract between the search logic and the UI layer, decoupling data shape from presentation. It uses `record` semantics to provide value equality and immutability, enabling straightforward deduplication and pattern-based handling of results. The three members (`Type`, `Key`, `Label`) collectively support both programmatic lookup and user-friendly rendering.
## Notes
- Immutability: SearchResult uses a primary constructor; properties are read-only and a modified instance must be created with a with-expression or a new constructor.
- Deconstruction: The positional constructor enables deconstruction: var (t, k, l) = result; or access via result.Type, result.Key, result.Label.
- Type relies on the SearchResultType enum; when consuming code, prefer switching on Type rather than comparing display strings.
- The `Key` should be stable and unique within a given `Type` to avoid ambiguity when presenting or selecting results.
---
@@ -86,9 +96,25 @@ public enum SearchResultType
```
Represents the category of a search result in the EchoHub client UI, distinguishing Channel results from Action results. Developers reach for this enum to branch rendering or navigation logic based on the result type, instead of using boolean flags or string comparisons.
Represents the kind of item produced by a search in the UI, distinguishing [`Channel`](../../../EchoHub.Core/Models/Channel.cs.md) results from `Action` results. Use `SearchResultType` when rendering or handling search results in the `SearchDialog` flow to steer UI decisions without inspecting the raw payload.
## Remarks
Because it is a small discriminant, SearchResultType is typically consumed alongside a broader SearchResult structure. It enables simple pattern matching in switch expressions or if statements, guiding UI decisions such as which view to open or which icon to display when a user selects a result.
This enum centralizes the UI's categorization of search results, enabling the dialog to select icons, labels, or handlers in a type-safe way. It decouples the results' payload from how they're displayed and makes it straightforward to extend with additional result kinds in the future.
## Example
```csharp
SearchResultType type = SearchResultType.Channel;
switch (type)
{
case SearchResultType.Channel:
Console.WriteLine("Render as channel");
break;
case SearchResultType.Action:
Console.WriteLine("Render as action");
break;
}
```
---
@@ -18,30 +18,16 @@ public sealed class StatusDialog
```
StatusDialog is a terminal-based UI component that presents a compact dialog for updating the current user's status and an optional status message. Its Show method renders the dialog initialized with the provided current status and message, and returns a StatusDialogResult when the user saves, or null if the user cancels.
The dialog consists of a title 'Set Status', a status option selector pre-populated with the current status, a text field for the status message, and Save/Cancel actions. On Save, the selected status is captured (defaulting to Online if nothing is selected) and the message is trimmed; an empty message becomes null. The method returns a new StatusDialogResult with those values and stops the application loop via app.RequestStop(); Cancel returns null and stops the loop.
Callers use the returned result to apply the updated status and message; otherwise, no changes are made.
StatusDialog is a Terminal.Gui-based dialog that enables a user to set their [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) and an optional status message. The static `Show` method displays the dialog within an `IApplication`, initializes the controls from `currentStatus` and `currentMessage`, and returns a `StatusDialogResult` when the user saves, or `null` if the dialog is cancelled.
## Remarks
StatusDialog encapsulates the presentation logic for updating user status, isolating UI concerns from business logic. It is a small, reusable piece that orchestrates Terminal.Gui controls (Dialog, Label, OptionSelector, TextField, Button) and relies on IApplication to drive the modal flow. The use of a default Online and trimming of the message ensures sane behavior even when fields are left blank.
## Example
```csharp
var result = StatusDialog.Show(app, currentStatus, currentMessage);
if (result != null)
{
// Apply updates to the user's status and message
currentStatus = result.Status;
currentMessage = result.Message;
}
```
StatusDialog serves as a focused UI primitive that isolates status-edit behavior from the rest of the application. By wiring `OptionSelector<UserStatus>` and a `TextField` to a lightweight `StatusDialogResult`, it provides a predictable, reusable pattern for collecting user input and converting it to a simple value object. This keeps the UI code cohesive while allowing the caller to handle the result without managing Terminal.Gui lifecycle details. The dialog is deliberately minimal and self-contained, relying on the provided `IApplication` to control its lifecycle.
## Notes
- A null result indicates the user cancelled the dialog; callers should guard against applying changes in this case.
- If the user leaves the Message field blank or whitespace, the message is stored as null.
- The Save action is wired as the default action (IsDefault = true), and both Save and Cancel terminate the modal interaction by invoking app.RequestStop().
- The `message` field is trimmed and, if empty or whitespace, stored as `null`.
- Cancelling returns `null` and no `StatusDialogResult` is produced.
- When saving, if the selected status is `null`, it defaults to `UserStatus.Online`.
---
@@ -61,9 +47,9 @@ public record StatusDialogResult(UserStatus Status, string? StatusMessage)
| `StatusMessage` | `string?` | — |
StatusDialogResult is a minimal, immutable data carrier returned when the status dialog completes. It groups the chosen user status (Status) with an optional message (StatusMessage) into a single value that downstream logic can consume without inspecting the dialog UI directly. As a C# record, it benefits from value-based equality and straightforward deconstruction.
StatusDialogResult is a lightweight value object that represents the outcome of the status dialog. It pairs the chosen [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) with an optional `StatusMessage`, providing a simple, transportable result for the caller to inspect and react to.
## Remarks
StatusDialogResult encapsulates the outcome of a UI interaction into a single semantic unit that can be passed through the application flow or stored for auditing. It separates presentation concerns from business logic: callers reason about the user's status and optional message rather than UI details. The nullable StatusMessage signals that extra context is optional; consumer code should handle the absence gracefully, typically by pattern matching on Status and checking for a non-null message. The record type also supports structural equality, making tests and comparisons concise.
As a `record`, it uses value semantics: two instances are equal if their `Status` and `StatusMessage` are equal, and it is immutable by design. This makes it ideal for passing the result across boundaries and for use in pattern matching or switch expressions when reacting to different statuses. The `StatusMessage` is nullable to allow callers to omit extra context when not needed.
---
@@ -8,7 +8,13 @@ public sealed class UpdateConfirmDialog
```
UpdateConfirmDialog is a sealed utility with a single static Show method that prompts the user to confirm an available update. It builds a small modal dialog titled “Update Available” showing the current and latest versions and offers two actions: Update (default) and Cancel; it returns true if the user chooses Update and false otherwise. The method runs the provided IApplication until the user makes a choice, using RequestStop to close the dialog and return the result.
UpdateConfirmDialog is a small, self-contained UI helper that presents a modal update prompt and returns the user's decision as a boolean. Call `UpdateConfirmDialog.Show` with an `IApplication` and the current and latest versions; it constructs a `Dialog` titled 'Update Available' containing a `Label` with the version message and two `Button`s, runs the dialog, and returns `true` when the user chooses to perform the update.
## Remarks
It encapsulates the update-confirmation interaction as a reusable, modal prompt that coordinates with the host application's event loop, avoiding duplication of dialog boilerplate across the codebase.
By encapsulating the entire dialog flow, this symbol isolates the update-confirmation UX from the rest of the UI, reducing duplication across the codebase. The modal pattern—calling `app.Run(dialog)` followed by `app.RequestStop()`—ensures callers receive the result synchronously without needing to manage focus or window lifecycles themselves. It also makes testing easier by providing a single, predictable entry point for the confirmation action.
## Notes
- The dialog is modal and blocks until the user presses `Update` or `Cancel`; callers should not attempt to perform further UI work until after `Show` returns.
- It interpolates `currentVersion` and `newVersion` into the message; ensure these values are safe to display and do not contain unexpected control characters.
@@ -8,21 +8,12 @@ public static class DroppedFileParser
```
DroppedFileParser is a small utility that interprets terminal-dropped input as potential file paths and resolves them to existing files. Use it when you need to convert user-typed or pasted text into concrete file paths without scattering filesystem checks across callers.
DroppedFileParser exposes a small, focused set of helpers for recognizing and extracting absolute file path(s) from terminal input that arrives via drag-and-drop. It understands common path forms (quoted text, Windows drive-letter paths like `X:\`, UNC paths like `\\server\share`, and POSIX absolute paths starting with `/`) and uses a cheap pre-check (`LooksLikePath`) to avoid filesystem access unless the input plausibly contains a path. The primary entry point, `TryGetFiles`, returns true when the input resolves to one or more existing files and returns the discovered paths in the `files` out parameter; it supports a single path (quoted or not) or multiple space-separated tokens (each optionally quoted) and lets callers inject a `fileExists` predicate for testability (defaults to `File.Exists`).
## Remarks
This abstraction centralizes the logic for recognizing path-like input and for extracting one or more existing file paths from either a single path or a space-separated list of paths. It exposes a fast pre-check (LooksLikePath) to avoid expensive filesystem calls for clearly non-path input, and a test-friendly parser (TryGetFiles) that can inject a custom file existence predicate. The design favors explicit handling of both Windows (drive letters and UNC) and POSIX-style absolute paths, including quoted components and spaces.
## Example
```csharp
var input = "\"C:\\Temp\\report.pdf\" C:\\Data\\log.txt";
if (DroppedFileParser.TryGetFiles(input, out var files))
{
// files contains: ["C:\\Temp\\report.pdf", "C:\\Data\\log.txt"]
}
```
DropppedFileParser centralizes the path-detection logic that UI input handlers would otherwise duplicate, simplifying callers and reducing unnecessary file-system work. `LooksLikePath` provides a fast-path signal so the expensive existence check runs only when the input plausibly represents a path, while `TryGetFiles` performs the actual existence checks and returns the concrete file list. The API supports both single-path and multi-path inputs, correctly handling spaces inside quoted paths by tokenizing tokens and stripping surrounding quotes where applicable; it enforces that all tokens are fully-qualified and existing, otherwise the call fails. The `fileExists` parameter makes unit tests deterministic by allowing injection of a fake predicate instead of touching the real filesystem.
## Notes
- LookSLikePath may return true for strings that resemble paths (e.g., starting with a quote, a slash, UNC prefix, or a drive letter), so TryGetFiles should be used to confirm actual file existence.
- TryGetFiles enforces that all tokens are fully-qualified paths and that each path exists (via the injectable fileExists predicate, which defaults to File.Exists). This reduces accidental assumptions about the input.
- The tokenization logic respects quoted segments so that spaces within a single path do not split tokens unintentionally.
- Relative paths are not accepted by `TryGetFiles`; it requires fully-qualified paths for each token (and for single-path input).
- Quote handling is strict: [`StripQuotes`](../../Commands/CommandHandler.cs.md) removes matching leading/trailing quotes only when both ends use the same quote character; mismatched quotes may leave quotes in the token and affect parsing.
- For testing, pass a custom `fileExists` delegate to avoid real I/O; otherwise the default uses `File.Exists`.
@@ -8,12 +8,12 @@ public static class EmojiHelper
```
EmojiHelper converts emoji grapheme clusters to text shortcodes for safe TUI rendering. It replaces emoji with fixed-width ASCII shortcodes when available, falling back to a generic [emoji] placeholder for unknown symbols; non-emoji text passes through unchanged.
EmojiHelper is a static utility that converts emoji grapheme clusters in a string into text shortcodes for safe rendering in terminal-based UIs. It scans input text, splits it into grapheme elements, and replaces any grapheme containing emoji with a corresponding shortcode from `EmojiShortcodes`; if no mapping exists for the full grapheme, it attempts the base emoji (the first rune) and uses its shortcode; if that also fails, it inserts a generic `[emoji]` placeholder. Non-emoji text passes through unchanged. This approach avoids inconsistent emoji rendering across terminals by providing fixed-width ASCII representations for display-only outputs.
## Remarks
This utility uses grapheme-aware processing to handle complex emoji sequences (including ZWJ-joined glyphs and modifier-bearing emojis) by iterating over text elements rather than individual code points. It first attempts a full-grapheme shortcode lookup, then falls back to the base emoji (the first rune of the grapheme) if necessary, and finally uses the [emoji] placeholder when no mapping exists. An initial pass quickly determines whether any emoji exist in the input to avoid unnecessary work. The implementation relies on StringBuilder for efficient string construction, StringInfo for grapheme segmentation, and the EmojiShortcodes mapping as the source of truth for replacements.
EmojiHelper centralizes the emoji-to-shortcode conversion, isolating terminal rendering concerns from application logic. It relies on `EmojiShortcodes` for mapping and uses `StringInfo.GetTextElementEnumerator` to respect grapheme boundaries, ensuring sequences like complex emoji are treated coherently. The abstraction keeps emoji translation testable and swapable, so you can adjust shortcodes without touching UI code.
## Notes
- Unknown or unmapped emoji are replaced with [emoji], which can reduce expressiveness if the shortcode dictionary is incomplete. Ensure EmojiShortcodes covers the emoji you expect to render in your UI.
- Unknown emoji yields a generic `[emoji]` placeholder; ensure `EmojiShortcodes` covers targets or plan fallback behavior.
- Emoji detection uses a set of Unicode ranges to decide whether a grapheme contains emoji; new or platform-specific emoji outside these ranges may be missed.
- This replacement is intended for display only; do not rely on reversibility for data persistence, and be aware that updates to `EmojiShortcodes` may change outputs.
@@ -8,11 +8,10 @@ public static class HexColorHelper
```
HexColorHelper is a small utility that converts hex color strings into Terminal.Gui coloring primitives. Use ParseHexColor to obtain an Attribute suitable for styling a control's foreground, and ParseHexToColor when you need a Color value with a safe fallback for invalid input.
HexColorHelper is a small static utility that converts hex color strings into Terminal.Gui color representations. Use `ParseHexColor` when you need an `Attribute` for immediate application to a UI element, and `ParseHexToColor` when you only need the `Color` value (with an optional `fallback`) for other color-related properties.
## Remarks
By centralizing hex parsing, HexColorHelper avoids duplicating color-conversion logic and provides predictable fallbacks for malformed input. It interprets a hex string as an RGB triplet and applies it as the foreground color (with no explicit background). This keeps styling decisions consistent across the UI while keeping the parsing logic isolated in one place.
These helpers centralize hex parsing to ensure consistent handling of hex colors across the UI layer. They both tolerate the common '#'-prefixed form and treat invalid inputs gracefully by returning null or a fallback color, preventing exceptions from propagating into UI code. By encapsulating parsing logic here, you avoid duplicating string-to-color conversions and make future changes (e.g., supporting shorthand hex) easier.
## Notes
- Invalid input yields null (for ParseHexColor) or the provided fallback (for ParseHexToColor); no exceptions are thrown.
- A 6-digit hex value is required after an optional leading '#'. Non-hex characters or incorrect length return fallback/null.
- Leading whitespace before the optional '#' is not trimmed; strings starting with spaces will fail to parse gracefully.
@@ -8,18 +8,4 @@ public static class NickColorHelper
```
NickColorHelper deterministically assigns a stable color to every nickname, ensuring the same nick always maps to the same palette entry. This mirrors classic IRC behavior and lets busy channels stay readable without per-user configuration.
## Remarks
NickColorHelper isolates color selection from rendering logic by exposing a pure function GetPaletteIndex and GetAttribute. The palette itself is a fixed sequence of medium-saturation colors designed for legibility on both dark and light backgrounds; changing the palette order would re-color every nick and break visual consistency across sessions.
## Example
```csharp
var color = NickColorHelper.GetAttribute("Alice");
// Use `color` when rendering Alice's username in the UI
```
## Notes
- Null nick will throw; ensure non-null before calling GetPaletteIndex.
- The palette order is fixed; reordering or removing entries changes every nickname's color.
- The mapping uses a case-insensitive FNV-1a hash; changing the hash or its normalization will alter which nick gets which color.
NickColorHelper deterministically maps a nickname to a color attribute for users who haven't picked a nickname color. The same nick always maps to the same palette entry (classic IRC client behavior), so a busy channel stays scannable without any configuration. Use `GetAttribute(string nick)` to obtain the color `Attribute` to apply to UI elements, with the color chosen from a fixed `Palette` in a deterministic way. The helper is a pure function (no Terminal.Gui types) so it is easy to unit-test without a display driver.
@@ -8,14 +8,12 @@ public class ChannelListSource : IListDataSource
```
A colored, list-backed IListDataSource that presents channel names with visual affordances: an active-channel indicator, unread count badges, and markers for protected, private and system channels. Use this when you need a ListView-compatible data source that maintains channel ordering, per-channel unread counts and simple visual state (active, mention, protected/private, system) instead of hand-rendering each row.
A specialized `IListDataSource` implementation that provides a colored, badge-capable channel list for the UI. Use `ChannelListSource` when you need a channel list that shows an active indicator, unread count badges, and visual differences for protected, private, mention, and system channels; call `Update` to replace the source data and rely on the `CollectionChanged` event to refresh the view.
## Remarks
ChannelListSource centralizes the channel-list state required by a ListView: the ordered channel names, a per-channel unread count map and several role sets (protected, mention, private and system). It exposes a single Update method that replaces the in-memory collections in one operation and (unless suspended) raises a Reset collection-changed event so consumers can re-layout or refresh. The class also provides MaxItemLength to help the host compute layout and ToList to produce a display-friendly list of channel strings (each prefixed with '#'). Rendering is delegated to the ListView via the Render method; the class supplies attributes (ActiveAttr, UnreadAttr, NormalAttr, BadgeAttr, MentionAttr, SystemAttr) and simple prefix/marker rules so the view paints active items, unread badges and visual separation for system channels.
`ChannelListSource` centralizes both the model and the presentation hints required to render a channel list: it stores the channel names (`_channelNames`), per-channel unread counts (`_unreadCounts`), categorical sets (`_protectedChannels`, `_mentionChannels`, `_privateChannels`, `_systemChannels`), and the `_activeChannel`. Visual presentation is driven by a small set of static attributes (`ActiveAttr`, `UnreadAttr`, `NormalAttr`, `BadgeAttr`, `MentionAttr`, `SystemAttr`) and the `Render` method composes the line prefix and decorations (active marker, system rule, protection/private markers, unread badge) before drawing to the provided `ListView`. The `Update` method replaces the internal collections, recomputes `MaxItemLength` (uses `channels.Max(c => c.Length + 6)` as a conservative width heuristic), and raises a `NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)` via the `CollectionChanged` event unless `SuspendCollectionChangedEvent` is set.
## Notes
- Update clears and replaces all internal collections; call it with the full desired state rather than trying to patch individual entries.
- The Count/MaxItemLength values are derived from the current channel list. MaxItemLength computes name.Length + 6 (reserved space for prefixes/badges), so layout logic should consider that padding when sizing the list column.
- CollectionChanged will be invoked with a NotifyCollectionChangedAction.Reset at the end of Update unless SuspendCollectionChangedEvent is true. SuspendCollectionChangedEvent is a simple in-memory flag — using it prevents the Reset event from being raised during an Update.
- IsMarked and SetMark are intentionally inert (IsMarked always returns false and SetMark is a no-op), so callers should not rely on marking support from this source.
- Render moves the ListView cursor using Math.Max(col - viewportX, 0) to account for horizontal scrolling (viewportX). Hosts should provide correct viewportX and width values so rendering and clipping behave as intended.
- `ChannelListSource` is not synchronized: internal collections are not thread-safe. Callers must ensure updates happen on the UI thread or otherwise synchronize access to avoid races.
- Set `SuspendCollectionChangedEvent` to `true` to suppress the reset notification during bulk updates; remember to re-enable it if callers rely on the `CollectionChanged` event for redraws.
- The `IsMarked` and `SetMark` implementations are no-ops, so the `IListDataSource` marking contract is not supported by this source; consumers expecting persisted item marks will not get them from `ChannelListSource`.
@@ -8,15 +8,12 @@ public class SearchListSource(List<SearchResult> items) : IListDataSource
```
List data source that feeds a search dialog's ListView: it maintains an original item list, supports case-insensitive filtering by label or key, raises a Reset collection-changed notification when the filter changes (unless suspended), and renders each row with color-coding depending on the SearchResultType.
A list-data source implementation used by the search dialog that presents a filtered view of [`SearchResult`](../Dialogs/SearchDialog.cs.md) items and renders each entry with type-specific coloring. Use `SearchListSource` when you need a lightweight, read-only collection for a `ListView` that supports text filtering via `Filter` and per-item rendering via `Render`.
## Remarks
This class combines two responsibilities commonly needed by a search dialog: fast, in-memory filtering of a fixed set of SearchResult records and rendering of those results into a ListView with per-type coloring. Consumers attach to CollectionChanged to refresh the UI when Filter(string) updates the visible set. Render uses RenderHelpers.WriteText to draw the label and then fills the remainder of the column; it chooses a highlight (selected) attribute from the ListView or a per-result attribute (channel/action) and preserves the list's background when a per-result attribute leaves the background as Color.None.
`SearchListSource` holds the full set of items in `_allItems` and maintains a filtered snapshot in `_filtered` that drives `Count`, `MaxItemLength`, `GetItem`, and `ToList`. Filtering is performed by `Filter` using `StringComparison.OrdinalIgnoreCase` against both the `Label` and `Key` of each [`SearchResult`](../Dialogs/SearchDialog.cs.md). Rendering delegates text layout to `RenderHelpers.WriteText` and chooses visual attributes based on the [`SearchResultType`](../Dialogs/SearchDialog.cs.md) (using `ChannelAttribute` and `ActionAttribute`); when a chosen attribute has no background color it inherits the `ListView` fill background so the entry blends with the surrounding cells. The `CollectionChanged` event is raised with a `NotifyCollectionChangedEventArgs` reset after `Filter` updates unless `SuspendCollectionChangedEvent` is set.
## Notes
- Filter is case-insensitive and matches either SearchResult.Label or SearchResult.Key.
- When Filter receives a null/whitespace query the visible list is reset to all items and a Reset event is raised (unless SuspendCollectionChangedEvent is true).
- IsMarked and SetMark are no-ops; this data source does not track per-item marks.
- Dispose is a no-op; there are no unmanaged resources to release.
- MaxItemLength returns 0 when there are no filtered items.
- This class does not provide internal synchronization; callers should ensure thread-safety when mutating the source list or calling Filter from multiple threads.
- `Render` indexes into `_filtered` directly and assumes the caller supplies a valid `item` index; callers should use `Count` or `GetItem` to validate indices to avoid out-of-range access.
- `IsMarked` and `SetMark` are intentionally no-ops: this source does not track per-item marks, so code that expects marking behavior will need a wrapper or a different `IListDataSource` implementation.
- `Dispose` is a no-op; there are no native resources held by `SearchListSource`, but consumers that expect disposal semantics should be aware nothing is released by calling `Dispose`.
@@ -8,14 +8,17 @@ public class UserListSource : IListDataSource
```
A data source implementation for a list view that presents online users with per-user nickname colors. Use this when you need a ready-made IListDataSource that holds tuples of display text, an optional nickname color (Attribute), and the username; it supplies item count, a maximal item width, batch updates via Update, and a Render implementation that paints a status/prefix in the normal role and the username portion in the configured nickname color while respecting selection and a fixed column width.
Custom list data source used to render the online users panel where each user's nickname can be shown in a per-user color. Use `UserListSource` when you need a simple, read-only data source that supplies visible text, optional nickname coloring via `Attribute? NameColor`, and username lookup for a `ListView`-style UI; it encapsulates how items are drawn and when the list notifies listeners of wholesale changes.
## Remarks
UserListSource is a UI-focused data source: it couples a small in-memory collection of user display tuples with a Render method tailored for a ListView consumer. It delegates grapheme-aware splitting to GraphemeHelper so prefix characters (status icon and optional role badge) are drawn in the list's normal attribute while the visible username text is drawn in the per-user nickname color unless the item is selected (selection forces the normal/Focus attribute). MaxItemLength is maintained as a convenience for layout calculations and is updated by Update.
`UserListSource` stores a list of tuples of the shape `(string Text, Attribute? NameColor, string Username)` and exposes that collection through the `IListDataSource` contract: `Count`, `ToList()`, the `CollectionChanged` event and `Render(...)`. The `Update(...)` method replaces the entire internal list, recomputes `MaxItemLength` using each item's `Text.GetColumns()`, and raises a single `NotifyCollectionChangedAction.Reset` notification unless `SuspendCollectionChangedEvent` is set. Rendering is handled by `Render(...)`: it asks `GraphemeHelper.GetGraphemes(...)` for grapheme clusters, finds where the visible username starts (skipping a leading status icon and optional role badge), draws the prefix in the normal attribute and the username in the per-user `NameColor` (unless the row is `selected`), and fills the remainder of the requested `width` with spaces.
## Notes
- Update replaces the entire contents; after calling Update the class raises NotifyCollectionChangedAction.Reset unless SuspendCollectionChangedEvent is true. If you set SuspendCollectionChangedEvent to batch multiple updates you are responsible for raising/triggering an appropriate collection changed notification afterward.
- MaxItemLength is computed using each entry's Text.GetColumns(), so wide characters and grapheme clusters affect reported width — MaxItemLength is a column/terminal-width measure, not a character count.
- Rendering is grapheme-aware and respects the provided width: text drawing stops when the accumulated column width reaches the requested width. This prevents partial grapheme rendering but means long names will be truncated to fit.
- Several IListDataSource members are intentionally trivial: IsMarked and SetMark are no-ops, ToList returns the visible Text values as objects, and Dispose is a no-op. Callers should not rely on any persistent marking or disposal behavior from this class.
- The implementation contains no internal synchronization; it is not inherently thread-safe. Ensure all access (especially Update and Render) is serialized by the caller when used from multiple threads.
- `Update(...)` replaces the entire backing list and always fires a `Reset` change notification (not incremental add/remove events). Consumers that rely on fine-grained collection changes should account for that.
- `SuspendCollectionChangedEvent` prevents `Update(...)` from raising `CollectionChanged`. This is a simple way to batch updates, but callers are responsible for firing or forcing a refresh later if needed.
- `IsMarked(...)` and `SetMark(...)` are no-ops; `UserListSource` does not track per-item marks. Callers expecting mark semantics must manage marks externally.
- `GetUsername(...)` returns `null` when `index` is out of range; callers should check for `null` before using the result.
- `MaxItemLength` is computed from `Text.GetColumns()` for each item; it reflects display column width rather than raw `string.Length` and becomes `0` when the source is empty.
- `Render(...)` uses `GraphemeHelper.GetGraphemes(...)` and per-grapheme `GetColumns()` calls and will truncate output when `drawnChars + cols > width`. This ensures column-consistent drawing for wide or combining characters but may be relatively expensive if called frequently — consider caching grapheme data or avoiding per-frame allocations if rendering many items each frame.
- When `selected` is `true`, the code uses the `Focus`/`Normal` role mapping (`normalAttr`) for both prefix and username; the `NameColor` is ignored while selected. This is an intentional styling choice but may surprise callers who expect nickname coloring even for selected rows.
- `Dispose()` is empty; there are no unmanaged resources to free. The class is not explicitly thread-safe — concurrent calls to `Update(...)` and `Render(...)` without external synchronization may race.
File diff suppressed because it is too large Load Diff
@@ -8,12 +8,23 @@ public static class HubConstants
```
HubConstants is a static container for global constants used by the chat hub to configure limits, paths, and feature boundaries. It provides values such as the hub path, default channel, and various size and constraint limits, ensuring consistent behavior across components and avoiding scattered magic numbers.
HubConstants acts as the single source of truth for the chat hubs configurable limits and defaults. It groups static, compile-time constants that govern where the hub is exposed, how sessions are identified (including the IRC gateway prefix), and the upper bounds for messages, attachments, avatars, and embeds, providing a centralized reference that other components consult for validation and formatting.
## Remarks
HubConstants centralizes cross-cutting, tunable values so changes propagate consistently across messaging validation, content embedding, and endpoint configuration. Because these are compile-time constants, they are not sourced from runtime configuration; if you need different behavior per deployment, introduce a separate configuration mechanism rather than altering these constants at runtime.
HubConstants isolates cross-cutting numerical constraints from business logic, ensuring all parts of the EchoHub system enforce the same rules. It enables tuning by operators—e.g., increasing `MaxMessageLength` or `MaxAttachmentsPerMessage`—without altering core workflows, while the IRC connection-id prefix helps the presence tracker distinguish IRC-based clients from native ones. The constants also centralize embed sizing and fetch behavior to maintain predictable link previews and resource usage across gateways and clients.
## Example
```csharp
// Validate message length against hub-wide limit
if (message.Text.Length > HubConstants.MaxMessageLength)
{
// handle too long
}
// Build the path for the chat hub
var hubPath = HubConstants.ChatHubPath;
```
## Notes
- The distinction between MaxMessageNewlines (30) and MaxConsecutiveNewlines (1) matters: the first limits overall newline usage, the second limits consecutive newline runs.
- Size limits are per-file (e.g., MaxImageSizeBytes, MaxAudioFileSizeBytes, MaxFileSizeBytes) and guide validation and storage decisions; never assume a single cap covers all attachment types.
- IrcConnectionIdPrefix is used by the presence tracker to distinguish IRC gateway connections from native SignalR clients; ensure prefix checks rather than simple contains checks to avoid misclassification.
- They are compile-time constants (const) and thus require a recompilation to change; runtime configuration is not supported.
- Changes to these values reflect architectural expectations across components (UI, gateway, presence tracker, and embeds) and should be coordinated to avoid breaking client assumptions.
@@ -8,23 +8,18 @@ public static class MessageConventions
```
Cross-protocol message conventions are centralized in this static helper. It provides formatting and parsing for IRC CTCP ACTION-style messages, so /me-like actions render consistently across clients. Action messages are stored as the CTCP framing: 0x01 + "ACTION " + text + 0x01; MessageConventions.FormatAction(text) wraps a plain text string in that payload, and TryParseAction(content, out actionText) extracts the inner text when the content matches the framing. In end-to-end encrypted rooms the action marker travels with the text, preserving semantics.
Cross-protocol message conventions for action messages. Action messages (the /me style) are stored using the IRC CTCP ACTION wire format: a 0x01 prefix, the literal string `ACTION `, the text, and a trailing 0x01 suffix. This class exposes the constants `ActionPrefix` and `ActionSuffix`, plus helpers `FormatAction` and `TryParseAction` to wrap and unwrap the action text, ensuring consistent storage, rendering, and encryption behavior.
## Remarks
- This abstraction prevents scattering the CTCP ACTION framing constants across the codebase and offers a single source of truth for how action messages are stored and read.
- It isolates the low-level framing from higher-level message handling, making testing and future changes safer and easier.
- The parsing path uses ordinal string comparisons and explicitly requires both the proper prefix and suffix, plus non-empty inner text, to succeed.
ActionConventions centralize the wire-format markers so changes in one place don't ripple through callers, and to provide a clear boundary between encoding and decoding of action messages. `FormatAction` encapsulates the exact wrapper, while `TryParseAction` validates the pattern and extracts the inner text without exposing the wire markers to callers. This avoids scattering the CTCP formatting details throughout the codebase and keeps rendering logic aligned with storage format.
## Example
```csharp
var action = MessageConventions.FormatAction("waves");
if (MessageConventions.TryParseAction(action, out var text))
{
// text == "waves"
}
string content = MessageConventions.FormatAction("waves");
bool ok = MessageConventions.TryParseAction(content, out var actionText);
// ok == true, actionText == "waves"
```
## Notes
- TryParseAction(content, out actionText) returns true only if the content starts with ActionPrefix, ends with ActionSuffix, and the extracted inner text has length > 0; otherwise actionText is null and the method returns false.
- The behavior relies on ordinal comparisons to avoid culture-related differences in prefix/suffix checks.
- The inner action text can contain arbitrary characters; the method only enforces the framing and non-emptiness of the payload.
- `TryParseAction` requires the content to start with `ActionPrefix`, end with `ActionSuffix`, and have non-empty inner text; otherwise it returns false and sets `actionText` to null.
- The implementation uses ordinal comparisons to check the markers for performance and culture-invariant behavior.
@@ -8,13 +8,11 @@ public static partial class ValidationConstants
```
ValidationConstants is a centralized, static container for validation constraints used throughout the EchoHub.Core domain. It defines reusable patterns for usernames, channel names, and hex color codes, as well as a set of length limits governing passwords, display names, bios, statuses, channel topics, and chat history. The included GeneratedRegex methods expose precompiled Regex instances derived from those patterns, enabling fast, consistent validation without incurring per-call regex compilation.
ValidationConstants is a centralized repository of validation rules used across the codebase. It defines the canonical pattern strings for usernames, channel names, and hex colors, together with numeric bounds for various user-facing fields. Specifically, it exposes the strings `UsernamePattern`, `ChannelNamePattern`, `HexColorPattern`, and several limit constants such as `MaxPasswordLength`, `MinChannelPasswordLength`, `MaxDisplayNameLength`, `MaxBioLength`, `MaxStatusMessageLength`, `MaxChannelTopicLength`, and `MaxHistoryCount`. In addition, it provides precompiled Regex accessors via the `GeneratedRegex`-decorated methods `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()`, enabling fast, centralized validation without scattering literal patterns across call sites.
## Remarks
ValidationConstants provides a single source of truth for input validation. By offloading regex compilation to source generation, it avoids runtime overhead while keeping the validation rules easily discoverable and consistent across the codebase.
The class is static and partial, so callers simply reference ValidationConstants.UsernameRegex(), ValidationConstants.ChannelNameRegex(), and ValidationConstants.HexColorRegex() to obtain ready-to-use Regex instances.
By centralizing these constraints, `ValidationConstants` minimizes drift in validation rules across features (sign-up, profile updates, channel creation, etc.) and makes it easy to update rules in one place. The `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()` methods are generated at compile time by the `GeneratedRegex` attribute, which yields ready-to-use, presumably cached `Regex` instances, reducing runtime regex compilation overhead at validation points.
## Notes
- GeneratedRegex provides compile-time-compiled Regex instances, which improves performance by avoiding repeated regex compilation at runtime.
- Updating any constraint here propagates the change to all validation sites, ensuring consistency; do not duplicate rules elsewhere.
- GeneratedRegex-based accessors rely on C# source generation; ensure your project enables source generators and targets a compatible framework, otherwise these methods may not be produced.
- The constants define the canonical validation boundaries pharmacologically used by the system; changing them updates all consumers that reference these values.
@@ -7,16 +7,24 @@
```mermaid
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
flowchart TB
IChannelService["IChannelService: entry"]
IChannelService -->|"GetChannelsAsync"| PaginatedResponse["PaginatedResponse<ChannelDto>"]
PaginatedResponse -->|"items"| ChannelDto["ChannelDto"]
IChannelService -->|"GetChannelByNameAsync"| ChannelDto
IChannelService -->|"CreateChannelAsync / UpdateTopicAsync / SetChannelPasswordAsync / RekeyChannelAsync / DeleteChannelAsync"| ChannelOperationResult["ChannelOperationResult"]
IChannelService -->|"GetChannelListAsync"| ChannelListItem["List<ChannelListItem>"]
IChannelService -->|"GetChannelMetaAsync"| ChannelMetaDto["ChannelMetaDto"]
IChannelService -->|"GetChannelCryptoAsync / GetChannelKeyEnvelopeAsync"| ChannelCryptoDto["ChannelCryptoDto"]
IChannelService -->|"EnsureSystemChannelAsync"| Channel["Ensure or create server-managed Channel"]
Channel -->|"returns"| ChannelDto
IChannelService["IChannelService: entry"] -->|"GetChannelsAsync(userId, offset, limit)"| PaginatedResponse["Build PaginatedResponse of ChannelDto"]
PaginatedResponse -->|"items: ChannelDto"| ChannelDto["Map DB rows to ChannelDto"]
IChannelService -->|"CreateChannelAsync(creatorUserId, name, topic, isPublic, password?, encryptionSalt?, wrappedRoomKey?)"| Channel["Create Channel record"]
Channel -->|"return"| ChannelOperationResult["ChannelOperationResult (success/error)"]
IChannelService -->|"UpdateTopicAsync(callerUserId, channelName, topic?)"| ChannelOperationResult
IChannelService -->|"SetChannelPasswordAsync(callerUserId, channelName, password?)"| ChannelOperationResult
IChannelService -->|"RekeyChannelAsync(callerUserId, channelName, oldPassword, newPassword, newEncryptionSalt, newWrappedRoomKey)"| ChannelCryptoDto["Update encryptionSalt and wrappedRoomKey"]
ChannelCryptoDto -->|"return"| ChannelOperationResult
IChannelService -->|"DeleteChannelAsync(callerUserId, channelName)"| ChannelOperationResult
IChannelService -->|"GetChannelByNameAsync(channelName)"| ChannelDto
IChannelService -->|"GetChannelMetaAsync(channelName)"| ChannelMetaDto
IChannelService -->|"GetChannelCryptoAsync(channelName)"| ChannelCryptoDto
IChannelService -->|"GetChannelKeyEnvelopeAsync(channelName) -> (EncryptionSalt, WrappedRoomKey)"| ChannelCryptoDto
IChannelService -->|"GetChannelTopicAsync(channelName) -> (Topic, Exists)"| ChannelMetaDto
IChannelService -->|"GetChannelListAsync()"| ChannelListItem["Return list of ChannelListItem"]
IChannelService -->|"EnsureChannelMembershipAsync(userId, channelName, password?) -> (Success, Error, PasswordRequired)"| ChannelOperationResult
IChannelService -->|"EnsureSystemChannelAsync(channelName, topic?)"| Channel["Create or reclaim system Channel"]
Channel -->|"return ChannelDto"| ChannelDto
```
## Contents
@@ -35,45 +43,32 @@ public interface IChannelService
```
Provides an asynchronous API for creating, updating, deleting and querying chat channels, managing membership, and exposing channel encryption metadata. Implement this interface to centralize channel lifecycle, access control and crypto-envelope access rather than manipulating persistence or membership directly.
Provides the canonical server-side API for creating, querying, updating, and deleting chat channels and for enforcing membership and channel-level security. Use `IChannelService` when implementing application logic that needs to manage channel lifecycle (CRUD), inspect channel metadata or crypto information, handle membership checks (including password-protected rooms), or ensure server-owned system channels exist and cannot be hijacked by user-created channels.
## Remarks
The interface groups CRUD operations, read/query methods, membership checks, and crypto-related lookups so callers can depend on a single abstraction for channel business rules. Mutating methods return ChannelOperationResult (which carries IsSuccess and factory helpers) to make success/failure handling explicit; query methods return lightweight DTOs or tuples for simple lookups. EnsureSystemChannelAsync is a server-managed path that ensures required system channels exist and prevents server content from being written into user-owned rooms.
`IChannelService` centralizes channel-related policy and state so higher-level features (e.g. connection/auth layers, hub message routing, admin tools) can treat channel management as a single abstraction. It separates responsibilities: CRUD and topic/password operations return a [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) that callers must inspect (via `ChannelOperationResult.IsSuccess`) while read-only queries (e.g. [`GetChannelByNameAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `GetChannelMetaAsync`, `GetChannelCryptoAsync`) let callers obtain DTO representations. Crypto and key-envelope methods (`GetChannelCryptoAsync`, [`GetChannelKeyEnvelopeAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`) keep cryptographic metadata operations colocated with channel lifecycle logic. The [`EnsureSystemChannelAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) method is intentionally server-managed: it creates missing system channels and reclaims any same-named user-owned channels so server content is never stored in a user-controlled room.
## Example
```csharp
// Create a public channel and inspect the operation result
var createResult = await channelService.CreateChannelAsync(creatorUserId, "general", "General discussion", true);
if (createResult.IsSuccess)
// create a public channel and then fetch its DTO if creation succeeded
var result = await channelService.CreateChannelAsync(creatorUserId, "general", "General chat", isPublic: true);
if (result.IsSuccess)
{
var created = createResult; // ChannelOperationResult.Success contains the created ChannelDto
var channel = await channelService.GetChannelByNameAsync("general");
// use 'channel' (type: ChannelDto) for further operations
}
else
{
// handle failure
}
// Ensure membership for a user (third parameter is the optional password/credential)
var membership = await channelService.EnsureChannelMembershipAsync(userId, "general", null);
if (membership.Success)
{
// user is a member or was added
}
else if (membership.PasswordRequired)
{
// prompt for password and retry
}
else
{
// membership failed; membership.Error contains a message
// handle failure (inspect result for details provided by the implementation)
}
```
## Notes
- Always check ChannelOperationResult.IsSuccess before assuming a mutating operation succeeded; use the provided factory helpers on ChannelOperationResult to construct success/failure values.
- Methods that return encryption metadata (encryption salt, wrapped room key) expose envelopes, not raw symmetric keys; treat any secrets derived from these values securely.
- The source contains redacted/truncated text in some method signatures (CreateChannelAsync and EnsureChannelMembershipAsync). Verify the real parameter names and optional overloads in the codebase before calling those methods.
- Methods that return [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) (for example `CreateChannelAsync`, [`UpdateTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), [`SetChannelPasswordAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`, `DeleteChannelAsync`) must have their `ChannelOperationResult.IsSuccess` checked before assuming the operation succeeded. Do not assume a returned DTO exists unless the operation reports success.
- Several parameters are nullable (`topic`, `password`, `encryptionSalt`, `wrappedRoomKey`); callers should explicitly pass `null` when no value is intended and be prepared for implementations to treat `null` as "no value" or as an instruction to remove/clear a setting (verify service semantics for your deployment).
- [`GetChannelTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns `(string? Topic, bool Exists)` — a `null` `Topic` can mean either an empty topic or that no topic was set; check `Exists` to distinguish a non-existent channel from a channel with a `null` topic.
- [`EnsureChannelMembershipAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns a tuple including `PasswordRequired`; if `PasswordRequired` is `true`, callers should prompt for and supply a password on subsequent calls. The `Error` element may contain implementation-specific failure information.
- `GetChannelsAsync` accepts `offset` and `limit` for pagination; callers are responsible for passing sensible bounds and handling potentially large result sets incrementally.
---
@@ -96,26 +91,18 @@ public record ChannelListItem(string Name, string? Topic, int OnlineCount, bool
| `IsProtected` | `bool` | `false` |
ChannelListItem is an immutable value that represents a single entry in a channel list. It carries the core metadata needed to display or transport channel information: the channel Name, an optional Topic, the current OnlineCount, and two visibility flags (IsPublic and IsProtected) which default to true and false respectively. Use this type whenever you need a concise, stable descriptor of a channel for UI lists, payloads, or comparisons, rather than a mutable or richer domain model.
ChannelListItem is an immutable value object that describes a single channel in a channel list. It carries the channel's display name (`Name`), an optional topic (`Topic`), the number of online users (`OnlineCount`), and visibility flags (`IsPublic` and `IsProtected`). As a `record`, it provides value-based equality and straightforward construction for transport or UI scenarios, with `IsPublic` defaulting to true and `IsProtected` defaulting to false.
## Remarks
ChannelListItem benefits from value-based equality inherent to records, so two items with identical fields compare as equal, which helps with list diffs, caching, and deduplication. The Topic is nullable to accommodate channels without a topic. Defaults (IsPublic = true, IsProtected = false) reflect common expectations for channels unless stated otherwise. Because this is a record, instances are immutable; to reflect changes (for example, a rising OnlineCount), create a new instance via a with-expression.
The use of a `record` signals that this is a lightweight value object intended for transport and comparison across boundaries. It models channel metadata as a single, cohesive unit, aiding deduplication and consistent rendering in lists or API responses.
## Example
```csharp
// Basic construction with defaults for visibility flags
var item = new ChannelListItem("general", "Public channel for announcements", 12);
// Or using named arguments for clarity
var itemNamed = new ChannelListItem(Name: "general", Topic: "Public channel for announcements", OnlineCount: 12);
// Immutability in action: create a modified copy with an updated OnlineCount
var updated = item with { OnlineCount = 13 };
var item = new ChannelListItem("general", "General discussion", 12);
```
## Notes
- Topic is nullable; pass null if the channel has no topic.
- To reflect a change in OnlineCount or other fields, use the with-expression since ChannelListItem is immutable.
- Topic may be null to indicate no topic is set.
- IsPublic defaults to true and IsProtected defaults to false; pass explicit values to override.
---
@@ -8,28 +8,12 @@ public interface IChatBroadcaster
```
An abstraction for broadcasting chat-related events and notifications to connected clients. Implementations deliver channel messages, presence updates, moderation events and connection-specific errors or disconnects to the appropriate recipients; use this interface when you want hub/transport-agnostic broadcasting logic (for example to decouple business logic from SignalR or another realtime transport).
A transport-agnostic abstraction for broadcasting chat events and presence changes to connected clients. Use `IChatBroadcaster` whenever server-side code (for example a hub, worker, or command handler) needs to notify one or more clients about messages, presence updates, channel lifecycle events, moderation actions, or errors without depending on a specific delivery mechanism.
## Remarks
This interface centralizes all outbound chat notifications the server emits: channel messages, user join/leave/presence events, channel lifecycle events (updated, deleted, nuked), moderation notifications (kicked, banned), message deletions, error messages to a particular connection, and forced disconnects. It exists to keep broadcasting responsibilities in one place so higher-level code can invoke intent ("send this message to the channel" or "force-disconnect these connections") without knowing how connections are routed or how the underlying transport addresses individual connections or groups.
Implementations must honor the documented routing hints (for example, do not echo a message back to an excluded connection when excludeConnectionId is supplied). Use the channelName and connectionId parameters to determine recipients; SendErrorAsync targets a single connection, while ForceDisconnectUserAsync targets a set of connection ids.
## Example
```csharp
// typical usage from server-side chat logic
// (messageDto and presenceDto are prepared elsewhere)
await broadcaster.SendMessageToChannelAsync("#general", messageDto, excludeConnectionId: currentConnectionId);
await broadcaster.SendUserJoinedAsync("#general", "alice", presenceDto, excludeConnectionId: currentConnectionId);
// send an error to a single connection
await broadcaster.SendErrorAsync(connectionId, "You are not authorized to perform that action.");
// force-disconnect multiple connections for a user session cleanup
await broadcaster.ForceDisconnectUserAsync(new List<string> { connA, connB }, "Session revoked");
```
`IChatBroadcaster` centralizes all outgoing chat-related notifications so callers do not need to know or implement the delivery/fan-out semantics. Each method maps to a well-defined event type: `SendMessageToChannelAsync` for chat messages, `SendUserJoinedAsync` / `SendUserLeftAsync` for presence changes, `SendChannelUpdatedAsync` / `SendChannelDeletedAsync` / `SendChannelNukedAsync` for channel lifecycle, moderation actions via `SendUserKickedAsync` / `SendUserBannedAsync`, and utility operations such as `SendMessageDeletedAsync`, `SendUserStatusChangedAsync`, `SendErrorAsync`, and `ForceDisconnectUserAsync` for forced disconnects. The interface is asynchronous (`Task`-based) so implementations can perform non-blocking I/O, retries, batching, or use different transports (for example SignalR, WebSockets, or a message bus) without changing callers. The `excludeConnectionId` parameter on message/presence methods encodes the common IRC convention of not echoing a message back to the originating connection while still delivering it to other connections belonging to the same user.
## Notes
- excludeConnectionId is documented for SendMessageToChannelAsync to avoid echoing the origin connection; other methods that lack an exclude parameter (for example SendUserLeftAsync) will be delivered to all intended recipients unless an implementation-specific filter is applied.
- SendUserStatusChangedAsync accepts a list of channel names so presence updates can be routed only to relevant channels; callers should pass the minimal set of channels that need the update to reduce unnecessary traffic.
- Implementations should be asynchronous and non-blocking; broadcasting to many recipients may be best-effort and not transactional across multiple method calls.
- `excludeConnectionId` prevents delivery only to the specified connection; other connections for the same user still receive the event. Callers should pass the sending connection id to avoid echoing to that connection but should not rely on it to suppress notifications to other sessions of the same user.
- `SendChannelUpdatedAsync` includes an optional `channelName` parameter in addition to the [`ChannelDto`](../DTOs/ChatDtos.cs.md). The intent of the optional `channelName` (for example: target channel selection vs. previous name) is not obvious from the signature and should be clarified by the implementation or caller to avoid mismatched behavior.
- All methods return `Task` and must be awaited or otherwise observed by callers to ensure errors in the broadcasting layer are surfaced; implementations may perform I/O and should handle transient failures internally or propagate meaningful exceptions to callers.
@@ -3,9 +3,76 @@
> **File:** `src/EchoHub.Core/Contracts/IChatService.cs`
> **Kind:** interface
*Figure: How IChatService works.*
```mermaid
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
flowchart TB
Start["Start"]
Start --> Conn["IChatService: UserConnectedAsync(connectionId, userId, username) registers connection"]
Conn --> Join["IChatService: JoinChannelAsync(connectionId, userId, username, channelName, password?) returns (History, Error, PasswordRequired)"]
Join --> CheckPwd{"Channel requires password?"}
CheckPwd -->|"yes"| RequirePwd["Return (History=null, Error='Password required', PasswordRequired=true)"]
CheckPwd -->|"no"| Joined["Return (History=List of MessageDto, Error=null, PasswordRequired=false)"]
Joined --> History["MessageDto: history items provided by GetChannelHistoryAsync(channelName, count, offset)"]
Conn --> Send["IChatService: SendMessageAsync(userId, username, channelName, content, originConnectionId?, replyToMessageId?)"]
Send --> CheckReply{"replyToMessageId != null?"}
CheckReply -->|"yes"| ValidateReply{"reply exists and is in same channel?"}
ValidateReply -->|"no"| RejectReply["Return Error (invalid reply target)"]
ValidateReply -->|"yes"| CreateMsg["Create MessageDto with content, sender, replyToMessageId"]
CheckReply -->|"no"| CreateMsg
CreateMsg --> Broadcast["IChatService: BroadcastMessageAsync(channelName, MessageDto) avoids echo to originConnectionId"]
Broadcast --> Channel["Channel: deliver message to channel members' connections"]
Conn --> Update["IChatService: UpdateStatusAsync(userId, username, UserStatus, statusMessage) returns optional string"]
Update --> PresenceList["IChatService: GetOnlineUsersAsync(channelName) returns list of UserPresenceDto"]
Broadcast --> ChannelUpdated["IChatService: BroadcastChannelUpdatedAsync(ChannelDto channel, channelName?)"]
ChannelUpdated --> ChannelDto["ChannelDto: channel metadata"]
ChannelUpdated --> ChannelDeleted["IChatService: BroadcastChannelDeletedAsync(channelName)"]
Conn --> Query["IChatService: GetChannelsForUserAsync(username) returns List of channels"]
```
```csharp
public interface IChatService
```
I have submitted the narrative documentation for IChatService and raised a critical flag about the malformed/redacted parameter in JoinChannelAsync. The documentation includes description, remarks, an example usage, and notes that point out the signature issue and nullable-return semantics for callers to verify against the concrete implementation.
Provides chat-layer operations for connection lifecycle, channel membership, messaging, presence and cross-process broadcasting. Reach for `IChatService` when implementing or calling the application-level chat logic (for example from controllers, real-time hubs or an IRC gateway) rather than manipulating lower-level transport or persistence APIs directly.
## Remarks
`IChatService` centralizes the domain operations needed by the real-time chat surface: tracking connections ([`UserConnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), joining and leaving channels ([`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`LeaveChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), sending and retrieving messages ([`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), presence ([`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetOnlineUsersAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), and broadcasting channel or message events to other processes ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface is designed for use by controllers and gateway components (the code comments indicate the IRC gateway uses several methods), so it intentionally mixes request/response operations (join, send) with one-way broadcast methods used to propagate state across processes.
## Example
```csharp
// Typical happy-path usage from a controller or hub
var (history, joinError, passwordRequired) = await chatService.JoinChannelAsync(connectionId, userId, username, "general");
if (joinError != null) {
// handle join failure (implementation-specific semantics)
return;
}
// Show the returned history to the user
foreach (var item in history) {
// item is a [`MessageDto`](../DTOs/ChatDtos.cs.md)
}
// Send a message; the returned nullable string has implementation-dependent meaning
var sendResult = await chatService.SendMessageAsync(userId, username, "general", "Hello everyone!");
if (sendResult != null) {
// react to non-null result per the concrete implementation
}
// Broadcast a message instance (e.g. from background processing or another gateway)
// `message` here is a [`MessageDto`](../DTOs/ChatDtos.cs.md) obtained from persistence or constructed by the implementation
// await chatService.BroadcastMessageAsync("general", message);
```
## Notes
- Several methods return `Task<string?>` (for example [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface does not document the exact semantics of a non-null string (error message vs. identifier vs. other). Consumers must consult the concrete implementation or its docs to interpret these values correctly.
- The `originConnectionId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) is used to avoid echoing a broadcast back to the originating connection (IRC-like behavior). Other sessions owned by the same user still receive the message.
- The `replyToMessageId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) must reference a message that exists in the same channel; implementations should validate this constraint.
- [`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns a tuple containing `History`, `Error`, and `PasswordRequired`. Callers should handle the `Error` and `PasswordRequired` flags before assuming `History` contains usable data.
- [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) supports simple pagination via `count` and `offset`; callers should choose `count` and `offset` to limit load and avoid returning excessively large histories in a single call.
- Broadcasting methods ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)) are intentionally one-way primitives used to notify other processes; they do not return operation results and callers should not rely on them for synchronous guarantees.
@@ -8,53 +8,93 @@ public interface IEchoHubClient
```
Represents the set of callbacks the server can invoke on a connected client. Implement this interface on client-side code that subscribes to the server's real-time hub so the client can react to server-initiated events such as incoming messages, presence updates, channel changes, and administrative actions.
Represents the callback contract for notifications and control messages the server can invoke on connected clients. Implement this interface on the client side (or provide a test double) when you need a strongly-typed set of server-to-client RPCs for events such as new messages, presence changes, channel updates, moderation actions, and error or disconnect notifications.
## Remarks
This interface defines a stable, strongly-typed surface for server-to-client notifications. Each method corresponds to a distinct event the server may raise (message delivery, user presence changes, channel lifecycle events, errors, and forced disconnects). Implementations keep client-side handling decoupled from the transport layer and allow the server to call back into client logic without embedding client behavior in server code.
This interface centralizes all server-originated client callbacks into a single, versioned surface so the server can address connected clients with a known set of operations. Each method returns a `Task` to allow asynchronous client implementations (IO, UI dispatching, persistence) and to make the callbacks composable for test harnesses and runtime adapters. The nullable annotations on parameters (for example the `UserPresenceDto?` in `UserJoined` and `string?` in `UserKicked`) indicate which values the server may omit; implementations must handle those cases.
## Example
```csharp
// Minimal client-side implementation that logs events; real handlers should avoid long-running work.
public class EchoClientHandler : IEchoHubClient
using System;
using System.Threading.Tasks;
public class ConsoleEchoClient : IEchoHubClient
{
public Task ReceiveMessage(MessageDto message)
{
Console.WriteLine($"Received message: {message}");
Console.WriteLine($"[{message.Channel}] {message.Sender}: {message.Text}");
return Task.CompletedTask;
}
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
{
Console.WriteLine($"{username} joined {channelName}");
Console.WriteLine($"User joined {channelName}: {username}");
return Task.CompletedTask;
}
public Task UserLeft(string channelName, string username)
{
Console.WriteLine($"{username} left {channelName}");
Console.WriteLine($"User left {channelName}: {username}");
return Task.CompletedTask;
}
public Task ChannelUpdated(ChannelDto channel)
{
Console.WriteLine($"Channel updated: {channel.Name}");
return Task.CompletedTask;
}
public Task UserStatusChanged(UserPresenceDto presence)
{
Console.WriteLine($"Status changed: {presence.Username} -> {presence.Status}");
return Task.CompletedTask;
}
public Task UserKicked(string channelName, string username, string? reason)
{
Console.WriteLine($"User kicked from {channelName}: {username} Reason: {reason ?? "(none)"}");
return Task.CompletedTask;
}
public Task UserBanned(string username, string? reason)
{
Console.WriteLine($"User banned: {username} Reason: {reason ?? "(none)"}");
return Task.CompletedTask;
}
public Task MessageDeleted(string channelName, Guid messageId)
{
Console.WriteLine($"Message deleted in {channelName}: {messageId}");
return Task.CompletedTask;
}
public Task ChannelDeleted(string channelName)
{
Console.WriteLine($"Channel deleted: {channelName}");
return Task.CompletedTask;
}
public Task ChannelNuked(string channelName)
{
Console.WriteLine($"Channel nuked: {channelName}");
return Task.CompletedTask;
}
public Task ForceDisconnect(string reason)
{
Console.WriteLine($"Force disconnect: {reason}");
return Task.CompletedTask;
}
// Other members can be implemented similarly; keep handlers quick and non-blocking.
public Task ChannelUpdated(ChannelDto channel) => Task.CompletedTask;
public Task UserStatusChanged(UserPresenceDto presence) => Task.CompletedTask;
public Task UserKicked(string channelName, string username, string? reason) => Task.CompletedTask;
public Task UserBanned(string username, string? reason) => Task.CompletedTask;
public Task MessageDeleted(string channelName, Guid messageId) => Task.CompletedTask;
public Task ChannelDeleted(string channelName) => Task.CompletedTask;
public Task ChannelNuked(string channelName) => Task.CompletedTask;
public Task ForceDisconnect(string reason) => Task.CompletedTask;
public Task Error(string message)
{
Console.Error.WriteLine(message);
Console.WriteLine($"Error from server: {message}");
return Task.CompletedTask;
}
}
```
## Notes
- Handlers are asynchronous (return Task): keep implementations short and non-blocking to avoid delaying the server's invocation path.
- Nullable parameters (e.g. UserPresenceDto? and string?) may be null; check before accessing members.
- Server-driven callbacks can occur concurrently; ensure any shared client state mutated by these methods is accessed in a thread-safe manner.
- Catch and handle exceptions inside handlers — unhandled exceptions may affect the connection or be observable by the server depending on the transport behavior.
- Respect nullability: parameters annotated with `?` (for example `UserPresenceDto?` and `string?`) may be `null` and callers should handle those cases gracefully.
- All methods return `Task`: implementations should avoid long-running synchronous work on the calling thread (use `async`/`await` or schedule work) to prevent blocking the runtime that invokes these callbacks.
- Implementations should avoid throwing exceptions from these methods where possible; unhandled exceptions may surface to the caller or the hosting infrastructure depending on how the callbacks are invoked.
@@ -8,28 +8,27 @@ public interface IMessageEncryptionService
```
IMessageEncryptionService defines a pluggable contract for encrypting and decrypting string data, using a distinctive prefix to mark encrypted content so callers can distinguish ciphertext from plain text and pass through non-encrypted values safely. It also exposes EncryptDatabaseEnabled to reflect the server setting for encrypting database content at rest, and provides nullable variants to handle optional fields without extra null checks.
The `IMessageEncryptionService` interface defines a centralized contract for encrypting and decrypting messages used in transit and at rest. It exposes a straightforward API to convert plaintext into ciphertext and back, while the `CiphertextPrefix` marks encrypted payloads so the implementation can transparently pass through values that are not encrypted. The `EncryptDatabaseEnabled` flag surfaces the server-side setting that indicates whether data stored in the database should be encrypted at rest, enabling callers to adapt their behavior to policy.
## Remarks
This interface acts as a thin abstraction that isolates encryption concerns from business logic, enabling swap-in of different algorithms or key-management strategies without touching call sites. The public CiphertextPrefix and the Decrypt pass-through behavior for non-encrypted values provide a simple, deterministic convention for distinguishing encrypted payloads. The nullable variants help preserve nullability semantics in data-transfer surfaces while still enabling encryption when a value is present.
This abstraction minimizes scattered crypto logic by presenting a single, testable surface for encryption decisions. The pass-through behavior for content that does not begin with the `CiphertextPrefix` helps prevent double-encrypting and keeps compatibility with data already in plaintext. By providing nullable-aware methods (`EncryptNullable` and `DecryptNullable`), it cleanly handles optional values without forcing callers to perform boilerplate null checks at call sites.
## Example
```csharp
// Given an IMessageEncryptionService implementation (injected or resolved via DI)
IMessageEncryptionService service = ...;
// Assume you have an instance of IMessageEncryptionService named `service`
string ciphertext = service.Encrypt("TopSecret");
string plaintext = service.Decrypt(ciphertext); // "TopSecret"
string plain = "customer-secret";
string cipher = service.Encrypt(plain);
string decrypted = service.Decrypt(cipher); // == plain
// Decrypting non-encrypted content yields the original value (pass-through)
string passthrough = service.Decrypt("plain-text"); // "plain-text"
string? nullablePlain = null;
string? nullableCipher = service.EncryptNullable(nullablePlain); // null
string? nullableDecrypted = service.DecryptNullable(nullableCipher); // null
bool atRest = service.EncryptDatabaseEnabled;
string? nullableValue = null;
string? encNullable = service.EncryptNullable(nullableValue); // null
string? decNullable = service.DecryptNullable(encNullable); // null
```
## Notes
- Decrypt will pass through values that do not start with the CiphertextPrefix.
- EncryptNullable/DecryptNullable gracefully handle nulls by returning null.
- EncryptDatabaseEnabled indicates whether server-side encrypt-at-rest is active; use it to guide storage strategies.
- The `CiphertextPrefix` ("$ENC$v1$") is a marker used to identify encrypted data. Decrypt will return the input unchanged if it does not start with this prefix.
- `EncryptDatabaseEnabled` reflects a server policy. It indicates whether data should be encrypted at rest, but callers must still invoke `Encrypt`/`EncryptNullable` before storage to ensure encryption occurs per policy.
@@ -8,30 +8,29 @@ public interface IUserService
```
IUserService defines a contract for asynchronous user-management operations within EchoHub.Core. It exposes methods to register and authenticate users, retrieve profiles by username or by ID, update profile details, and set a user's avatar. Implementations of this interface serve as the single logical boundary for user lifecycle concerns, allowing REST endpoints and the IRC gateway to funnel through a consistent surface and enabling easier testing and swapping of storage or identity providers. The RegisterUserAsync method acknowledges server configuration: when Server:Registration is set to "invite", an inviteCode is required; when set to "closed", new accounts are rejected; all such flows funnel through this service.
`IUserService` is the asynchronous contract for common user-account operations: registration, authentication, and profile access. Implementations may back these calls with REST, an IRC gateway, or other transports, but callers interact with this interface to perform login, account creation, and profile queries without coupling to a specific transport.
## Remarks
By centralizing these operations behind IUserService, the rest of the system depends on a stable, testable contract rather than concrete data stores or authentication mechanisms. It coordinates with the UserOperationResult wrapper to communicate success or failure and, for retrieval operations, to surface user data returned on success, keeping error handling consistent across the application.
By returning `Task<UserOperationResult>` for mutating operations and `Task<UserProfileDto?>` for profile queries, the interface cleanly models success/failure and optional data. The [`UserOperationResult`](../DTOs/CommonDtos.cs.md) type provides `Success(UserProfileDto user)` and `Fail(UserError error, string message)` helpers, enabling implementations to construct consistent outcomes. The `RegisterUserAsync` method carries a server-policies cue in its comment: when `Server:Registration = "invite"`, an `inviteCode` is required; in `"closed"` mode, new accounts are refused. This centralizes registration policy at the service boundary and avoids scattering policy checks across call sites.
## Example
```csharp
// Example usage of the IUserService contract
var result = await userService.RegisterUserAsync("alice", "P@ssw0rd", displayName: "Alice", inviteCode: "INV-123");
if (result.IsSuccess)
{
// registration succeeded; you can proceed with login or profile fetch
}
```
```csharp
var profile = await userService.GetUserProfileAsync("alice");
if (profile != null)
// Example usage of IUserService
public async Task DemoAsync(IUserService userService)
{
// use profile data
var reg = await userService.RegisterUserAsync("alice", "Secret123", inviteCode: "INVITE-42");
if (reg.IsSuccess)
{
var profile = await userService.GetUserProfileAsync("alice");
// Use profile as needed
}
}
```
## Notes
- If you call UpdateProfileAsync with all arguments as null, the operation may be a no-op; only pass the fields you intend to update.
- GetUserByIdAsync returns a UserProfileDto?; handle the null case when the user does not exist.
- For registration, ensure your server's registration policy (invite vs closed) is aligned with your inviteCode usage; otherwise registration may fail.
- The `inviteCode` parameter is context-sensitive and should be supplied when the server is configured with `Server:Registration = "invite"`; otherwise it may be omitted.
- All methods are asynchronous; callers should `await` the results and branch on `UserOperationResult.IsSuccess` as appropriate.
- [`GetUserProfileAsync`](../../EchoHub.Client/Services/ApiClient.cs.md) and `GetUserByIdAsync` return `UserProfileDto?`, reflecting the possibility that a user profile may not be found or accessible in certain contexts.
@@ -26,20 +26,14 @@ public record DeleteAccountRequest(string Password)
| `Password` | `string` | — |
This record models the password-confirmation payload required when a user initiates destructive self-service account actions (such as deleting their account). It captures the password as a single field to prove the users intent before the action is executed.
Represents a request payload that carries the user's `Password` to re-confirm destructive self-service actions on the account. This separate `DeleteAccountRequest` DTO isolates credential input from other account data and is intended for use in flows that require explicit user re-authentication before irreversible operations (e.g., account deletion).
## Remarks
DeleteAccountRequest encapsulates a sensitive credential within a lightweight boundary object to keep password handling explicit in the delete workflow. By isolating the password in a dedicated payload, the system can perform authentication checks, auditing, and policy enforcement at the appropriate boundary. The record is immutable and minimal (a single Password property), which simplifies model binding and reduces the surface area for accidental data exposure.
## Example
```csharp
// When initiating a delete flow, supply the password for re-confirmation.
var request = new DeleteAccountRequest("P@ssw0rd!");
```
Isolates sensitive credential input into a minimal, purpose-built payload, enabling focused validation and auditing of destructive actions. It complements authentication state by forcing an explicit password re-entry rather than relying on session state alone, which helps mitigate accidental or unauthorized deletions. This pattern supports clearer separation of concerns between domain models and security-critical request data.
## Notes
- Treat the Password as sensitive; avoid logging or exposing it in responses.
- Use this payload only in the delete flow; ensure that the password validation is performed server-side before performing the destructive action.
- Do not log or persist the `Password` value in plaintext; keep it transient and ensure redaction in any logs.
- Ensure transport security (`TLS`) when transmitting this payload; avoid storing passwords in memory longer than needed; clear the value after usage if possible.
---
@@ -69,28 +63,16 @@ public record ExportedAttachmentDto(
| `SentAt` | `DateTimeOffset` | — |
Represents the metadata of an attachment that has been exported from a channel. It groups the file name, a URL to access the file, the file size in bytes, a textual kind descriptor, the originating channel name, and the timestamp when it was sent. Use this DTO when returning or transmitting export results to clients or cross-system boundaries to ensure a stable, serializable shape that is decoupled from internal domain models.
Represents the metadata of an attachment that has been exported, carrying the essential details needed to access and display it—`FileName`, `Url`, `FileSize`, `Kind`, `ChannelName`, and `SentAt`. It serves as a transport contract between the export logic and clients or downstream services rather than exposing internal domain entities.
## Remarks
- Being a record, instances are immutable and equality is value-based, making it ideal for transport across layers or for caching export results. It serves as a clean contract between the export process and API or consumer layers.
- It acts as a boundary object, decoupling presentation/API concerns from domain entities while preserving the essential attachment metadata needed by clients (name, access URL, size, kind, origin channel, and timestamp).
## Example
```csharp
var attachment = new ExportedAttachmentDto(
FileName: "invoice.pdf",
Url: "https://cdn.example.com/exports/invoice.pdf",
FileSize: 254000,
Kind: "document",
ChannelName: "billing",
SentAt: DateTimeOffset.UtcNow
);
```
`ExportedAttachmentDto` acts as a boundary-crossing contract: it decouples the external payload from the internal attachment representation and exposes only the data consumers require. The inclusion of a `Url` implies a downloadable resource that may be protected or time-limited, so callers should treat access as potentially ephemeral and handle expiration appropriately. Because this is a `record`, instances are immutable by default, which helps preserve the integrity of the export snapshot across layers.
## Notes
- The Kind property is a free-form string; if there is a known finite set of kinds, consider introducing a dedicated enum later to avoid inconsistent values.
- FileSize is a long and should be non-negative; implement validation at boundaries if negative values could be produced by upstream systems.
- Ensure the Url is appropriate for client access (consider expiration, authentication, and CORS as needed) since this DTO surfaces a direct link to the exported attachment.
- The `Url` is often a signed or temporary link; do not assume long-lived access and design clients to handle expiration (e.g., 404 or 403 responses).
- This DTO is strictly a data carrier; avoid embedding business logic in the payload and prefer mapping from domain models to this shape when exporting data.
---
@@ -118,26 +100,10 @@ public record ExportedMessageDto(
| `ReplyToMessageId` | `Guid?` | — |
ExportedMessageDto is an immutable data transfer object that captures the essential details of a message exported from a channel: its identity (Id), the channel it came from (ChannelName), when it was sent (SentAt), the message content (Content), and an optional reference to the message it replies to (ReplyToMessageId). It serves as a serialization-friendly payload used by export or archival pipelines, decoupled from the in-memory domain model.
ExportedMessageDto is an immutable data transfer object (record) that captures the essential data of a single exported message: the message `Id`, the `ChannelName` it was sent in, the `SentAt` timestamp, the `Content`, and an optional `ReplyToMessageId` if the message is a reply. It provides a stable, serializable contract for exporting messages to external systems or archives, decoupled from domain behavior so consumers can rely on a consistent shape without depending on domain entities.
## Remarks
ExportedMessageDto provides a stable contract for export pipelines by decoupling serialized data from the internal domain entities. Being a record, it benefits from value-based equality and immutability, which simplifies de-duplication and testing of exported payloads. The nullable ReplyToMessageId models the optional threading relationship: null means the message has no parent. Use ChannelName and SentAt as lightweight contextual metadata when reconstructing conversations in external systems.
## Example
```csharp
var message = new ExportedMessageDto(
Id: Guid.NewGuid(),
ChannelName: "general",
SentAt: DateTimeOffset.UtcNow,
Content: "Hello world",
ReplyToMessageId: null
);
```
## Notes
- The ReplyToMessageId is nullable; null indicates no parent message.
- As a record, equality is based on all properties; two messages with identical data compare as equal.
- If you need to derive a modified copy without mutating the original, use the with-expression (e.g., var updated = message with { Content = "Updated" };).
As a `record`, `ExportedMessageDto` provides value-like semantics and a predictable equality contract, which is helpful when comparing exported records or caching results during export pipelines. It also separates export concerns from the rest of the domain, making it easier to evolve the internal models without breaking external consumers.
---
@@ -165,14 +131,12 @@ public record UserDataExportDto(
| `Attachments` | `List<ExportedAttachmentDto>` | — |
Represents a persisted snapshot of a user's data as stored by the server, intended for data export or portability. It consolidates the export timestamp, the server identity, the user's profile, and the exported messages and attachments; in end-to-end encrypted rooms the message contents are ciphertext, since the server never has access to plaintext.
`UserDataExportDto` is a `record` that represents a complete snapshot of the server's stored data for a given user, produced when exporting user data for portability or archival. It contains the export timestamp (`ExportedAt`), the originating server name ([`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md)), the user's profile (`Profile`), and the exported content items: messages (`Messages`) and attachments (`Attachments`). In end-to-end encrypted rooms, the message payload is preserved as ciphertext, since the server cannot provide plaintext it never possessed.
## Remarks
UserDataExportDto is an immutable data transfer object that anchors the export pipeline to the server's stored representation. By pairing profile, messages, and attachments into a single artifact, it simplifies serialization, auditing, and versioning while guarding the boundaries between storage concerns and export logic.
This DTO acts as the stable envelope for user data exports, keeping metadata, profile, and content items together for portability and archival use. It decouples export semantics from how data is stored, permitting changes to storage without breaking export contracts. Note that for end-to-end encrypted rooms, the `Messages` are ciphertext as stored; no plaintext is accessible to the server.
## Notes
- The Messages collection contains ciphertext for end-to-end encrypted rooms; do not decrypt on the server. Decryption and user presentation must happen client-side with proper keys.
- Large exports can be memory-intensive; plan for streaming or chunked delivery in exporters.
---
@@ -27,22 +27,13 @@ public record LoginRequest(string Username, string Password)
| `Password` | `string` | — |
Represents the credentials needed to log a user in. This immutable record carries a Username and Password and is intended to be used as a data transfer object when submitting login data to authentication endpoints.
Represents the credentials payload for a login operation as an immutable data transfer object. It carries the two required fields, `Username` and `Password`, and is intended to be sent to the authentication boundary to perform sign-in. Use `LoginRequest` when you need to pass user credentials through service boundaries in a strongly-typed, single payload rather than as separate arguments.
## Remarks
As a positional record, LoginRequest provides value-based equality and deconstruction. It is immutable, with init-only properties, which helps prevent accidental mutation of credential data as it travels across system boundaries. Treat Password as sensitive data: avoid logging or displaying it, and ensure transport security when sending this DTO.
## Example
```csharp
var request = new LoginRequest("alice", "P@ssw0rd!");
```
Because `LoginRequest` is a `record`, it provides value-based equality and immutability, which makes it a natural data carrier across application layers. This abstraction helps decouple transport concerns from domain logic by centralizing credentials into a single, typed payload.
## Notes
- Password is sensitive data; avoid logging or displaying it; mask when emitted in logs or error messages.
- This is a simple data-transfer object; it contains no business logic.
- Do not log or serialize the `Password` value; treat `LoginRequest` as sensitive data and ensure transport uses TLS.
---
@@ -72,27 +63,25 @@ public record LoginResponse(
| `NicknameColor` | `string?` | — |
LoginResponse is a data transfer object that represents the server's response to a successful login. It bundles the authentication tokens (Token and RefreshToken), the token expiration moment (ExpiresAt), and the authenticated user's identity (Username), along with optional personalization fields (DisplayName and NicknameColor). This object is intended for consumption by clients to establish authenticated sessions, attach the access token to requests, refresh tokens when needed, and present user information in the UI.
`LoginResponse` represents the result of a login attempt, carrying the [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md), `RefreshToken`, `ExpiresAt`, and user identity data like `Username`, with optional `DisplayName` and `NicknameColor` for UI personalization. As a `record`, it is immutable and uses value-based equality, making it a convenient, transportable payload for authentication flows.
## Remarks
LoginResponse is an immutable value object (a record) whose identity is defined by its content. It cleanly separates transport concerns from domain logic, acting as a simple contract that different layers can rely on without side effects. The optional DisplayName and NicknameColor fields model user-facing personalization; callers must handle potential nulls when those fields are not provided.
Immutability and value-based equality make `LoginResponse` easy to compare, cache, and pattern-match in authentication workflows. It groups all login-related data in one cohesive container, reducing the risk of mismatched fields across layers. The optional `DisplayName` and `NicknameColor` allow UI layers to present user-friendly details without forcing these values for every login.
## Example
```csharp
var response = new LoginResponse(
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
RefreshToken: "def123-refresh",
RefreshToken: "defghijklmnopqrstuvwxyz",
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
Username: "alex",
DisplayName: "Alex Doe",
NicknameColor: "#FF6A00"
Username: "alice",
DisplayName: "Alice",
NicknameColor: "#1E90FF"
);
```
## Notes
- DisplayName and NicknameColor may be null if the server omits them.
- Treat this type as data-only; avoid adding behavior such as validation or mutation.
- Token values are sensitive; avoid logging them and consider secure storage/handling in the client.
- Token and RefreshToken are sensitive; avoid logging them or exposing them in UI or analytics outputs. Treat these values as secrets and secure any transport or storage paths that handle them.
---
@@ -111,21 +100,14 @@ public record RefreshRequest(string RefreshToken)
| `RefreshToken` | `string` | — |
RefreshRequest is a small, immutable data transfer object (a C# record) that carries a single value: the RefreshToken. It is used when a client requests a new access token from the authentication service, typically by posting this payload to the refresh endpoint.
An immutable data container representing the payload of a token refresh request. It exposes a single property, `RefreshToken`, which the authentication workflow uses to obtain new access tokens.
## Remarks
By representing the refresh payload as a dedicated type, the API boundary gains a clear, strongly-typed contract that can be validated and logged consistently. The use of a record ensures value-based equality and immutable semantics, which helps prevent accidental mutation during transport or handling and makes it straightforward to pattern-match or deconstruct if needed in higher layers. In the overall authentication flow, this DTO sits alongside other EchoHub authentication DTOs and forms the low-level transport shape for refresh token exchanges.
## Example
```csharp
var request = new RefreshRequest("sample-refresh-token");
```
Because this is a `record` with a single value, it provides value-based equality and straightforward deconstruction, making it ideal as a data-transfer object (DTO) across API boundaries. It decouples transport concerns from token-issuance logic, enabling the controller to receive and forward the refresh token without embedding behavior.
## Notes
- Do not log or expose the RefreshToken; avoid writing it to logs or UI.
- Ensure the token is transmitted over HTTPS and handled only in the request body, not in URLs.
- Validate that the token is non-empty before sending to the refresh endpoint; handle nulls gracefully.
- `RefreshToken` is sensitive data; avoid logging it or exposing it in error payloads.
- This type is a plain DTO with no validation or side effects; validation should occur in the service layer.
---
@@ -147,22 +129,13 @@ public record RegisterRequest(string Username, string Password, string? DisplayN
| [`InviteCode`](../Models/InviteCode.cs.md) | `string?` | `null` |
RegisterRequest is a compact data-transfer object used to convey the information necessary to register a new user. It requires a Username and Password, and optionally accepts a DisplayName and an InviteCode. Implemented as a C# positional record, it is immutable and uses value-based equality, making it ideal for transport across API boundaries and for straightforward comparisons in tests. This DTO is typically produced by a client during registration and consumed by server-side authentication logic. The DisplayName parameter is nullable with a default of null, allowing clients to omit it; InviteCode is also nullable and used only when the onboarding flow supports invitation codes.
RegisterRequest is a data-transfer object that captures the input for a user registration operation. It encapsulates the required `Username` and `Password` and includes optional `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) so callers can supply additional metadata in a single payload to the authentication endpoint.
## Remarks
This symbol acts as a stable contract for the registration flow: it encapsulates the required credentials and optional metadata in a single, immutable object. By using a record, equality and deconstruction align with value semantics, making it easy to compare requests and to pass them through layers without mutation. Because DisplayName and InviteCode are optional, validation often happens elsewhere, enabling flexible client behavior while preserving a clear API boundary.
## Example
```csharp
// Typical usage with all fields
var full = new RegisterRequest("jdoe", "P@ssw0rd", "John Doe", "INVITE-42");
// Minimal usage: only required fields
var minimal = new RegisterRequest("jdoe", "P@ssw0rd");
```
As a simple DTO, `RegisterRequest` acts as a stable contract between the public API surface and the authentication logic. It isolates the registration input structure from internal domain models, enabling independent evolution and simpler testing while the underlying registration workflow evolves.
## Notes
- Do not log or leak the Password value; treat it as sensitive data and rely on secure transport and proper logging practices.
- Optional fields may be null; server-side validation should enforce any business rules regarding DisplayName or InviteCode as appropriate.
- Do not log or serialize the `Password` field in logs or telemetry; treat it as sensitive data and rely on transport security.
- The optional fields `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) may be `null`; downstream code should handle nulls gracefully and only include them when provided.
---
@@ -45,25 +45,10 @@ public record AttachmentDto(
| `AsciiPreview` | `string?` | `null` |
Represents a file attachment attached to a chat message. It carries the attachment kind, a URL to access the resource, the original file name, the size of the file, and an optional ASCII preview used for color-tag art when available. This DTO is used when composing or processing message payloads that include attachments, or when consuming message data that contains attachment metadata. In end-to-end encrypted channels the content behind the URL and the preview may be ciphertext that the server cannot read.
A file attached to a message is represented by `AttachmentDto`. It carries the attachment's kind ([`AttachmentKind`](../Models/AttachmentKind.cs.md)), a URL to retrieve the content (`Url`), the original file name (`FileName`), and the file size in bytes (`FileSize`). If available, `AsciiPreview` holds color-tag ASCII art for images; in end-to-end encrypted channels the data behind `Url` and the preview is ciphertext the server cannot read.
## Remarks
AttachmentDto serves as a compact, immutable value object that consolidates attachment metadata for transport, storage, and rendering across UI and API boundaries. Being a record provides value-based equality, which simplifies deduplication and caching scenarios, and makes it natural to compare attachments without inspecting the entire payload. It decouples attachment handling from the message body, enabling consistent rendering and processing of attachments regardless of how the message content is structured.
## Example
```csharp
// Example: construct an attachment DTO for a file attachment
var attachment = new AttachmentDto(
default(AttachmentKind),
"https://cdn.example.com/files/document.pdf",
"document.pdf",
204800,
null);
```
## Notes
- AsciiPreview is optional; when present, it provides a text-based preview but is not guaranteed to render a full image. Clients should gracefully fall back to the URL or file name if the preview is absent.
- AttachmentDto is a record, so instances are immutable and compare by value. This supports straightforward caching and deduplication strategies across layers.
Because `AttachmentDto` is a record, it provides value-based equality and immutability, making it a stable transport object across layers. It decouples the attachment metadata from the message payload, enabling clients to render previews or retrieve content on demand without embedding binary data in the message. The `AsciiPreview` field offers a lightweight preview for image attachments, while `Url` points to the resource whose handling may be encrypted in transit.
---
@@ -83,15 +68,10 @@ public record ChannelCryptoDto(bool IsEncrypted, string? EncryptionSalt)
| `EncryptionSalt` | `string?` | — |
ChannelCryptoDto carries the public cryptographic metadata required by a client to derive its join credential from a passphrase. It should be used by clients during the channel join flow to determine if a passphrase-based derivation is necessary and to access the salt used for key derivation, without ever handling the wrapped room key.
ChannelCryptoDto is a small data container that exposes the channel's cryptographic policy: whether encryption is enabled (`IsEncrypted`) and the salt used to derive a join credential from a passphrase (`EncryptionSalt`). Use it when you need to pass this metadata across system boundaries without exposing the wrapped room key.
## Remarks
This DTO isolates derivation parameters from actual keys, enabling authentication-related components to reason about how a credential is derived without touching or exposing key material. The IsEncrypted flag indicates whether a passphrase-based join is applicable, and EncryptionSalt provides the salt used in the derivation when encryption is in effect. When IsEncrypted is false, EncryptionSalt may be null, reflecting that no passphrase-based derivation is required.
## Notes
- If IsEncrypted is true, EncryptionSalt should be non-null to derive the join credential; when false, the salt may be null.
- This is a simple data transfer object intended to convey derivation parameters safely; never serialize or expose wrapped key material.
Consolidating `IsEncrypted` and `EncryptionSalt` into a single value object reduces coupling between channel-joining logic and cryptographic operations. It makes intent explicit at call sites that must decide how to derive credentials from a passphrase. Importantly, the actual wrapped room key remains outside this DTO, preserving the security boundary that keys are only handled by the cryptographic subsystem. The nullable `EncryptionSalt` communicates that a salt is omitted when encryption is disabled.
---
@@ -127,30 +107,25 @@ public record ChannelDto(
| `IsSystem` | `bool` | `false` |
ChannelDto is an immutable data transfer object that encapsulates the core metadata of a chat channel. It groups the channels unique identifier, display name, an optional topic, visibility, message count, and creation timestamp, together with flags that describe its characteristics (protected, encrypted, and system channels). This object is commonly produced by the server when retrieving or creating channel data and is consumed by clients and services that need a stable snapshot of a channels state. As a record, ChannelDto provides value-based equality and supports convenient cloning via with-expressions without mutating the original instance.
ChannelDto is an immutable data transfer object that carries the essential metadata of a chat channel: `Id`, `Name`, `Topic`, `IsPublic`, `MessageCount`, `CreatedAt`, and the optional flags `IsProtected`, `IsEncrypted`, and `IsSystem`. As a `record`, it provides value-based equality and a straightforward bundle of properties suitable for transport across layers or API boundaries without exposing domain entities. Use it when returning channel summaries, listings, or lightweight channel representations to clients or other services, rather than leaking internal domain models.
## Remarks
ChannelDto serves as a transport-friendly abstraction that decouples channel metadata from domain models. The boolean flags encode common channel semantics: IsPublic indicates whether the channel is publicly discoverable, IsProtected denotes restricted access, IsEncrypted signals encryption usage, and IsSystem marks built-in, system-managed channels. CreatedAt represents the creation-time snapshot and should be treated as immutable; for updates, create a new ChannelDto instance (e.g., with a with-expression) rather than mutating the existing one.
ChannelDto exists to decouple transport contracts from domain models; by consolidating channel metadata into a single, serializable shape, it enables stable APIs and easier versioning. The `IsSystem` flag allows distinguishing system channels (like announcements) from user-created ones, while `CreatedAt` helps clients sort or display recency.
## Example
```csharp
var channel = new ChannelDto(
Id: Guid.NewGuid(),
Name: "general",
Topic: "General discussion",
IsPublic: true,
MessageCount: 482,
CreatedAt: DateTimeOffset.UtcNow,
IsProtected: false,
IsEncrypted: true,
IsSystem: false
Guid.NewGuid(),
"general",
"General discussion",
true,
128,
DateTimeOffset.UtcNow
);
```
## Notes
- Topic may be null; consumers should handle absence of a topic gracefully.
- ChannelDto is immutable; to derive a modified version use the with expression (e.g., channel with { Name = "new-name" }).
- Boolean flags default to false when omitted, so explicit values should reflect the actual channel semantics.
- Topic is nullable; consumers should handle `null` before displaying a topic, or provide a fallback.
---
@@ -186,14 +161,13 @@ public record ChannelMetaDto(
| `CreatedAt` | `DateTimeOffset` | — |
ChannelMetaDto is a data transfer object that captures human-facing metadata for a chat channel as surfaced by the /meta command. It exposes the channel's identity (Id), presentation (Name), optional description (Topic), security properties (IsEncrypted, IsProtected), participation metrics (MessageCount, UniqueUserCount), a best-effort size estimate of content (EstimatedSizeBytes), and the creation timestamp (CreatedAt). For encrypted channels, the server retains counts, timestamps, and blob sizes but cannot read the content itself; EstimatedSizeBytes is the sum of stored attachment blob sizes plus message text length, so it is an estimate rather than an exact on-disk total.
ChannelMetaDto is an immutable data transfer object that presents a concise, human-facing snapshot of a channel's metadata (the `/meta` command) to clients. It exposes the channel's identity (`Id`, `Name`), optional `Topic`, security/status flags (`IsEncrypted`, `IsProtected`), audience metrics (`MessageCount`, `UniqueUserCount`), and an estimated on-disk footprint (`EstimatedSizeBytes`), which is the sum of stored attachment blob sizes plus message text length and thus an estimate rather than an exact total. For encrypted channels the server still knows these figures — counts, timestamps, and stored blob sizes — even though it cannot read the content itself. The `CreatedAt` field records when the channel was created.
## Remarks
This immutable record serves as a stable, client-facing contract that decouples internal storage from UI rendering. By aggregating these fields, it enables lightweight channel listings and meta views without exposing message content, while still providing enough information to gauge activity and scope.
ChannelMetaDto serves as a stable, read-only contract between server and clients for channel overviews. As an immutable `record`, it guarantees value-based equality and prevents accidental mutation, which simplifies caching and change detection in UI layers. The metadata it carries—identity, topic, security flags, counts, and size—supports efficient rendering of channel lists and summaries without exposing the channel contents.
## Notes
- Topic may be null; clients should handle absence gracefully when rendering.
- EstimatedSizeBytes is an approximation; the value may drift as new messages or attachments are added.
- The `EstimatedSizeBytes` is an estimate (sum of stored attachment blob sizes and message text length); it is not an exact on-disk size and can drift as content changes.
---
@@ -206,7 +180,7 @@ public record CreateChannelRequest(
string Name,
string? Topic = null,
bool IsPublic = true,
string? [REDACTED:CONNECTION_STRING_PASSWORD]
string? Password = null,
string? EncryptionSalt = null,
string? WrappedRoomKey = null)
```
@@ -218,20 +192,20 @@ public record CreateChannelRequest(
| `Name` | `string` | — |
| `Topic` | `string?` | `null` |
| `IsPublic` | `bool` | `true` |
| `EncryptionSalt` | `string? [REDACTED:CONNECTION_STRING_PASSWORD]
string?` | `null` |
| `Password` | `string?` | `null` |
| `EncryptionSalt` | `string?` | `null` |
| `WrappedRoomKey` | `string?` | `null` |
Represents the payload for creating a new chat channel. It encapsulates the channel name, an optional topic, a visibility flag, and optional cryptographic data used to secure channel communications. A redacted credentials field stands in for a sensitive connection password and should be supplied securely at runtime rather than stored or logged.
The `CreateChannelRequest` is an immutable data transfer object that encapsulates all parameters needed to create a new chat channel. It requires a `Name` and exposes optional settings including `Topic`, whether the channel is public via `IsPublic` (default true), and optional security fields such as `Password`, `EncryptionSalt`, and `WrappedRoomKey` used for encrypted channel setup. Use this record when issuing a channel creation operation so that all related options are passed as a single, strongly-typed payload rather than a loose collection of parameters.
## Remarks
This record is an immutable value object intended to be used as a single payload passed from client to API for channel creation. It coalesces related creation parameters in one place, facilitating validation and transport across layers while remaining independent of any particular persistence or network protocol. The redacted password field highlights a security concern: avoid exposing credentials in logs or UI surfaces; handle it through secure channels only.
By collecting channel creation options into a single `CreateChannelRequest`, the boundary between API inputs and domain logic is cleanly expressed. The defaults on `IsPublic` and the optional nature of the other fields enable flexible requests while preserving a stable, serializable contract across process boundaries. This abstraction also makes future extension safer: new optional settings can be added without altering existing call sites.
## Notes
- Name is required; Topic, IsPublic, EncryptionSalt, WrappedRoomKey are optional with sensible defaults (Topic = null, IsPublic = true, EncryptionSalt = null, WrappedRoomKey = null).
- IsPublic defaults to true; set to false to create a private channel.
- Sensitive fields (the redacted password) must be handled securely; avoid logging or exposing the value in logs or UI.
- Do not log sensitive fields: avoid writing `Password`, `EncryptionSalt`, or `WrappedRoomKey` to logs or telemetry.
- Nullable fields imply validation; ensure meaningful values before persisting or acting on them.
- If `IsPublic` is false, consider validating that a `Password` is provided for access control; enforce this at the API or domain layer if required.
---
@@ -261,25 +235,28 @@ public record EmbedDto(
| `ThemeColor` | `string?` | `null` |
EmbedDto is a lightweight, immutable data carrier for the metadata needed to render a rich embed in chat messages. As a C# record, it provides value-based equality and convenient construction, making it ideal for transporting embed information across layers without mutating state. It carries optional metadata fields (SiteName, Title, Description, ImageAscii, ThemeColor) and requires a Url that points to the embed resource.
EmbedDto is an immutable data container used to carry the metadata needed to render a rich embed, such as in chat messages or UI panels. It groups the surface data for an embed: `SiteName`, `Title`, `Description`, `ImageAscii`, `Url`, and an optional `ThemeColor`, so callers can supply a complete embed definition in a single object.
## Remarks
This abstraction centralizes all embed-related data into a single contract, decoupling embedding details from other message payloads. By using a record, it gains structural equality and easy pattern matching, which simplifies testing and usage in render pipelines. The optional ThemeColor guides UI theming, while ImageAscii allows lightweight, ASCII-based previews when a graphical asset is unavailable.
As a `record`, `EmbedDto` provides value-based equality and supports deconstruction, making it straightforward to compare embeddings or pattern-match in rendering logic. It serves as a clean boundary between data authors and renderers: producers populate an `EmbedDto`, consumers render an embed from its fields without needing to understand surrounding domain.
## Example
```csharp
var embed = new EmbedDto(
SiteName: "Aurora Gallery",
Title: "Landscape Preview",
Description: "A sample landscape embed",
ImageAscii: "[ASCII_ART]",
Url: "https://example.org/embeds/landscape",
ThemeColor: "#3366FF");
SiteName: "EchoHub",
Title: "Welcome",
Description: "A friendly hello from EchoHub.",
ImageAscii: " ___ \n (o o) \n \_/ ",
Url: "https://echohub.example",
ThemeColor: "#4B8BBE"
);
```
## Notes
- All fields except Url are optional, so a minimal EmbedDto can be created with just the Url.
- Being a record, EmbedDto is immutable and supports with-expressions to create modified copies without changing the original instance.
- `ThemeColor` is optional; omit it to use a default theming.
- `Url` is required; ensure it is a valid URL to enable link previews.
- Because `EmbedDto` is a `record`, two instances with identical field values compare equal.
---
@@ -309,25 +286,7 @@ public record JoinChannelResult(
| `WrappedRoomKey` | `string?` | `null` |
JoinChannelResult is a value object that conveys the outcome of attempting to join a chat channel. It exposes whether the operation succeeded, provides the channel's message history for immediate rendering, and carries optional security-related data (password requirement, encryption salt, and wrapped room key) that consumers can act on after the join completes.
## Remarks
JoinChannelResult centralizes all information produced by a join attempt, keeping the caller decoupled from the join logic. By pairing a success flag with the History and optional security fields, it supports both happy-path UI rendering and encrypted or password-protected channels without additional payloads. The inclusion of EncryptionSalt and WrappedRoomKey suggests a workflow where the client may fetch or negotiate encryption material as part of joining, rather than as a separate round-trip.
## Example
```csharp
// Successful join with history
List<MessageDto> history = new List<MessageDto>();
var result = new JoinChannelResult(true, history);
// Join that requires a password and includes encryption material
var secured = new JoinChannelResult(true, history, PasswordRequired: true, EncryptionSalt: \"salt123\", WrappedRoomKey: \"wrappedKey\");
```
## Notes
- Error is typically non-null only when Success is false; use it to surface the failure reason to the user.
- EncryptionSalt and WrappedRoomKey are meaningful only for encrypted or password-protected channels; they may be null in plain channels.
- History should be treated as the initial set of messages to render immediately after a join; it may be empty in failure scenarios or when a channel has no prior messages.
Represents the outcome of a join-channel operation as a `JoinChannelResult` type. It exposes a `bool` `Success` flag, a `List<MessageDto>` `History` of messages retrieved for the channel, and optional metadata including a `string?` `Error`, a `bool` `PasswordRequired`, and optional encryption data (`string?` `EncryptionSalt`, `string?` `WrappedRoomKey`).
---
@@ -365,15 +324,13 @@ public record MessageDto(
| `ReplyTo` | `ReplyRefDto?` | `null` |
MessageDto is an immutable data transfer object that captures the essential details of a chat message as it moves across the EchoHub chat API surface. Implemented as a C# record, it provides value-based equality and straightforward construction for message data, making it ideal for serialization and transport between layers (e.g., API, client, and service boundaries). The object aggregates core message data such as Id, Content, SenderUsername, ChannelName, and SentAt, while also supporting optional enhancements like Attachments and Embeds, a human-friendly SenderDisplayName, and a ReplyTo reference for threaded conversations. This shape keeps message-related concerns contained in a single DTO without leaking domain internals, enabling predictable data contracts for consumers.
Represents a chat message as a data contract used by the chat API. It captures the message `Id`, the textual `Content`, and author info (`SenderUsername`, optional `SenderNicknameColor`, optional `SenderDisplayName`), the `ChannelName`, and the `SentAt` timestamp. Optional `Attachments` and `Embeds` support rich content, while `ReplyTo` references a prior message.
## Remarks
This symbol serves as a boundary object that encapsulates a complete chat message payload, including optional media and UI hints. By composing AttachmentDto and EmbedDto, it allows rich messages to travel without forcing callers to depend on internal domain types. The use of a record emphasizes that MessageDto represents a snapshot of message data at a point in time; consumers should treat instances as immutable and, if changes are needed, create new instances. The presence of optional fields (SenderNicknameColor, Attachments, Embeds, SenderDisplayName, ReplyTo) reflects real-world variability in messaging scenarios (e.g., plain text messages, media-enabled messages, or replies).
This DTO is designed as a transport-friendly aggregation of message data, suitable for serialization across clients and services. By referencing the dedicated `AttachmentDto` and `EmbedDto` types, it remains extensible for rich content, and its optional fields (`Attachments`, `Embeds`, `ReplyTo`, `SenderNicknameColor`, `SenderDisplayName`) allow the same shape to cover both simple and feature-rich messages.
## Notes
- Attachments and Embeds may be null; downstream code should handle nulls or default to empty collections to avoid null reference errors.
- SenderNicknameColor and SenderDisplayName are optional UI hints and may be absent; consumers should gracefully handle missing values.
- ReplyTo is optional and only populated for messages that are replies to another message; check for null before accessing related data.
- `Attachments` and `Embeds` may be `null`; treat them as empty sequences when rendering or iterating.
---
@@ -399,27 +356,13 @@ public record RekeyChannelRequest(
| `NewWrappedRoomKey` | `string` | — |
Passphrase change for an encrypted channel: the client proves knowledge of the old passphrase (old auth key), then supplies the re-wrapped room key under the new one.
This RekeyChannelRequest is a data transfer object used to perform a channel rekey. It carries the old password to prove knowledge of the current key, the new password and its salt, and the re-wrapped room key to be used under the new credentials.
The `RekeyChannelRequest` record represents the data the client sends to request a rekey of an encrypted channel. It conveys knowledge of the current passphrase (via `OldPassword`) and the new credentials and wrapped key to apply (via `NewPassword`, `NewEncryptionSalt`, and `NewWrappedRoomKey`).
## Remarks
This type serves as a single payload boundary in the channel rekey workflow, encapsulating all data required to authenticate the existing context and establish a new encryption context for the room. Being a record enforces immutability and provides straightforward value-based equality, which simplifies testing and auditing of rekey requests. It acts as a contract between the client and server for the rotation of the room key tied to a new passphrase.
## Example
```csharp
var request = new RekeyChannelRequest(
OldPassword: "old-passphrase",
NewPassword: "new-passphrase",
NewEncryptionSalt: "salt-42",
NewWrappedRoomKey: "BASE64_WRAPPED_ROOM_KEY"
);
```
This DTO enables the server to verify the client's possession of the existing auth key while atomically applying new encryption material in a single operation. It decouples the client's input from the rekeying logic, allowing validation, auditing, and rollback policies to be applied at the server boundary.
## Notes
- Do not log or expose OldPassword, NewPassword, or NewWrappedRoomKey; treat them as highly sensitive and avoid telemetry.
- NewEncryptionSalt should be a cryptographically strong, per-operation salt generated by a secure RNG; do not reuse salts.
- This object represents a single rekey operation and should not be reused for multiple independent requests.
- Do not log `OldPassword` or `NewPassword`; treat these values as ephemeral and ensure transport-layer secrecy.
---
@@ -443,23 +386,13 @@ public record ReplyRefDto(
| `Content` | `string` | — |
ReplyRefDto is a compact, immutable data transfer object that identifies the message a user is replying to. It carries the target message's ID, the original sender's username, and the Content of that message as transmitted over the network, enabling clients and services to render contextual reply previews and preserve the reply's linkage. Content is treated exactly like ordinary message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). If the original message has been deleted, the related MessageDto will be null; the reply reference remains a valid anchor for rendering the reply context.
Represents a reference to the message that a reply targets. It carries the target message's identifier (`MessageId`), the original sender's username (`SenderUsername`), and the reply content (`Content`), which is treated exactly like message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). Null on a `MessageDto` when the original message no longer exists.
## Remarks
Represents the reply target in chat threads as a minimal reference, decoupling the UI payload from the full MessageDto. It ensures consistent wire-format handling across plaintext and end-to-end encrypted rooms, while allowing clients to display reply context without requiring the entire original payload.
## Example
```csharp
var reference = new ReplyRefDto(
MessageId: Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301"),
SenderUsername: "alice",
Content: "Hello world"
);
```
ReplyRefDto acts as a compact pointer that preserves the link between a reply and its target message without duplicating payloads. It separates transport- and encryption-aware handling from display logic, enabling clients to decrypt or render the referenced content while the server retains plaintext-only signals. In threaded chat UX, this symbol supports rendering reply previews and context for the target message.
## Notes
- Content is the exact on-wire representation of the referenced message; it may be ciphertext in encrypted rooms and should be decrypted by the client when applicable.
- If the original message has been deleted, the MessageDto may be null, but the ReplyRefDto still anchors the reply context for UI rendering; callers should handle potential missing referenced data gracefully.
- Be aware that `Content` might be ciphertext in encrypted rooms and may not be human-readable until decrypted; do not display it as plaintext without decryption.
---
@@ -479,17 +412,7 @@ public record SendMessageRequest(string ChannelName, string Content)
| `Content` | `string` | — |
SendMessageRequest is an immutable data transfer object that encapsulates the information required to send a message to a specific chat channel. It combines the ChannelName and the Content to be delivered so transport or messaging layers can operate on a single payload. As a record, it provides value-based equality and easy cloning with the with-expression, which helps when constructing variations without mutating existing instances.
## Remarks
Acts as a boundary contract between UI/API layers and the messaging service. The record's immutability and structural equality make it reliable for logging, caching, and test assertions. Validation rules or routing decisions should live outside this DTO; this type should not perform domain validation. Its simple two-string shape also makes it friendly to common serialization mechanisms, enabling straightforward transport across boundaries.
## Notes
- No validation is performed by the type itself; ensure ChannelName and Content conform to domain rules before sending.
- The type is immutable; to modify, create a new instance (or use the with-expression) rather than mutating an existing one.
- Suitable for serialization; the plain two-property shape works well with JSON, XML, or other common serializers.
SendMessageRequest is a simple, immutable data carrier (record) that encapsulates the channel to which a message should be sent and the message content itself. Use this `SendMessageRequest` when you need to issue a message to a specific chat channel, providing both the `ChannelName` and the `Content` in a single object rather than passing multiple parameters or ad-hoc structures.
---
@@ -508,18 +431,18 @@ public record SendUrlRequest(string Url)
| `Url` | `string` | — |
SendUrlRequest is a tiny, immutable URL payload represented as a C# record. Its intended for scenarios where a URL must be passed across boundaries in a strongly-typed way rather than as a raw string, gaining value-based equality and straightforward deconstruction in the process.
SendUrlRequest is a minimal value object used to convey a URL as a request payload. As a `record` with a single `string Url` positional parameter, it provides value-based equality and immutability, making it ideal for passing URL data through layers or across API boundaries instead of threading raw `string` values.
## Remarks
Using a record for this DTO ensures immutability, value-based equality, and built-in deconstruction. This makes SendUrlRequest a natural fit for messaging or API surfaces that expect a dedicated URL payload type instead of raw strings, reducing the chance of accidental mutation and enabling pattern-based handling of the URL payload.
`SendUrlRequest` serves as a precise contract for operations that require a URL. Its `record` semantics ensure structural equality and allow easy deconstruction; by encapsulating the `Url` property, it clarifies intent and supports serialization as a simple payload.
## Example
```csharp
var request = new SendUrlRequest("https://example.com");
var req = new SendUrlRequest("https://example.com");
```
## Notes
- No validation is performed inside the type; ensure the URL is valid at the call site or in downstream handlers.
- No URL validation is performed by this type; validate the URL in the caller or service layer before processing.
---
@@ -538,26 +461,7 @@ public record UpdateTopicRequest(string? Topic)
| `Topic` | `string?` | — |
Represents a request to update the topic of a chat or conversation. This immutable record acts as a lightweight DTO that carries an optional Topic value; use it when issuing an update operation—provide a non-null Topic to set a new topic, or pass null to indicate that the topic should be cleared or left unchanged by the API, depending on server semantics.
## Remarks
This abstraction communicates the intent of updating only the topic field, leveraging a nullable Topic to express optionality. The record nature provides value-based equality and simple construction, and you can create modified copies with the with-expression (e.g., updating the Topic while preserving other fields in a derived request).
## Example
```csharp
// Set a new topic
var request = new UpdateTopicRequest("New Topic");
// Clear the topic (behavior depends on the API)
var clearRequest = new UpdateTopicRequest(null);
// Create a modified copy
var updated = request with { Topic = "Updated Topic" };
```
## Notes
- Topic is nullable; serialization and API behavior may vary—null may mean "no change" or "clear" depending on the endpoint.
- Because this is a record, instances are immutable; use the with-expression to derive variations without mutating the original.
Represents a request payload to update a topic, encapsulating an optional `Topic` value. As a positional-record, it provides an immutable, lightweight data carrier that callers populate with the new topic string when issuing an update to a chat's topic.
---
@@ -587,12 +491,14 @@ public record UserDto(
| `LastSeenAt` | `DateTimeOffset` | — |
UserDto is a lightweight, immutable data transfer object that conveys a user's identity and presence-related attributes across boundaries such as API responses or UI bindings. It aggregates the user's unique identifier, login name, optional display name and nickname color, current status, and the last seen timestamp so clients can present a consistent and responsive user summary.
`UserDto` is an immutable data transfer object that carries a concise snapshot of a user for chat workflows. It exposes the users `Id` (`Guid`), `Username`, optional `DisplayName` and `NicknameColor`, the current `Status` ([`UserStatus`](../Models/UserStatus.cs.md)), and the `LastSeenAt` timestamp (`DateTimeOffset`). Use this DTO when returning or transferring lightweight user data across API boundaries or UI layers instead of exposing full domain entities.
## Remarks
As a record, UserDto benefits from value-based equality and structural immutability, making it easy to compare user summaries and safely pass them around without worrying about accidental mutation. DisplayName and NicknameColor are optional to accommodate scenarios where presentation details are missing. LastSeenAt and Status provide presence information that can drive UI indicators and sorting.
Being a `record` with positional parameters, `UserDto` benefits from value-based equality and convenient deconstruction, which is helpful for tests and payload comparisons. The nullable fields `DisplayName` and `NicknameColor` reflect optional user profile data; readers should handle the possibility of missing values gracefully.
## Notes
- DisplayName and NicknameColor are nullable; null should be treated as absent presentation data.
- Nullable fields require null checks during consumption.
- Being immutable, modifying a `UserDto` requires creating a new instance (e.g., via a `with` expression).
- The `LastSeenAt` is a `DateTimeOffset`; ensure consistent time zone handling across systems.
---
@@ -32,14 +32,21 @@ public record ApiResponse(bool Success, string? Message = null, List<string>? Er
| `Errors` | `List<string>?` | `null` |
ApiResponse is a lightweight data transfer object used to convey the outcome of an operation. It carries a required Success flag and optional Message and Errors to provide feedback and diagnostics to callers.
Represents a standard API outcome as a `record` with a `bool` `Success`, an optional `string?` [`Message`](../Models/Message.cs.md), and an optional `List<string>?` `Errors`. Use `ApiResponse` to package the result of API operations or service methods into a single, strongly-typed object for consistent client consumption instead of scattering boolean flags and messages across code.
## Remarks
Used as a common response shape across service boundaries to avoid ad-hoc return types. The primary purpose is to separate control flow (success/failure) from payload, facilitating simple success messaging and error propagation. Be mindful that Errors is a `List<string>`, which remains mutable if the same instance is shared; convert to a read-only collection or copy before returning to external consumers.
By centralizing outcome data in `ApiResponse`, callers can handle success/failure logic in a uniform way and avoid ad-hoc boolean checks scattered through the code. The `Errors` collection is intended for granular, field-level validation messages that the client can display; the [`Message`](../Models/Message.cs.md) offers a concise summary, while `Success` drives flow control.
## Example
```csharp
var success = new ApiResponse(true);
var failure = new ApiResponse(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" });
```
## Notes
- The Errors property is a mutable `List<string>`—wrap or copy it if you intend to preserve a fixed snapshot when returning to consumers.
- Message may be null; supply a default user-friendly message or handle nulls in UI/logging.
- The `Errors` property is a mutable `List<string>`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList<string>` or an immutable collection.
- When `Success` is true, you may omit [`Message`](../Models/Message.cs.md) and `Errors` or set them as appropriate; when `Success` is false, provide a meaningful [`Message`](../Models/Message.cs.md) and optionally populate `Errors` to detail issues.
---
@@ -61,27 +68,22 @@ public record ApiResponse<T>(bool Success, string? Message = null, List<string>?
| `Data` | `T?` | `default` |
`ApiResponse<T>` is a generic wrapper you return from API methods to convey a successful outcome, an optional human-friendly message, and a payload of type T, along with any per-call errors. Use this pattern when you want a consistent contract for success, messaging, and data across endpoints rather than returning raw data alone.
A generic wrapper for operation results that standardizes API responses. It indicates success with `Success` and carries an optional [`Message`](../Models/Message.cs.md), a `List<string>` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`.
## Remarks
`ApiResponse<T>` is an immutable value type (a record with a primary constructor) that standardizes how results are communicated. It separates the data payload from status information, allowing clients to inspect Success, Message, and Errors independently from Data. Because Message and Errors are optional, responses can remain concise for successful operations while still providing rich error detail when needed.
This abstraction decouples the shape of a successful response from the actual data, enabling consistent error handling and client-side parsing across services. By returning `ApiResponse<T>` from operations, you centralize how success, messages, and validation details are conveyed, which simplifies cross-cutting concerns like localization and error translation.
## Example
```csharp
using System.Collections.Generic;
// Successful response with data
var success = new ApiResponse<string>(true, "Operation completed", null, "payload-data");
// success with data
var result = new ApiResponse<string>(true, "Operation completed", null, "payload");
// error with details
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Email is invalid" }, null);
// Failed response with errors
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" }, null);
```
## Notes
- Message and Errors are nullable; always check Success before relying on these fields, and provide defaults if you need non-null output.
- `ApiResponse<T>` is immutable; to modify it, use a with-expression to create a copy (e.g., var updated = result with { Data = newData };).
---
## ChannelOperationResult
@@ -107,14 +109,13 @@ public record ChannelOperationResult(ChannelDto? Channel, ChannelError? Error, s
| `ErrorMessage` | `string?` | — |
ChannelOperationResult is a lightweight result wrapper used by channel-creation/lookup operations to return either a ChannelDto on success or an error descriptor on failure. Callers typically inspect IsSuccess and then access Channel or Error/ErrorMessage, using the static factories to produce a well-formed result rather than constructing it directly.
ChannelOperationResult is an immutable wrapper that conveys the outcome of a channel-related operation. It either carries a [`ChannelDto`](ChatDtos.cs.md) when the operation succeeds or a `ChannelError` with an `ErrorMessage` when it fails; the static helpers `Success` and `Fail` make the intent explicit when constructing results.
## Remarks
It captures the outcome of channel-oriented operations in a single, immutable value, reducing the need for exception-based control flow. By pairing either a Channel with no error or an Error with a message, it forces consumers to handle both success and failure paths in a uniform way. It complements the ChannelDto and ChannelError types by providing a minimal, self-describing container that can be passed through layers without leaking implementation details.
ChannelOperationResult uses a C# `record` to express a simple, value-like outcome. It centralizes success/failure information for channel-oriented operations, enabling uniform error handling and reducing scattered null-checks. Consumers should inspect `IsSuccess` before accessing [`Channel`](../Models/Channel.cs.md); when `IsSuccess` is true, [`Channel`](../Models/Channel.cs.md) is non-null, and when false, `Error` and `ErrorMessage` describe the failure.
## Notes
- Prefer the static factories to create instances to preserve the intended invariant that a result carries either a Channel or an error. The public constructor can produce degenerate states if misused.
- The ErrorMessage is optional; provide a descriptive message to aid debugging when using Fail.
- Prefer constructing via `ChannelOperationResult.Success(...)` or `ChannelOperationResult.Fail(...)` rather than the primary constructor to preserve the invariant that a successful result has a non-null [`Channel`](../Models/Channel.cs.md) and a failed result has non-null `Error`.
---
@@ -134,15 +135,10 @@ public record ErrorResponse(string Error, string? Detail = null)
| `Detail` | `string?` | `null` |
ErrorResponse is a small, immutable data transfer object used to convey error information from the server to API clients. Implemented as a C# record with two positional properties, Error and Detail, it carries a concise error identifier or message and optional supplemental details. Use it when standardizing error payloads across API endpoints or error-handling middleware that wants to provide a consistent error shape.
Encapsulates a standardized error payload with a mandatory `Error` code and an optional `Detail` string for extra context. As a `record`, it is immutable by design and supports value-based equality and deconstruction, which makes it ideal for returning a single, comparable error object from APIs or services. Use this type to produce consistent, serializable error information across the system.
## Remarks
Using a record provides value-based equality and immutability, making ErrorResponse a stable payload that is easy to compare in tests and to clone with modifications via with-expressions. The Error field represents a short error code or message, while Detail offers optional, human-friendly context. This type is intended to be reused across API boundaries, ensuring clients receive a uniform error shape.
## Notes
- Avoid leaking sensitive internals in Error; prefer stable, client-friendly codes or messages.
- Detail is nullable; when null, serialization may omit the property depending on serializer settings.
- As a DTO, this record should be produced by a dedicated error-handling path rather than constructed manually in business logic.
Centralizes the error payload shape to ensure all error responses share a single contract. The optional `Detail` field provides human-friendly context without breaking clients that only inspect the `Error` code. Because it is a `record`, it naturally supports comparisons and pattern matching when handling error responses.
---
@@ -164,24 +160,13 @@ public record PaginatedResponse<T>(List<T> Items, int Total, int Offset, int Lim
| `Limit` | `int` | — |
Represents a paginated result set for a collection of items of type T. It bundles the items for the current page together with paging metadata (Total, Offset, and Limit), enabling consumers to render pages and request subsequent pages without fetching the entire dataset. Use `PaginatedResponse<T>` when an API or service returns a slice of a larger collection and you need to convey both the page content and the overall size.
Represents a paged result set for a collection of items of type `T`. It bundles the current page of data (`Items`) with paging metadata: the total item count (`Total`), the starting offset (`Offset`), and the page size limit (`Limit`). This shape is used by APIs that support paging to convey both the data and how to fetch additional pages; the use of a `record` provides value-based equality and immutability for API responses.
## Remarks
This generic DTO unifies paging across different endpoints by pairing a page of items with metadata describing the total size of the set and the paging window (Offset and Limit). Consumers can derive the total number of pages and navigate accordingly, without duplicating paging logic.
## Example
```csharp
var page = new PaginatedResponse<int>(
Items: new List<int> { 1, 2, 3 },
Total: 10,
Offset: 0,
Limit: 3
);
```
Using a `record` for `PaginatedResponse<T>` gives value-based equality and an immutable data shape, which makes it natural for transporting paging results across boundaries. It centralizes both the data (`Items`) and its paging metadata (`Total`, `Offset`, `Limit`) in a single coherent DTO, reducing the risk of mismatch between data and paging state when consumed by clients or other services.
## Notes
- The Items property is a `List<T>`, which is mutable. Mutating the list after construction will affect the PaginatedResponse instance. If you require immutability of the collection, consider exposing `ReadOnlyCollection<T>` or `IReadOnlyList<T>` instead of `List<T>`, or wrap the list before returning.
- Because `PaginatedResponse<T>` is a record, the wrapper itself uses value-based equality, but the `List<T>` contained in Items is compared by reference. Two instances with equal contents but different `List<T>` instances will not compare equal.
- The `Items` collection is a `List<T>`, which is mutable. If you require immutability guarantees, wrap it in a read-only collection or clone the list before exposure.
---
@@ -208,14 +193,13 @@ public record UserOperationResult(UserProfileDto? User, UserError? Error, string
| `ErrorMessage` | `string?` | — |
Represents the outcome of a user-related operation: it either carries a UserProfileDto for success or a UserError and an ErrorMessage for failure. Use IsSuccess to branch on the result and create instances via Success(user) for success or Fail(error, message) for failure.
An immutable result wrapper for user-related operations. It encapsulates either a [`UserProfileDto`](ProfileDtos.cs.md) payload via [`User`](../Models/User.cs.md) on success, or a `UserError` and a diagnostic `ErrorMessage` on failure. Use the static factories `Success` and `Fail` to construct consistent results, and check `IsSuccess` to decide how to proceed.
## Remarks
This abstraction uses a record with nullable payload fields to model a simple Result pattern without introducing a separate discriminated union. It provides a single return type across methods that can either yield a user profile or fail with details, enabling concise consumer code that checks IsSuccess first. Because User is nullable when the result is a failure, and because Error and ErrorMessage are null on success, callers should guard access to User unless IsSuccess is true. The helper methods ensure the invariant that a successful result always carries a user while a failure carries an error and message.
By encapsulating both success payload and failure details into a single value, this symbol standardizes how user-operation results are communicated. Callers check `IsSuccess` and then access either the [`User`](../Models/User.cs.md) payload or the `Error`/`ErrorMessage` to react. Because it is a `record`, equality is based on its contents, which helps tests and caching rely on value semantics.
## Notes
- Read result.User only after confirming IsSuccess; otherwise the value may be null.
- On failure, User will be null; consult Error and ErrorMessage for details.
- Directly constructing with a mismatched state (for example, a non-null `Error` but a null or missing `ErrorMessage`) can create inconsistent results; prefer the provided factories to enforce the invariant that success results include a [`User`](../Models/User.cs.md) and no error, while failures include an `Error` and an `ErrorMessage`.
---
@@ -235,14 +219,11 @@ public enum ChannelError
```
ChannelError enumerates the discrete failure cases that can arise when managing channels in EchoHub. It provides a finite set of error codes so callers can distinguish invalid input, duplicates, missing resources, permission issues, and protected resources without resorting to free-form strings.
ChannelError is an enum that enumerates the standard error conditions that may arise when working with channels in the `EchoHub` domain. It provides a typed set of failure reasons—`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, and `Protected`—to be returned by channel-related operations, enabling callers to branch on the specific cause and handle it uniformly rather than parsing strings.
## Remarks
This enum lives in the DTO layer to convey precise failure reasons from service or repository operations to API clients. By centralizing channel-related errors, it enables consistent error handling, mapping to user-friendly responses, and easier client-side interpretation across create, update, and lookup workflows. The member names align with common REST/DTO conventions, reducing ambiguity when serializing and documenting API contracts.
## Notes
- Changing the enum's members or their order can impact clients that serialize/deserialize error codes; treat it as a public contract.
- If you enable numeric JSON serialization for enums, ensure the API contract documents the expected codes to avoid confusion.
`ChannelError` centralizes the failure kinds that can occur during channel-related operations and is intended to be carried by DTOs that report operation results. It enables type-safe error handling, allowing callers to pattern-match on the exact failure (`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, `Protected`) and map them to appropriate responses without parsing human-generated messages. This separation of error kind from presentation keeps the API consistent as channel semantics evolve.
---
@@ -262,21 +243,12 @@ public enum UserError
```
Represents the set of user-related errors that can occur during authentication, registration, lookup, or other user-identity operations in the EchoHub DTO layer. This enum provides a typed, contract-friendly way to communicate failure modes from server to client, enabling centralized handling and consistent feedback without scattering string literals across the codebase.
Values include:
- ValidationFailed: input data failed validation.
- AlreadyExists: a resource with the given identifier already exists.
- NotFound: the requested user or resource could not be found.
- InvalidCredentials: credentials were invalid during authentication.
- Banned: the user is banned from the system.
The `UserError` enum defines the canonical set of failure conditions related to user accounts that may be surfaced by operations in the core DTO layer. Members include `ValidationFailed`, `AlreadyExists`, `NotFound`, `InvalidCredentials`, and `Banned`, each representing a distinct error scenario that downstream code can pattern-match to drive error responses and user messaging.
## Remarks
By consolidating these common errors into a single enum, this abstraction decouples transport contracts from domain logic and supports uniform error mapping on the client. It simplifies UI messaging, and it allows the server to evolve its error vocabulary without changing method signatures.
This enum centralizes common user-domain errors so that authentication, registration, and profile-management flows can share a consistent error-handling strategy. By codifying these cases in a single type, callers can translate domain failures into uniform API responses and UI messages without depending on implementation details.
## Notes
- Be mindful of how the enum is serialized in API responses (numeric vs string); consider standardizing on string representations to avoid client breakage when new values are added.
- Adding new values is a contract change; document and version the API accordingly, and ensure clients handle unknown values gracefully.
- This enum is a DTO-level error vocabulary; do not encode domain exceptions here.
- When mapping these errors to user-facing messages, avoid exposing sensitive internal details and rely on generic messaging driven by the enum value.
---
@@ -25,21 +25,13 @@ public record CreateInviteRequest(int? MaxUses = null, int? ExpiresInHours = nul
| `ExpiresInHours` | `int?` | `null` |
This record serves as the payload for creating an invitation. It carries optional constraints that govern the invite: MaxUses limits how many times the invite can be redeemed, and ExpiresInHours determines how long the invite remains valid (in hours). When constructing the request, omit values you dont want to constrain; null properties indicate the server should apply its defaults.
Represents the request payload for creating an invite, carrying optional constraints for the invite. The nullable `MaxUses` and `ExpiresInHours` allow callers to omit constraints. As a `record`, it provides value-based equality and immutability, making it a convenient, typed carrier for API calls.
## Remarks
Because CreateInviteRequest is a C# record, it provides value-based equality and immutable semantics, making it a reliable DTO for API calls and caching. The nullable properties express optional constraints without introducing separate flags, keeping the surface area small and expressive.
## Example
```csharp
var request = new CreateInviteRequest(MaxUses: 5, ExpiresInHours: 24);
```
This type centralizes the concept of invite constraints and cleanly separates client request construction from business logic. It interoperates with the invite-creation pathway by encoding optional parameters as nullable properties, allowing the API to apply defaults when a field is null.
## Notes
- Null on a property means no constraint; the API defaults apply.
- Many serializers omit null fields; if the API requires an explicit indicator for "no constraint," ensure your serializer preserves the field or you configure it accordingly.
- If you need to convey zero constraints explicitly, pass 0 (not null) for the respective property; null is not the same as zero.
- Null values indicate 'not specified' and will be treated as absent by the invite-creation endpoint; set only the fields you intend to constrain.
---
@@ -69,26 +61,9 @@ public record InviteDto(
| `UseCount` | `int` | — |
InviteDto is a small, transport-oriented representation of an invitation. It encapsulates the invitation code, the creator's username, the moment of creation, an optional expiry, and simple usage counters, making it suitable for API responses and inter-layer data transfers without revealing domain internals.
InviteDto is an immutable data transfer object that carries the metadata for an invitation: the `Code`, the creator's username (`CreatedByUsername`), the creation time (`CreatedAt`), an optional expiration (`ExpiresAt`), and usage counters (`MaxUses` and `UseCount`). It is designed for transporting invitation data across application boundaries without behavior, making it easy to serialize, deserialize, and compare by value.
## Remarks
As a record, InviteDto is immutable and uses value-based equality, which makes caching and comparisons straightforward. It decouples transport concerns from domain logic by presenting only the data clients need. The fields map directly to invitation semantics: Code is the token, CreatedByUsername and CreatedAt capture provenance, ExpiresAt denotes expiry (nullable means no expiry), and MaxUses/UseCount express the usage limits and current consumption.
## Example
```csharp
var invite = new InviteDto(
Code: "WELCOME-ABC123",
CreatedByUsername: "admin",
CreatedAt: DateTimeOffset.UtcNow,
ExpiresAt: DateTimeOffset.UtcNow.AddDays(7),
MaxUses: 5,
UseCount: 0
);
```
## Notes
- Null ExpiresAt means the invitation does not expire; ensure your validation logic accounts for that.
- InviteDto is immutable; to reflect state changes (e.g., after a use), construct a new instance rather than mutating the existing one.
- Use UTC times for CreatedAt/ExpiresAt to avoid timezone ambiguity.
Because it is defined as a `record`, `InviteDto` benefits from value-based equality and structural immutability, ensuring that two invitations with the same data compare equal and that the payload remains unchanged after construction. The nullable `ExpiresAt` conveys that an invitation might have no expiration; consumers must treat a null as no expiry. The `MaxUses` together with `UseCount` enables the system to enforce limits at the boundary without embedding logic here. This symbol sits at the boundary between persistence, API contracts, and business logic, keeping the shape of invitation data consistent across layers.
---
@@ -27,14 +27,14 @@ public record AssignRoleRequest(string Username, ServerRole Role)
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
AssignRoleRequest is a lightweight, immutable data transfer object that carries the intent to assign a specific server role to a user. It encapsulates just two pieces of information—the target Username and the desired Role—and is intended to be serialized and sent to moderation or authorization services that perform the actual role assignment.
AssignRoleRequest is a lightweight, immutable data container (a positional `record`) that carries the target `Username` and the `Role` to be assigned. It serves as the payload for moderation workflows when granting a [`ServerRole`](../Models/ServerRole.cs.md) to a user, enabling consistent transport of this intent across API boundaries without embedding behavior. As a `record`, it uses value-based equality and can be copied with a `with` expression to create variations.
## Remarks
The record type provides value-based equality and immutability, making it a reliable payload for messaging boundaries between UI, services, and backend handlers. By expressing the action as data rather than behavior, it supports clean separation of concerns and straightforward routing in moderation workflows.
This symbol acts purely as a data carrier for the moderation flow, separating payload shape from the enforcement logic. It relies on the `Username` and `Role` values to identify the target user and the desired permission, enabling services to validate and enact the change consistently.
## Notes
- Ensure Username conforms to identity rules at the boundary before processing the request.
- Because this is an immutable record, callers should create a new instance for every distinct request; do not modify an existing instance.
- Ensure `Username` is a valid existing member; the DTO does not enforce existence.
- The `Role` must be a valid [`ServerRole`](../Models/ServerRole.cs.md) value; rely on server-side validation to handle invalid roles.
---
@@ -53,13 +53,7 @@ public record BanRequest(string? Reason = null)
| `Reason` | `string?` | `null` |
BanRequest is a lightweight, immutable data container used when issuing moderation bans. It carries an optional Reason and is designed to be passed as a single object through the moderation pipeline instead of a group of disparate parameters. This structure makes future extension straightforward (e.g., adding additional ban metadata) without changing call sites.
## Remarks
BanRequest acts as a boundary between the transport/presentation layer and the moderation domain. Using a record provides value-based equality and predictable serialization, which aids testing, logging, and caching. The optional Reason supports both silent bans and bans accompanied by rationale, with policy decisions about requiring a reason typically enforced at higher layers.
## Notes
- Reason is nullable; handle nulls gracefully when displaying or persisting data, and apply any policy about requiring a reason at the appropriate layer.
BanRequest is a simple data carrier used to submit a moderation ban action, optionally including a rationale. Its only member, `Reason`, is nullable and defaults to null, so callers may omit a reason when none is provided.
---
@@ -78,15 +72,19 @@ public record KickRequest(string? Reason = null)
| `Reason` | `string?` | `null` |
KickRequest is a lightweight, immutable payload used when performing a moderation kick. It carries an optional Reason describing why the kick occurred. Callers construct this record when issuing a kick action and attach the reason if one is known; if no reason is provided, Reason remains null. The record shape ensures value-based equality and easy serialization across boundaries, making it a convenient transport object for moderation workflows.
KickRequest is a minimal, immutable data carrier used to convey a moderation kick action. It carries an optional `Reason` explaining why the kick is issued. Callers instantiate a `KickRequest` when initiating a kick, providing a `Reason` if available; if no reason is supplied, the `Reason` property is `null`.
## Remarks
KickRequest isolates the transport of a kick action from its core moderation logic. This abstraction makes it easy to extend later with additional fields (for example, moderatorId, timestamp, or kick ban duration) without changing the public contract. It also supports consistent logging and audit trails by treating the kick reason as optional metadata.
KickRequest being a `record` makes it a value object with structural equality and immutability, which is helpful when routing kick intents through handlers or messaging layers. It encapsulates the kick payload so that higher-level services can work with a single, consistent input type rather than ad-hoc parameters.
## Example
```csharp
var req = new KickRequest("Spamming in chat");
```
## Notes
- Reason is optional; validate as needed at the API boundary if your scenario requires a non-null reason.
- When serializing, null Reason might be omitted depending on serializer configuration; be explicit if you need to communicate 'no reason'.
- This is a simple DTO; do not conflate it with the domain entity for a kick; use it to transport data.
- `Reason` is nullable; downstream code should handle `null` and decide whether a reason is required.
- Records provide value-based equality; two `KickRequest` instances with the same `Reason` compare equal.
---
@@ -106,22 +104,6 @@ public record MuteRequest(string? Reason = null, int? DurationMinutes = null)
| `DurationMinutes` | `int?` | `null` |
MuteRequest is a compact, immutable data transfer object used to initiate a moderation mute. It carries two optional fields: Reason and DurationMinutes, allowing you to specify a rationale and a duration when issuing a mute; omitting either field leaves that detail to the receiver's policy.
## Remarks
By grouping the fields into a single record, this abstraction reduces API surface area and provides a consistent payload for mute-related actions across the moderation layer. The record semantics also enable value-based equality and straightforward testing and transport.
## Example
```csharp
// Mute for 30 minutes with a reason
var request = new MuteRequest("Spamming in chat", 30);
// Mute without specifying details
var request2 = new MuteRequest();
```
## Notes
- Reason may contain user-provided content; avoid including it in logs or telemetry unless explicitly permitted.
- Because the type is a record with nullable fields, ensure boundary validation and handle nulls gracefully at the call site or in the receiving layer.
MuteRequest is a lightweight data transfer object used to specify the parameters of a mute action in moderation flows. It includes two optional values: `Reason`, a `string?` describing why the mute is issued, and `DurationMinutes`, an `int?` indicating how long the mute should last; both default to `null` if not provided. This allows callers to mute with a default duration or provide additional context for auditing and user experience.
---
@@ -27,21 +27,7 @@ public record AvatarUploadResponse(string AvatarAscii)
| `AvatarAscii` | `string` | — |
AvatarUploadResponse is a tiny, immutable data container that represents the servers response to an avatar-upload operation. It carries a single payload, AvatarAscii, which holds the ASCII-art representation of the uploaded avatar. Use this type as a typed contract when returning avatar data from a service or API endpoint, rather than returning a raw string scattered through your responses.
## Remarks
This abstracted DTO isolates the avatar representation behind a named contract, making it easier to evolve the API (e.g., by adding metadata) without breaking call sites. The record semantics ensure value-based equality and straightforward deconstruction, which pairs well with serialization and testing.
## Example
```csharp
var resp = new AvatarUploadResponse("ASCII_ART");
Console.WriteLine(resp.AvatarAscii);
```
## Notes
- AvatarAscii may contain newline characters; ensure your JSON/HTTP layer preserves them.
- Keep the payload size reasonable; extremely large ASCII art can inflate responses.
- This type is a pure DTO with no behavior; avoid placing business logic here.
AvatarUploadResponse is a lightweight data container that carries the ASCII representation of a user-uploaded avatar. Its sole payload is the `AvatarAscii` string, which downstream clients can render to display the avatar in text form after an upload.
---
@@ -65,23 +51,22 @@ public record UpdateProfileRequest(
| `NicknameColor` | `string?` | `null` |
UpdateProfileRequest is a data transfer object used when updating a user's profile. All fields are optional, enabling partial updates by supplying only the fields you want to change (DisplayName, Bio, or NicknameColor). This object is typically sent to a profile update endpoint or service, where the provided values are applied while unspecified fields remain unchanged.
This `UpdateProfileRequest` is a `record` that carries a partial update payload for a user's profile. By supplying only non-null properties (e.g. `DisplayName`, `Bio`, or `NicknameColor`), callers express which fields should be updated; fields left as `null` indicate no change for that field.
## Remarks
By modeling the payload as a record with nullable properties, this abstraction communicates intent clearly: you're patching specific aspects of a profile rather than replacing it wholesale. It decouples API contract from the underlying domain model and reinforces immutability semantics for the request object. The combination of a concise DTO and nullable members makes it straightforward for clients to express partial updates without constructing separate patch types.
Using a `record` provides value-based equality and inherent immutability, which makes it ideal for data-carrying DTOs. The ability to set properties to `null` gives a clean contract for partial updates; consumers should treat nulls as 'do not modify' for that field and pass through only the intended changes to the update operation.
## Example
```csharp
// Update only the display name
var request1 = new UpdateProfileRequest(DisplayName: "Nova");
// Update multiple fields
var request2 = new UpdateProfileRequest(DisplayName: "Nova", Bio: "Software engineer", NicknameColor: "#1E90FF");
```csharp
var request = new UpdateProfileRequest(DisplayName: "Nova", NicknameColor: "#FFAA00");
```
## Notes
- Omitted properties are treated as "no update" by the receiver; a null value may be interpreted differently depending on backend semantics.
- If you need to clear a value, verify the server's rules: null may not clear a field unless explicitly supported; you may need to provide an empty string or use a dedicated API path to clear a value.
- Ensure the update handler interprets nulls as "no change" to avoid overwriting existing values.
---
@@ -103,14 +88,15 @@ public record UpdateStatusRequest(
| `StatusMessage` | `string?` | `null` |
UpdateStatusRequest is a small, immutable data transfer object used to submit a user's status update. It bundles the new Status and, optionally, an accompanying StatusMessage to be processed by a profile update operation.
This `UpdateStatusRequest` record encapsulates the payload required to update a user's profile status. It carries the new [`UserStatus`](../Models/UserStatus.cs.md) and an optional `StatusMessage`, and is intended to be used when issuing a status update to APIs or command handlers where a consistent update payload is expected.
## Remarks
Being a C# 9 record, UpdateStatusRequest is immutable and supports value-based equality, which makes it reliable to pass across process boundaries and into tests. The Status is a required field that identifies the new user state via UserStatus, while StatusMessage provides optional context. This DTO participates in the profile update workflow and is typically serialized as part of requests to the profile service.
By modeling the input as a dedicated value object, this abstraction centralizes validation and transport concerns at the boundaries between the domain and application layers, ensuring a stable contract for status updates. It also isolates update-related concerns from the rest of the profile payload, making it easier to evolve serialization, auditing, or routing rules without touching domain entities.
## Notes
- StatusMessage is nullable; if the receiver accepts no message, null can be sent and should be handled gracefully.
- Because UpdateStatusRequest is a record, you can create modified copies using the with expression, e.g. existing with { Status = newStatus } to preserve other fields.
- The `StatusMessage` property is nullable. Callers must handle the possibility of a missing message when consuming this payload.
---
@@ -142,15 +128,10 @@ public record UserPresenceDto(
| `IsIrc` | `bool` | `false` |
Represents a single snapshot of a user's presence in EchoHub. This record aggregates the user's identity (Username and optional DisplayName), their current presence state (Status and optional StatusMessage), and their server role (Role). It also carries UI-related hints such as NicknameColor and an IsIrc flag indicating whether the presence originated from IRC. The type is a C# record with positional parameters, making it an immutable, value-based data object that is ideal for transport across API boundaries and for equality comparisons of presence data.
Represents the presence-related data for a user in profile contexts, bundling the `Username`, optional `DisplayName`, optional `NicknameColor`, current `Status`, optional `StatusMessage`, `Role`, and the `IsIrc` flag into a single immutable DTO (with `IsIrc` defaulting to `false`). It is intended to be created and transported as a coherent unit when rendering user cards or updating presence in the UI or API responses, rather than scattering these fields across multiple structures.
## Remarks
Consolidating identity, status, and role into one DTO reduces the number of cross-cutting data transfers required to render a user in a presence list or chat UI. The NicknameColor provides a presentation cue without forcing consumers to derive display styling; the IsIrc flag lets calling code distinguish between sources. As a record, instances compare by their values, enabling straightforward caching, deduplication, and change detection.
## Notes
- Nullable fields (DisplayName, NicknameColor, and StatusMessage) may be null; callers should handle nulls gracefully.
- IsIrc defaults to false; set to true when constructing from IRC-origin data.
- This is a positional-parameter record; properties are init-only and the object is immutable after construction; create a new instance to represent a changed presence.
Acts as a stable boundary for presence data used by profile-related UI and API surfaces, consolidating identity, status, and role information into one payload. The [`UserStatus`](../Models/UserStatus.cs.md) and [`ServerRole`](../Models/ServerRole.cs.md) collaborators encode the allowed presence states and roles, while `NicknameColor` provides a UI cue without forcing a separate domain type. Being a `record`, it relies on value equality to simplify change detection and caching as presence updates propagate.
---
@@ -190,13 +171,9 @@ public record UserProfileDto(
| `LastSeenAt` | `DateTimeOffset` | — |
Represents a compact, transport-friendly snapshot of a user's profile used across boundaries (e.g., API responses, UI layers). As a C# record, it provides value-based equality and immutability, ensuring a stable contract when serializing user data. It collects identity (Id, Username), optional display attributes (DisplayName, Bio, NicknameColor, AvatarAscii), current status (Status, StatusMessage), role (Role), and timestamp metadata (CreatedAt, LastSeenAt).
UserProfileDto is an immutable data transfer object that represents a snapshot of a user's profile for API responses and inter-layer communication. Implemented as a `record`, it carries a stable payload including the user's identity (`Id` of type `Guid`, `Username`), optional display details (`DisplayName`, `Bio`, `NicknameColor`, `AvatarAscii`), presence (`Status` of type [`UserStatus`](../Models/UserStatus.cs.md), `StatusMessage`), role (`Role` of type [`ServerRole`](../Models/ServerRole.cs.md)), and timestamps (`CreatedAt`, `LastSeenAt` of type `DateTimeOffset`).
## Remarks
This DTO exists to decouple internal domain models from the data contract exposed to clients. By using a dedicated record, changes to the underlying domain models won't automatically ripple into API payloads. The explicit nullable fields model optional user attributes, and the timestamp fields communicate when the profile was created and last observed; consumers must handle time values robustly across time zones.
## Notes
- Nullable properties (DisplayName, Bio, NicknameColor, AvatarAscii, StatusMessage) may be null; handle accordingly in consumers.
- CreatedAt and LastSeenAt are DateTimeOffset values; when displaying, convert to a user-friendly timezone or use UTC representation as defined by the API contract.
By modelling the payload as a `record`, `UserProfileDto` benefits from value-based equality and straightforward serialization for API clients. It serves as a transport contract that decouples external API surfaces from the internal domain model, allowing optional fields to convey partial profile information without mutating server state.
---
@@ -24,14 +24,10 @@ public record EncryptionKeyResponse(string Key)
| `Key` | `string` | — |
EncryptionKeyResponse is a tiny, immutable data transfer object that carries a single encryption key via its Key property. Use it whenever a caller must receive an encryption key in a strongly-typed envelope (instead of returning a plain string) to improve clarity and compatibility with serialization and tooling.
EncryptionKeyResponse is a minimal, strongly-typed envelope used to return an encryption key from server-side DTOs. It is implemented as a C# `record` with a single property `string Key`, providing value-based equality and convenient deconstruction while keeping the surface area stable for serialization and future extension.
## Remarks
By leveraging a C# record, EncryptionKeyResponse benefits from value-based equality, structural deconstruction, and concise construction. It serves as a semantic wrapper around the raw key, making intent explicit in APIs that issue or relay keys, and aligns with other DTOs in the EchoHub.Core DTOs layer.
## Notes
- The Key contains sensitive material; avoid logging or exposing it in request traces. Ensure transport channels are secure (TLS) and that only authorized callers can obtain the key.
- Because it is a simple wrapper, use it when a typed envelope adds value (e.g., API contracts or structured responses) and avoid over-modeling plain, ephemeral keys.
Using a one-property `record` as a DTO provides a stable, strongly-typed surface for returning the key, while enabling easy evolution (e.g., adding metadata like algorithm, expiration, or salt) without breaking client contracts. It also leverages `record` semantics to support value-based equality and clean deconstruction when used in responses.
---
@@ -60,14 +56,13 @@ public record ServerStatusDto(
| `RegistrationMode` | `string` | `"open"` |
ServerStatusDto is an immutable data-transfer object that represents the current status of a server in EchoHub. It exposes the server name, an optional description, the number of online users, the total number of channels, and a registration mode (defaulting to open). As a C# record with a primary constructor, it benefits from value-based equality and convenient deconstruction, making it a natural payload for API responses that describe the server's state.
Represents a lightweight, immutable snapshot of a server's status for transport between layers or to clients. It exposes the server's `Name`, optional `Description`, current `OnlineUsers`, total `TotalChannels`, and the `RegistrationMode` (defaulting to `open` when not provided).
## Remarks
A record provides value-based equality and immutability for a simple data carrier, which is exactly what a status payload is. The Description field is optional, so consumers must be prepared to handle null. The shape is designed to be serialized to JSON for API responses and easily deconstructed when mapping to other domain models.
Because this is a `record`, it uses value-based equality and immutable properties, making it ideal as a DTO boundary between internal domain models and external consumers. Construct this type from your server state when returning status information to clients, rather than leaking domain entities.
## Notes
- Nullable Description means clients must handle nulls.
- RegistrationMode defaults to "open" when not supplied, preserving backward compatibility.
- As a record, two instances with identical property values compare equal (value equality).
- `Description` is nullable (`string?`). Guard against null or provide a fallback when presenting it to callers.
- To derive a modified copy (e.g., update `OnlineUsers`), use the `with` expression since `ServerStatusDto` is immutable.
---
@@ -8,11 +8,14 @@ public class Attachment
```
Represents a file attached to a message, such as an image, audio, or document. A message may carry zero or more attachments alongside its text content (Discord-style).
Represents a file attached to a message (such as an image, audio, or any file), enabling a message to carry zero or more attachments alongside its text content. The `Attachment` entity associates a downloadable resource with its parent [`Message`](Message.cs.md) via `MessageId` and, optionally, [`Message`](Message.cs.md), while storing the attachment's URL (`Url`), filename (`FileName`), size (`FileSize`), type (`Kind`), and an optional ASCII preview (`AsciiPreview`).
## Remarks
Decouples attachment data from the message to allow independent storage and retrieval while keeping a lightweight reference to the owning message. The Url provides the relative download path (for example, /api/files/{fileId}) and FileName preserves the original filename. FileSize stores the stored blob size in bytes, which corresponds to ciphertext size when database encryption is enabled. AsciiPreview offers a rendered ASCII-art preview for images in color-tag format and is null for non-image attachments; it is stored encrypted-at-rest and, in end-to-end encrypted channels, remains room-encrypted.
Attachments decouple media from the textual content of a message, allowing the system to manage downloads, permissions, and encryption independently from the message body. The `AsciiPreview` provides a lightweight visual cue for image attachments, and its presence is influenced by how media is encrypted at rest or within channel scopes. The [`AttachmentKind`](AttachmentKind.cs.md) helps callers distinguish among images, audio, and other file types to apply appropriate handling.
## Notes
- AsciiPreview is only populated for image attachments; for other kinds of attachments it is null.
- The Message navigation property may be null if the related Message entity isn't loaded; use MessageId for persistence and rely on Message when the relationship is loaded.
- The `Url` is a relative download path (for example, `/api/files/{fileId}`); clients should prefix it with the API base URL when constructing a full link.
- The `AsciiPreview` is null for non-image attachments and is stored encrypted at rest in encrypted channels.
- The `FileSize` is the number of bytes stored for the attachment and may reflect ciphertext size when encryption is enabled.
@@ -13,27 +13,7 @@ public enum AttachmentKind
```
AttachmentKind enumerates the possible types of a message attachment and signals how the client should render it. Use this enum when you know the specific attachment kind (image, audio, or file) so the UI can render an ASCII preview, a playback control, or a download option instead of a generic attachment rendering.
AttachmentKind is an enum that encodes how a message attachment should be rendered in the client. It enables rendering logic to pick the appropriate UI: for `Image` attachments, an ASCII image preview is shown; for `Audio`, a play control is exposed; and for `File`, a download line is presented. Use this enum when you need to branch rendering behavior based on the attachment's kind, instead of scattering rendering decisions across the codebase.
## Remarks
This enum centralizes the presentation logic for attachments and serves as a simple discriminator that decouples the attachment data from its rendering. By representing the modality with a single value, components can switch on kind to choose the appropriate UI affordance without inspecting the content payload. It helps maintain a clean separation between the data model (what the attachment is) and the presentation (how it should be shown).
## Example
```csharp
AttachmentKind kind = AttachmentKind.Image;
switch (kind)
{
case AttachmentKind.Image:
Console.WriteLine("Render as ASCII image preview");
break;
case AttachmentKind.Audio:
Console.WriteLine("Render with audio controls");
break;
case AttachmentKind.File:
Console.WriteLine("Render as downloadable file");
break;
}
```
## Notes
- If the enum is extended in the future, ensure all switch expressions include a default/fallback to handle unknown values gracefully.
This enum centralizes how attachments are presented, decoupling the attachment data from UI rendering code. It helps the rendering layer evolve independently (e.g., swapping ASCII previews or adding new affordances) without changing attachment structures.
@@ -8,4 +8,4 @@ public class Channel
```
Represents a chat channel (room) within EchoHub's domain model. It stores the channel's identity, metadata for access control, an optional topic, and the collection of messages that belong to the channel, as well as an encryption envelope used for end-to-end security. Use this type to model a distinct conversation space that can be public or restricted, with the possibility of system-managed channels that are auto-created and not user-initiated. The class ties together the channel's identity (Id, Name), its description (Topic), its visibility (IsPublic) and authentication data (PasswordHash), its system-channel semantics (IsSystem), its client-managed encryption data (EncryptionSalt, WrappedRoomKey), creation auditing (CreatedAt, CreatedByUserId), and the message history (Messages).
Channel models a chat channel within EchoHub's chat surface. It exposes an identifier `Id` (`Guid`), a required `Name` (`string`), an optional `Topic` (`string?`), and a flag `IsPublic` (`bool`) that defaults to `true`. The model also supports server-managed channels via `IsSystem` (`bool`), which are auto-created and read-only for all roles; users cannot create them. When a channel is password-protected, `PasswordHash` (`string?`) stores the hashed password. For end-to-end encryption, the envelope is represented by `EncryptionSalt` (`string?`) and `WrappedRoomKey` (`string?`), both client-generated so that the server never has access to the room content. Creation metadata is captured by `CreatedAt` (`DateTimeOffset`) and `CreatedByUserId` (`Guid`). The `Messages` collection (`List&lt;Message&gt;`) contains the related [`Message`](Message.cs.md) entities that belong to this channel.
@@ -8,21 +8,11 @@ public class ChannelMembership
```
ChannelMembership is a lightweight data container that models the association between a user and a channel, recording when the user joined. It is intended for persistence and transport of membership data; instantiate and persist this model when recording channel participation rather than scattering ad-hoc data structures.
The `ChannelMembership` class is a simple data container that models the association between a user and a channel within the EchoHub system. It stores the `UserId`, the `ChannelId`, and the time the membership was created (`JoinedAt`), which defaults to the current UTC time if not specified.
## Remarks
ChannelMembership encapsulates the many-to-many relationship between users and channels along with a join timestamp, enabling straightforward CRUD operations, serialization, and display of membership data. As a plain DTO, it contains no behavior beyond storage of UserId, ChannelId, and JoinedAt; it complements User and Channel entities by representing their linkage. The JoinedAt default is DateTimeOffset.UtcNow at construction, which is convenient for new memberships but should be overridden or preserved from storage when loading existing records.
## Example
```csharp
var membership = new ChannelMembership
{
UserId = Guid.NewGuid(),
ChannelId = Guid.NewGuid()
// JoinedAt defaults to DateTimeOffset.UtcNow
};
```
Locates a specific user's membership in a channel and records when it happened. It serves as a lightweight linkage between `UserId` and `ChannelId`, with `JoinedAt` providing a timestamp of when the membership was established.
## Notes
- The default JoinedAt value applies only to newly created instances; deserialization from a data store will populate JoinedAt from the stored value.
- This class is a plain data holder with no validation or invariants; enforce domain rules at a higher layer when necessary.
- `JoinedAt` defaults to `DateTimeOffset.UtcNow` at object creation; when loading from a data store this value may be overridden by stored data, so rely on the persisted timestamp in that case.
- There are no invariants enforced here; enforce uniqueness and referential constraints at the database or repository layer.
@@ -8,29 +8,4 @@ public class InviteCode
```
Represents a registration invitation code used to gate account creation when the server's registration mode is set to invite. An InviteCode captures the unique identifier, the actual code string, who created it, and when it was created, plus optional expiration and per-invite usage constraints. When a new REST or IRC account is created and the system is configured for invite-based registration, the incoming code must match an existing InviteCode that has not expired and that has remaining uses.
## Remarks
InviteCode acts as a persistence-side contract for invitation-based onboarding. It separates the concerns of registration gating from user data and provides a straightforward way to enforce expiration and single-use or limited-use policies at the data layer. The server's registration flow should consult these properties to validate a code before creating a new account and to record each use via UseCount, potentially preventing additional uses after MaxUses is reached.
## Example
```csharp
// Example usage: initialize a new invite code that will expire in 7 days and allow up to 5 uses
Guid adminUserId = Guid.NewGuid();
var invite = new InviteCode
{
Id = Guid.NewGuid(),
Code = "INVITE-2026-ACME",
CreatedByUserId = adminUserId,
CreatedByUsername = "admin",
CreatedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7),
MaxUses = 5,
UseCount = 0
};
```
## Notes
- Use of 'required' Code property ensures that a code value is provided when constructing instances; compile-time enforcement.
- ExpiresAt null means never expires; If ExpiresAt is not set, the code is perpetual.
- The class does not implement persistence or concurrency control; UseCount and MaxUses must be enforced by the application or data layer.
A data model representing a single registration invitation. When the server is configured with `Server:Registration = "invite"`, new accounts (REST and IRC alike) must present a valid, unexpired, and not-fully-used code to register. The `InviteCode` tracks the invitation's identity and policy: the persistent identifier `Id`, the required invitation value `Code` (marked `required` in the model), who created it (`CreatedByUserId` and `CreatedByUsername`), and when it was created (`CreatedAt`). The invitation may expire via `ExpiresAt` (null meaning it never expires), and its usage is controlled by `MaxUses` with current usage stored in `UseCount`. By default, a new invite is single-use (`MaxUses` = 1) and `CreatedAt` is initialized to the current UTC moment. This class is intended to be stored and consulted by the registration workflow to enforce invite-based onboarding.
@@ -8,7 +8,36 @@ public class Message
```
Message is the persistence model for a chat message in EchoHub, capturing who sent it, when, where, and what was said. Content is required text (which may be empty if the message carries only attachments), with an optional EmbedJson and a list of Attachments for attached files; SenderUserId/SenderUsername identify the author and ChannelId/Channel locate the conversation. Messages may reply to another message via ReplyToMessageId. It also includes legacy pre-attachments fields (Type, AttachmentUrl, AttachmentFileName, AttachmentFileSize) retained to support a one-time startup migration that folds old single-attachment messages into Attachments; new code never writes these and they are nulled after migration and not exposed in DTOs.
Represents a single message in a channel, encapsulating the text payload, sender identity, timestamp, and any attachments. It serves as the core record for conversations and is designed to be persisted by the data layer and consumed by the UI to render threads and channel histories. The message may carry rich content via `EmbedJson` and can reference a previous message through `ReplyToMessageId` to model simple threading. The `Content` property is required, yet a message may legitimately have empty content if it carries attachments.
## Remarks
Architecturally, Message acts as the persistence model for chat messages, combining the modern Attachments collection with legacy fields retained to support a one-time startup data migration. New code never writes the legacy fields; they are nulled after migration and are not exposed in DTOs.
Message is the domain aggregate for a chat entry, linking to its [`Channel`](Channel.cs.md) via `ChannelId`/[`Channel`](Channel.cs.md) and to its sender via `SenderUserId`/`SenderUsername`. Attachments are modeled as a separate collection (`Attachments`), enabling a clean separation between textual payloads and media. Legacy fields (`Type`, `AttachmentUrl`, `AttachmentFileName`, `AttachmentFileSize`) exist solely to support a one-time startup data migration into the new attachments model; new writes should use the `Attachments` collection, and these legacy fields are not exposed in DTOs and are nulled after migration. The `ReplyToMessageId` enables basic threading by pointing to the message this one replies to, if any; downstream logic should gracefully handle references to messages that may have been deleted.
## Example
```csharp
var message = new Message
{
Id = Guid.NewGuid(),
Content = "Welcome to the channel!",
SenderUserId = Guid.NewGuid(),
SenderUsername = "system",
ChannelId = Guid.NewGuid(),
Attachments = new List<Attachment>
{
new Attachment
{
Id = Guid.NewGuid(),
MessageId = Guid.Empty,
Url = "https://example.com/file.png",
FileName = "file.png",
FileSize = 4096
}
},
SentAt = DateTimeOffset.UtcNow
};
```
## Notes
- Legacy fields are for migration only; do not rely on them for new code.
- `SentAt` defaults to `DateTimeOffset.UtcNow` on instantiation; override if you have a specific send time.
- Use `EmbedJson` for optional rich content, and handle its absence gracefully in the UI.
@@ -14,11 +14,11 @@ public enum MessageType
```
Represents the category of a message in EchoHub. MessageType defines the four concrete payload kinds that a message can carry: Text, Image, File, or Audio. Use this enum whenever a component, data model, or API needs to convey which kind of content is attached to a message so consumers can handle, display, or validate it in a type-safe way instead of relying on strings or magic numbers.
Represents the category of a message payload within the model, enabling code to distinguish between textual content, images, files, and audio. Use `MessageType` to drive type-specific logic (rendering, validation, or serialization) by switching on the enum values rather than inspecting the payload directly.
## Remarks
Centralizes classification: this enum provides a single source of truth for message content kinds, enabling consistent routing, rendering, and validation across the system. It helps collaborators—models, serializers, and UI layers—make decisions based on content type without duplicating logic for string constants. By using an enum, you get compile-time checks and clearer intent.
By centralizing the variety of message payloads behind a single discriminator, `MessageType` makes it easier to extend support for new kinds. Renderers, validators, and serializers can rely on this enum to route behavior without peeking into payload internals, promoting cleaner separation of concerns.
## Notes
- When stored or transferred, the underlying value defaults to int (0-3) in the order shown; changing the sequence or renaming members may break persisted data.
- If external systems expect string representations, consider mapping to/from MessageType names to avoid breaking compatibility.
- When adding a new member to `MessageType`, update all switch expressions that handle the enum to avoid unhandled values at runtime. Prefer exhaustiveness to catch omissions at compile time.
- Do not repurpose existing values; if the meaning changes, introduce a new member to preserve backward compatibility and serialization stability.
@@ -8,7 +8,27 @@ public class RefreshToken
```
RefreshToken is a persistence model that represents a refresh token tied to a user. It stores a hashed token (TokenHash), the associated user via UserId, and validity information such as ExpiresAt and CreatedAt (which defaults to the current UTC time), plus an optional RevokedAt timestamp. It exposes IsExpired, IsRevoked, and IsActive to quickly assess the tokens state. A developer would create and persist these tokens when issuing refresh tokens in an authentication flow, check IsActive (or IsExpired/IsRevoked) when validating a refresh attempt, and use RevokedAt to mark a token as revoked.
Represents a `RefreshToken` that carries the metadata and state needed to sustain a user session via token-based authentication. It encapsulates the token hash, the owning user, expiration, and revocation data, and exposes simple predicates to answer the token's current validity. The `TokenHash` is marked `required`, guaranteeing a hash is provided during initialization; `CreatedAt` records when the token was created (defaulting to `DateTimeOffset.UtcNow`); `ExpiresAt` defines when the token becomes invalid; `RevokedAt` records a revocation timestamp when the token is revoked. The computed properties `IsExpired`, `IsRevoked`, and `IsActive` reflect the token's lifecycle status, so callers can check validity without inspecting each field. The [`User`](User.cs.md) navigation property links the token to its owner for convenient data access in domain services or ORMs.
## Dependencies
- `DateTimeOffset`
## Remarks
This class serves as a persistence-facing token entity with a foreign key to User and a corresponding navigation property, enabling lifecycle management (creation, expiry, revocation) at the data layer while providing simple state checks for business logic.
Architecturally, this symbol serves as the boundary for token-based authentication in the domain. It centralizes lifecycle logic (expiry and revocation) into a single place, enabling consistent checks via `IsActive` across services. The presence of [`User`](User.cs.md) further supports straightforward navigation to the owner, which is helpful when presenting token data in dashboards or auditing scenarios.
## Example
```csharp
// Example: creating a new `RefreshToken` (TokenHash is required)
Guid userId = Guid.NewGuid();
var token = new RefreshToken
{
Id = Guid.NewGuid(),
TokenHash = "sha256-abc123",
UserId = userId,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
```
## Notes
- Be aware that `CreatedAt` is initialized to the current UTC time at construction. If you load an existing token from storage, ensure the stored value for `CreatedAt` is preserved.
- `IsActive` depends on both `IsExpired` and `IsRevoked`. If you set `RevokedAt` but forget to update `IsRevoked`, the token might appear active.
@@ -14,31 +14,7 @@ public enum ServerRole
```
Represents the role assigned to a member within a server context in EchoHub. It defines four distinct levels of authority: Member, Mod (moderator), Admin, and Owner. Use this enum whenever you need to distinguish capabilities, gate UI or actions, or persist role information instead of relying on magic numbers.
Represents the role a user holds within a server in EchoHub. It categorizes users into distinct permission tiers: `Member`, `Mod`, `Admin`, and `Owner`, which are used to drive authorization and feature availability without scattering numeric checks throughout the codebase.
## Remarks
By centralizing roles in a single enum, the codebase can map each role to its corresponding permissions in one place, enabling consistent authorization checks across services. The explicit integer values also support stable serialization and interop when persisting or transmitting role data, without forcing string-based representations.
## Example
```csharp
var role = ServerRole.Admin;
switch (role)
{
case ServerRole.Owner:
case ServerRole.Admin:
// elevated permissions
break;
case ServerRole.Mod:
// moderation tasks
break;
case ServerRole.Member:
// regular user actions
break;
}
Console.WriteLine($"User role: {role}"); // prints Owner, Admin, Mod, or Member
```
## Notes
- Do not treat ServerRole as a Flags enum; do not combine roles with bitwise operators.
- Prefer using the named constants in checks; avoid relying on numeric ordering for access decisions.
- Changing the underlying values (03) can affect serialized data; coordinate evolution across all consumers to preserve compatibility.
This enum provides a stable abstraction for role-based access control, allowing components to reason about capabilities (moderation, configuration, ownership) by comparing against `ServerRole` values. Centralizing roles reduces duplication of permission logic and helps ensure consistent authorization across command handlers, UI components, and services. It also offers an extension point: adding a new role or reordering the hierarchy can be localized to this enum and its consumers.
@@ -8,13 +8,12 @@ public class ServerStatsReport
```
Represents a snapshot of server activity for a single reporting window, produced periodically by the stats-report background job. It captures timing data (PeriodStart, PeriodEnd, WindowHours, GeneratedAt) and per-window metrics (MessagesSent, FilesUploaded, BytesUploaded, NewMembers, ActiveMembers, Connections, Disconnections, Kicks, Bans) as well as end-of-window totals (TotalMembers, OnlineNow, PeakOnline) for persistence as pretty-printed JSON.
ServerStatsReport is a snapshot of server activity for a single reporting window, produced periodically by the `stats-report` background job. It records when the report was generated, the start and end of the window, the window length in hours, and a set of per-window activity counters (`MessagesSent`, `FilesUploaded`, `BytesUploaded`, `NewMembers`, `ActiveMembers`, `Connections`, `Disconnections`, `Kicks`, `Bans`) as well as end-of-window totals (`TotalMembers`, `OnlineNow`, `PeakOnline`); the report is serialized as pretty-printed JSON and persisted for historical trend analysis. The window is defined as "since the previous report" (or since startup for the first report).
## Remarks
Serves as a stable, serializable container for periodic server activity, enabling dashboards and trend analyses to compare windows over time. By separating window semantics (start/end, duration) from generation time, it supports reliable aggregation and rhythm-based alerts when metrics diverge.
ServerStatsReport serves as the canonical persisted unit for time-bounded server activity, decoupling the reporting job from storage and analytics. It combines both within-window activity and end-of-window aggregates to support dashboards, trend charts, and anomaly detection across multiple windows. As a plain data container, it is populated by the reporting process and then written to the data store; its structure is stable to ensure reliable longitudinal comparisons.
## Notes
- GeneratedAt is intended to equal PeriodEnd; ensure synchronization when populating the model. The default initializer uses DateTimeOffset.UtcNow, which may diverge if PeriodEnd is set to a different value.
## Dependencies
- DateTimeOffset (System) — used for all timestamp properties on the model.
- GeneratedAt is intended to reflect the moment the window ended; ensure GeneratedAt is kept in sync with PeriodEnd to avoid confusion (GeneratedAt should effectively equal PeriodEnd when the report is produced).
- PeriodEnd should be greater than or equal to PeriodStart; WindowHours should be non-negative.
- BytesUploaded uses a 64-bit signed integer; extremely large attachment activity should still stay within `BytesUploaded`'s range to avoid overflow.
@@ -8,14 +8,25 @@ public class User
```
The User class is a domain model that represents a person using EchoHub, encapsulating identity (Id, Username, PasswordHash), profile details (DisplayName, Bio, NicknameColor, AvatarAscii), presence (Status, StatusMessage), role-based access (Role), moderation flags (IsMuted, MutedUntil, IsBanned), and auditing timestamps (CreatedAt, LastSeenAt). Username and PasswordHash are required to create a usable user, while other fields are optional to support rich profiles; defaults establish an online, member-facing user with current timestamps when a new instance is created.
Represents a user entity in the EchoHub domain, aggregating identity, profile, presence, and lifecycle data. It is the primary model used when creating, retrieving, and persisting user information, with required credentials enforced at construction via the `required` modifiers on `Username` and `PasswordHash`.
## Remarks
Designed to be a single source of truth for user state, it coordinates authentication, authorization via `Role`, and moderation flags such as `IsMuted` and `IsBanned`. The default values — `Status` set to `UserStatus.Online`, `Role` set to `ServerRole.Member`, and timestamps on creation — provide sensible startup behavior while keeping optional fields available for richer profiles. It serves as the canonical user payload across core services and data stores, reducing duplication and drift between layers.
This class serves as a central data container used across authentication, user management, presence rendering, and authorization checks. Its designed to be lightweight and serializable for persistence, while keeping domain concerns cohesive with a single user entity. The defaults for Status and Role, along with the auditing timestamps, provide a sensible initial state for newly created users.
## Example
```csharp
var user = new User
{
Id = Guid.NewGuid(),
Username = "jdoe",
PasswordHash = "pbkdf2$...",
DisplayName = "Jane Doe",
Status = UserStatus.Online,
Role = ServerRole.Member
};
```
## Notes
- The required fields (Username and PasswordHash) enforce that essential credentials are provided when constructing a user instance.
- PasswordHash should be treated as sensitive data; avoid exposing it in logs or API responses and ensure the persistence layer handles security appropriately.
- If hydrating from storage, ensure CreatedAt and LastSeenAt reflect the persisted values rather than new defaults.
- The `required` modifier on `Username` and `PasswordHash` enforces initialization when constructing a `User` via object initializers (compile-time check).
- `CreatedAt` and `LastSeenAt` default to the moment of object creation but may be replaced by deserialized data from storage.
- `MutedUntil` is meaningful only when `IsMuted` is true; it can be null if not muted.
@@ -14,4 +14,10 @@ public enum UserStatus
```
Represents the current presence state of a user in EchoHub, used by UI presence indicators and presence logic throughout the app. Use Online when the user is connected and active, Away when the user is idle, DoNotDisturb to signal notifications should be minimized, and Invisible when the user should not appear online to others.
Represents a user's presence status within the application, guiding UI rendering, presence-based filtering, and notification behavior. The enum exposes four discrete states: `Online`, `Away`, `DoNotDisturb`, and `Invisible` to express typical availability scenarios.
## Remarks
Centralizing presence into `UserStatus` prevents scattered string values or boolean flags across the codebase, promoting consistent semantics for how users are shown and how presence-related logic runs. It also future-proofs the API by allowing new statuses to be added without changing call-sites that consume the type. This enum typically intersects with UI components that render status indicators and with services that filter or route behavior based on a user's current state.
## Notes
- Changing the set of statuses (adding/removing/reordering enum members) is a breaking change that can affect serialization, persistence, and cross-boundary API compatibility; prefer backward-compatible extensions by adding new members rather than reordering existing ones.
@@ -8,31 +8,27 @@ public static class RoomCrypto
```
Client-side envelope encryption primitives used for end-to-end encrypted channels: derive per-room keys from a passphrase, generate random room content keys (RCKs), and encrypt/decrypt room content using AES-GCM. Use this class when you need a canonical, interoperable way to create room key material, wrap/unlock a room key with a passphrase-derived key, and produce/recognize the wire format used on the server ($RC1$base64(nonce||tag||ciphertext)).
Client-side utilities for envelope encryption used by private (end-to-end encrypted) channels. Use `RoomCrypto` when you need a simple, opinionated way to derive keys from a passphrase, generate a random room content key (RCK), and encrypt/decrypt room content in the wire format this project uses (a `$RC1$`-prefixed base64 blob for text and a nonce||tag||ciphertext blob for raw bytes).
## Remarks
This class encapsulates the protocol choices and low-level crypto work so callers don't compose PBKDF2, hex encoding, and AES-GCM themselves. It implements an envelope pattern: the client generates a random 256-bit room content key (RCK) to encrypt room data; the RCK is stored server-side wrapped (AES-GCM) with a key-encryption key (KEK) derived from the user's passphrase. PBKDF2-SHA256 with 210000 iterations produces 64 bytes: the first 32 bytes (returned as lowercase hex) are the auth key used as the join gate, and the final 32 bytes are the KEK (never sent). Re-wrapping the RCK on passphrase change avoids re-encrypting history.
`RoomCrypto` implements the client-side half of an envelope-encryption scheme: the client generates a random 256-bit room content key (RCK) that actually encrypts all channel content, and a key-encryption key (KEK) derived from the user's passphrase is used to wrap the RCK before the wrapped RCK is stored on the server. The derivation uses PBKDF2-SHA256 with `Pbkdf2Iterations` (210000) and a `SaltSizeBytes` (16) salt; the resulting 64 bytes are split so the first `KeySizeBytes` (32) bytes are exported as a lowercase hex `AuthKeyHex` (the join credential) and the last `KeySizeBytes` bytes are kept as the `KeyEncryptionKey`. `RoomCrypto` keeps a small, explicit surface: `GenerateSalt`, `GenerateRoomKey`, `DeriveKeys`, `EncryptText`, `TryDecryptText`, `IsRoomCiphertext`, and byte-level `EncryptBytes`/`DecryptBytes` (used internally). The text wire format is the literal `CiphertextPrefix` (`"$RC1$"`) followed by `Convert.ToBase64String(nonce||tag||ciphertext)`; binary APIs return/expect the raw `nonce||tag||ciphertext` blob. The implementation zeroes the slice of derived bytes used for the auth key after converting to hex to reduce exposure of sensitive material.
## Example
```csharp
// Typical client flow:
// 1) Create room: generate salt and room key, derive keys from passphrase, wrap RCK and send auth key + wrapped blob to server.
// Typical client flow: derive keys from a passphrase, create a room key, encrypt and decrypt text.
var salt = RoomCrypto.GenerateSalt();
var roomKey = RoomCrypto.GenerateRoomKey();
var derived = RoomCrypto.DeriveKeys("correct horse battery staple", salt);
// derived.AuthKeyHex is sent to server as the join credential
// derived.KeyEncryptionKey (KEK) is used locally to wrap roomKey with AES-GCM (use EncryptBytes/EncryptText as appropriate)
// `derived.AuthKeyHex` is sent to the server as the join credential; `derived.KeyEncryptionKey` stays local.
var roomKey = RoomCrypto.GenerateRoomKey();
// 2) Encrypt/decrypt room content with the room key
var plaintext = "hello room";
var ct = RoomCrypto.EncryptText(plaintext, roomKey);
if (RoomCrypto.IsRoomCiphertext(ct) && RoomCrypto.TryDecryptText(ct, roomKey, out var recovered))
var ciphertext = RoomCrypto.EncryptText("hello room", roomKey);
if (RoomCrypto.TryDecryptText(ciphertext, roomKey, out var plaintext))
{
// recovered == "hello room"
// plaintext == "hello room"
}
```
## Notes
- PBKDF2 parameters are fixed: 16-byte salt, 210000 iterations, 64-byte output; the auth key is returned as lowercase hex and the KEK as raw bytes.
- AES-GCM parameters are fixed: 12-byte nonce, 16-byte tag, 32-byte key (AES-256). Text wire format is the literal prefix "$RC1$" then base64(nonce||tag||ciphertext).
- TryDecryptText returns false for non-room ciphertext or when decryption/authentication fails (malformed base64, wrong key, or tampering). Protect KEK and RCK in memory and avoid persisting raw keys.
- `RoomCrypto` expects a `KeySizeBytes`-length key (32 bytes) for its AES-GCM operations; supplying a key of the wrong length will fail when constructing the cipher.
- Nonces are randomly generated per-encryption (`NonceSizeBytes` = 12). Do not reuse a `roomKey`/nonce pair for different plaintexts; the implementation already generates random nonces, so avoid reusing the same nonce manually.
- `TryDecryptText` returns `false` (and sets `plaintext` to empty) both for non-room content (missing the `CiphertextPrefix`) and for any integrity/format errors (bad base64, authentication failure).
@@ -8,18 +8,17 @@ public static class AsciiBannerService
```
Renders input text as a 5-row block-character banner (the /banner command). It uses a self-contained, hand-authored font defined in code, with no dependencies or network access, producing plain text content that can be transmitted like any other message; the renderer trims input to the maximum length and skips characters not defined in the font.
AsciiBannerService renders a string as a five-row block-character banner using a hand-authored, figlet-style font defined entirely in code. It is self-contained — no dependencies and no network access — and returns plain text suitable for transport or encryption just like any other message. Use `Render` when you need a compact, dependency-free banner for logs, UI previews, or console-like output.
## Remarks
This symbol provides a deterministic, dependency-free banner renderer that can be used anywhere a compact ASCII-art label is desirable. The font is embedded in code as a glyph dictionary, so rendering is purely local and consistent across environments. Input is uppercased to match the glyph keys, glyphs are joined per row with a single space, and ink is rendered by replacing the '#' glyphs with the block character '█' and '.' with spaces; trailing spaces on each line are trimmed to minimize payload.
AsciiBannerService provides a centralized, self-contained banner rendering capability that does not rely on external resources. The glyphs are embedded in a private `Font` dictionary, ensuring deterministic rendering across environments. The banner width is bounded by `MaxInputLength` (20 characters) and the height is fixed to `Rows` (5), which keeps banners predictable in size and performance. Input is normalized by converting to uppercase with `ToUpperInvariant()`, and only characters present in `Font` are rendered; unsupported characters are skipped. The final output is assembled with a `StringBuilder`, joining glyph rows horizontally with spaces and replacing `#` (ink) with the solid block character `█` and `.` (blank) with spaces. Trailing spaces on each line are trimmed to minimize payload.
## Example
```csharp
string? banner = AsciiBannerService.Render("EchoHub");
if (banner != null)
Console.WriteLine(banner);
var banner = AsciiBannerService.Render("TEST");
```
## Notes
- Non-renderable input (no supported characters) yields null; callers should handle null results to avoid printing empty banners.
- The method trims whitespace and enforces a maximum length of 20 characters; longer input is truncated before rendering.
- Returns `null` when the input is empty, whitespace, or contains no renderable characters.
- Non-renderable characters are skipped; only characters present in `Font` contribute to the banner.
- The input is capped at `MaxInputLength` characters, and the output always consists of exactly `Rows` lines if renderable content exists.
@@ -8,12 +8,23 @@ public static class FileValidationHelper
```
FileValidationHelper centralizes lightweight, stream-based validation for common image formats and audio file names. Its IsValidImage(Stream) method reads the stream header (without changing the stream's position) and recognizes JPEG, PNG, GIF, and WebP by their magic numbers, returning true for known formats and false otherwise. IsAudioFile(string) validates a file names extension against a predefined set of audio extensions in a case-insensitive manner. Together, these helpers let callers pre-filter content before attempting to decode or process media data.
FileValidationHelper is a compact utility that centralizes quick, non-destructive checks for media file types. It exposes `IsValidImage(Stream)` to determine if the provided stream represents a known image format by peeking at its header bytes, while always restoring the stream's original position. It also exposes `IsAudioFile(string)` to decide whether a file name uses one of the recognized audio extensions. Use these helpers to validate inputs in upload or ingestion paths without loading or parsing full files, and to keep format-detection logic consistent across the codebase.
## Remarks
This symbol provides a single, testable utility to detect supported media formats without pulling in a full decoder. By encapsulating the magic-number checks and the extension-based guard, it reduces duplication and concentrates format-coverage decisions in one place. It favors a fast, low-allocation validation path and leaves actual parsing to dedicated components.
By coalescing the magic-byte checks in one place, this abstraction reduces duplication and the risk of inconsistent format handling across components that ingest media. The detection rules cover JPEG, PNG, GIF, and WebP at the header level, with WebP requiring a RIFF header followed by the WebP tag; the private helper `StartsWith` encapsulates the prefix comparison to keep `IsValidImage` focused on intent. The `AudioExtensions` set drives a fast, case-insensitive extension lookup for `IsAudioFile` without touching disk data.
## Example
```csharp
using System.IO;
byte[] header = new byte[] { 0xFF, 0xD8, 0xFF };
using var ms = new MemoryStream(header);
bool isImage = FileValidationHelper.IsValidImage(ms);
bool isAudio = FileValidationHelper.IsAudioFile("song.MP3");
```
## Notes
- Non-seekable streams cause IsValidImage to return false (the check stream.CanSeek is performed up-front).
- IsAudioFile relies solely on the file extension and does not inspect file contents.
- WebP detection requires a RIFF header followed by a WEBP tag at the expected offsets; malformed headers degrade gracefully to false.
- The stream passed to `IsValidImage` must be seekable; non-seekable streams will not have their position reset and may lead to false results.
- `IsAudioFile` performs a purely extension-based check and does not inspect file contents.
- The image-detection logic recognizes specific headers (JPEG, PNG, GIF, WebP) and is not a full format validator; for strict validation, perform content analysis beyond these checks.
@@ -8,21 +8,21 @@ public class ImageToAsciiService
```
ImageToAsciiService is a lightweight utility that converts an input image stream into color-aware ASCII art by packing two vertical pixels into a single character cell using half-block characters and per-cell color tags. Use GetDimensions to pick a target resolution and ConvertToAscii when you need a textual, ASCII-only representation of an image for logs, chat, or environments without graphical support.
ImageToAsciiService converts an image stream into ASCII art using two vertical pixels per character and half-block characters. The static `GetDimensions` translates a size code (`'s'`, `'m'`, `'l'`) into ASCII art dimensions (40x40, 80x80, 120x120 respectively) and returns the default dimensions from `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` for other codes. The instance method `ConvertToAscii` accepts a `Stream` containing an image and returns a string composed of color tokens and block characters. Each character cell encodes two vertical pixels; a foreground color token `{F:RRGGBB}` and a background color token `{B:RRGGBB}` are emitted when colors change, followed by a block character (either `█` or `▀`), with `{X}` used to reset coloring. The output uses only printable ASCII and avoids terminal escape sequences.
## Remarks
The class embodies a small, focused translation between raster images and ASCII art. It emits inline color tokens only when the color changes, preserving color fidelity while keeping the output readable in plain-text environments. The two-pixel vertical mapping (top pixel as the foreground color, bottom pixel as the background) enables higher-density representation than single-character ASCII, while remaining printable and parseable by consumers that understand the {F:...}{B:...}{X} tags. An even-height safeguard ensures the processing loop always handles complete pixel pairs, resizing the image as needed to maintain consistent output.
`ImageToAsciiService` encapsulates the image-to-ASCII rendering logic, separating it from image loading and presentation concerns. It centralizes the color-token encoding and block-character strategy so callers can produce text-based previews in environments that cannot render images. By relying on [`HubConstants`](../Constants/HubConstants.cs.md) for defaults, global rendering preferences propagate naturally to this converter.
## Example
```csharp
using System.IO;
var stream = File.OpenRead("path/to/image.png");
using var fs = File.OpenRead("path/to/image.png");
var service = new ImageToAsciiService();
string ascii = service.ConvertToAscii(stream, 80, 40);
Console.WriteLine(ascii);
string ascii = service.ConvertToAscii(fs);
```
## Notes
- The ASCII output relies on the presence of the {F:RRGGBB}{B:RRGGBB}{X} tags and the block characters; ensure your rendering environment understands these tokens, otherwise you will see literal tags.
- If a height is provided as an odd number, the implementation advances to an even height, which may slightly alter the aspect ratio of the produced art.
- The converter emits color-change tokens only when the foreground or background color differs from the previous pixel pair, which keeps the output compact for large areas of uniform color.
- Each ASCII cell represents two vertical pixels; the image is resized to the requested `width` and `height` (defaulting to `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` if not specified). This can alter aspect ratio, so choose dimensions with that in mind.
- The format relies on the tokenized color syntax (e.g. `{F:RRGGBB}` and `{B:RRGGBB}`) being understood by the consumer; renderers that ignore these tokens will display plain block characters without color.</
@@ -8,12 +8,24 @@ public class IrcBroadcaster : IChatBroadcaster
```
Broadcasts chat events to IRC clients by translating application-level messages and room/user events into IRC protocol lines and sending them via the IrcGatewayService. Decrypts transport-layer-encrypted message content so IRC clients (which do not support the application's app-layer encryption) receive readable text; end-to-end room ciphertext markers (e.g. $RC1$) are preserved. Use this class when you need to mirror server chat rooms and user lifecycle events to connected IRC clients.
Bridges application chat events into IRC wire-protocol messages and sends them to connected IRC clients. Use `IrcBroadcaster` when you need chat activity (messages, joins/parts, kicks/bans, topic changes) reflected on an IRC gateway so traditional IRC clients see the room as an IRC channel; prefer using a plain chat broadcaster when you do not need IRC-formatted output.
## Remarks
IrcBroadcaster is the IRC-specific implementation of the IChatBroadcaster contract and acts as the bridge between the chat model and the IRC wire format. It relies on IMessageEncryptionService to remove transport-layer encryption for IRC consumers and on IrcMessageFormatter to produce IRC-compliant lines (including splitting long messages and formatting reply prefixes/embeds). Messages are dispatched by enumerating connections returned from the gateway and sending formatted lines to each connection; the broadcaster also follows IRC conventions such as avoiding echoing a message back to the originating connection.
`IrcBroadcaster` is an [`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implementation that adapts the application's chat model to IRC conventions. It pulls active connections from the [`IrcGatewayService`](IrcGatewayService.cs.md) (`GetConnectionsInChannel` / `GetAllConnections`), formats user-visible text with `IrcMessageFormatter.FormatMessage` (using the gateway `Options.PublicBaseUrl` for absolute links), and sends one or more IRC lines to each connection via `conn.SendAsync`. Because IRC clients cannot handle app-layer encryption, the broadcaster uses the injected [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) to decrypt transport-encrypted payloads before formatting; end-to-end room ciphertext markers (`$RC1$`) are left intact. The implementation also follows IRC conventions for echo suppression (it excludes the originating connection by `excludeConnectionId`) and for addressing (matching by `ConnectionId` or `Nickname` where appropriate).
## Example
```csharp
// given existing instances of IrcGatewayService and IMessageEncryptionService
var gateway = /* existing IrcGatewayService */;
var encryption = /* existing IMessageEncryptionService */;
var broadcaster = new IrcBroadcaster(gateway, encryption);
// broadcast a message to the #general channel but don't echo back to the sender
await broadcaster.SendMessageToChannelAsync("general", messageDto, excludeConnectionId: "conn-123");
```
## Notes
- Message content is decrypted before formatting; E2E room ciphertext (explicit markers like $RC1$) is left unchanged so room-encrypted messages are not exposed.
- The broadcaster avoids echoing by comparing connection IDs (excludeConnectionId) when sending normal messages. Be aware this requires callers to pass the originating connection id to suppress local echoes correctly when appropriate.
- Sends are awaited sequentially per connection/line (each connection's SendAsync is awaited in a loop). Under high fan-out this can introduce latency; consider batching or parallelization at the caller/gateway level if latency becomes an issue.
- `IrcBroadcaster` explicitly decrypts transport-layer encrypted content using the injected [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md); this is necessary because IRC clients do not support the application's app-layer encryption. Messages that contain E2E room ciphertext markers (`$RC1$`) are preserved as-is.
- Echo suppression is performed by comparing `conn.ConnectionId` to the provided `excludeConnectionId`. This avoids re-sending a message to the origin connection while still delivering it to other sessions belonging to the same user (different connections/nicknames).
- `IrcMessageFormatter.FormatMessage` may split a single logical message into multiple IRC lines; each resulting line is sent individually with `conn.SendAsync`, so large messages can result in multiple network writes.
- `SendUserStatusChangedAsync` is a no-op because IRC lacks an active presence broadcast; clients discover away/idle state via `WHO`/`WHOIS` instead.
@@ -8,15 +8,13 @@ public sealed class IrcClientConnection : IAsyncDisposable
```
Manages a single IRC client TCP connection: wraps a TcpClient/Stream pair, serializes outgoing IRC lines, provides simple registration and channel membership state, and exposes read/write helpers for the IRC protocol. Use this when handling a single connected client in server code so you get consistent CRLF framing, UTF-8 encoding without BOM, and serialized writes.
Manages a single IRC-over-TCP client connection: reads and writes CRLF-delimited lines, holds simple registration state, and tracks joined channels. Use `IrcClientConnection` when the server needs a lightweight, per-socket representation of a connected IRC client (rather than passing raw `TcpClient`/`Stream` around).
## Remarks
This type represents the per-connection state and I/O for one IRC client. It centralizes the socket-level StreamReader/StreamWriter setup (UTF-8 without BOM, CRLF line endings, AutoFlush) and enforces serialized writes with an internal SemaphoreSlim. The class keeps mutable registration and presence properties (nickname, username, authentication flags, away message, joined channels) so higher-level command handlers and broadcaster threads can consult or update a single source of truth. Channel membership access is guarded by a private lock and exposed via snapshot methods so broadcaster threads can read without additional synchronization.
`IrcClientConnection` encapsulates the I/O and minimal protocol state for one IRC client. It creates `StreamReader`/`StreamWriter` pair configured with UTF-8 without BOM (via `UTF8Encoding`) and `StreamWriter` options `AutoFlush` and `NewLine = "\r\n"` so callers can read/write logical IRC lines. Outgoing writes are serialized using the private `SemaphoreSlim` (`_writeLock`) so concurrent senders do not interleave data. Channel membership is stored in the private `_joinedChannels` and guarded by `_channelLock`, allowing safe reads by broadcaster threads while command handlers mutate the set. Connection identity is exposed through `ConnectionId` (prefixed with `HubConstants.IrcConnectionIdPrefix` and a `Guid`), and a simple `Hostmask` string is provided based on `Nickname` and `Username`.
## Notes
- ReadLineAsync swallows read exceptions and returns null; treat a null result as "connection closed" or irrecoverable read error.
- SendAsync swallows write exceptions (connection lost) after serializing via an internal semaphore; callers cannot observe write failures directly.
- Registration properties (Nickname, Username, UserId, IsRegistered, etc.) are not synchronized by this class — callers should coordinate concurrent access if needed.
- Channel membership APIs (JoinChannel, LeaveChannel, IsInChannel, GetJoinedChannels) are thread-safe: the implementation takes a lock and GetJoinedChannels returns a snapshot list to avoid callers iterating the internal set directly.
- Hostmask composes Nickname and Username; Username may be null so Hostmask uses Username ?? Nickname in its string formatting.
- DisposeAsync closes the underlying TcpClient and disposes the reader/writer and semaphore; do not use the connection after disposing.
- `ReadLineAsync` returns `null` on error or when the underlying read fails; callers must treat `null` as a disconnected/error condition rather than a valid empty line.
- `SendAsync` and the numeric helpers (`SendNumericAsync`) swallow exceptions raised while writing (connection loss is silently ignored), so sending failures will not throw — design choice to avoid bubbling socket errors to callers.
- Only channel-related state (`_joinedChannels`) is synchronized. Properties such as `Nickname`, `Username`, `IsRegistered`, `IsAuthenticated`, and `AwayMessage` are not individually thread-safe; if you access them concurrently from multiple threads, add external synchronization.
- `DisposeAsync` closes the underlying `TcpClient` and disposes the reader, writer, and `_writeLock`, but does not attempt to coordinate or await in-flight operations beyond disposing those resources.
@@ -24,6 +24,7 @@
- [HandleTopicAsync](#handletopicasync)
- [HandleUserAsync](#handleuserasync)
- [HandleWhoAsync](#handlewhoasync)
- [HandleWhoisAsync](#handlewhoisasync)
- [IrcToEchoHubChannel](#irctoechohubchannel)
- [RequireRegisteredAsync](#requireregisteredasync)
- [RunAsync](#runasync)
@@ -33,7 +34,6 @@
- [SendNamesReplyAsync](#sendnamesreplyasync)
- [SendWelcomeBurstAsync](#sendwelcomeburstasync)
- [TryCompleteRegistrationAsync](#trycompleteregistrationasync)
- [HandleWhoisAsync](#handlewhoisasync)
---
@@ -46,15 +46,15 @@ public sealed class IrcCommandHandler
```
Acts as the main dispatcher that translates incoming IRC protocol messages from a single IrcClientConnection into actions against the server-side chat, user and channel services. Reach for this class when you need the gateway that accepts an IRC client, performs registration/authentication (SASL or PASS), sends the welcome/MOTD burst, and maps channel/query commands (JOIN, PART, PRIVMSG, NAMES, TOPIC, WHO/WHOIS, LIST, etc.) into the server's chat/channel/user APIs.
Handles and dispatches IRC client commands for a single connected client and bridges them to the server-side chat services. Reach for `IrcCommandHandler` when you need an adapter that translates IRC commands (registration, authentication, channel operations, queries like `NAMES`/`WHOIS`, messaging) into calls on the backend services ([`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md)) and emits the corresponding IRC replies (welcome burst, MOTD, topic/NAMES lists, etc.).
## Remarks
This sealed handler centralizes IRC protocol handling for one client connection. It coordinates authentication (including SASL and PASS fallbacks), completes IRC registration (NICK/USER), and then routes post-registration commands to the underlying IChatService, IUserService, and IChannelService. It also enforces gateway-specific policies mentioned in the source comments: encrypted rooms are not readable over IRC (so joins are blocked), system channels are not proxied to IRC, and private channels are omitted from LIST results. Message history replay must be decrypted via IMessageEncryptionService before being sent to the IRC client.
`IrcCommandHandler` is the protocol-layer coordinator for an IRC gateway: it receives parsed [`IrcMessage`](IrcMessage.cs.md) instances from the [`IrcClientConnection`](IrcClientConnection.cs.md), interprets IRC semantics (registration flows, SASL vs PASS authentication, channel join/part, PMs, queries), and invokes the appropriate backend services and helpers ([`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) for decrypting history, `ILogger` for diagnostics). The class groups responsibilities into logical regions — authentication, welcome/MOTD, channel operations, and query commands — and exposes a long-running `RunAsync` loop driven by a `CancellationToken` to process client messages until the connection ends. Many of the private handlers (`HandleCapAsync`, `HandleAuthenticateAsync`, `HandleNickAsync`, `HandleUserAsync`, `HandleJoinAsync`, `HandlePrivmsgAsync`, `HandleNamesAsync`, `HandleTopicAsync`, `HandleWhoisAsync`, etc.) encapsulate the IRC-to-service mapping and the reply generation.
## Notes
- Encrypted rooms are intentionally blocked from JOIN over the IRC gateway: the gateway does not hold room keys and therefore cannot expose encrypted room contents to IRC clients.
- Private channels are hidden from LIST to match the SignalR client's channel visibility; expect LIST to only include public/discoverable channels.
- Registration and authentication have multiple paths (SASL, PASS, or account registration fallback); callers should expect asynchronous authentication flow and that RunAsync accepts a CancellationToken to stop processing.
- Registration/authentication ordering is important: the handler supports both SASL (`AUTHENTICATE`) and legacy `PASS` flows and contains explicit fallbacks (e.g. try to auto-register on auth failure). Callers should not assume a client is fully registered until the registration completion path in `TryCompleteRegistrationAsync` completes.
- Encrypted and system channels are treated specially: the handler deliberately prevents joining channels that cannot be safely proxied (end-to-end encrypted rooms or server-only system channels), and history replay requires decrypting messages via [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) before sending them to the IRC client.
- All public operations are async and driven by `RunAsync(CancellationToken)`: callers should respect the `CancellationToken` and be prepared for async exceptions to surface from the handlers; the class uses `ILogger` for recording failures and important state transitions.
---
@@ -86,14 +86,10 @@ public IrcCommandHandler(
| `logger` | `ILogger` | — |
The constructor initializes an IrcCommandHandler by wiring together its required collaborators: IrcClientConnection, IrcOptions, IChatService, IUserService, IChannelService, IMessageEncryptionService, and ILogger. It stores these dependencies in private fields so the command-handling logic can access the IRC connection, configuration, chat and user/channel services, encryption features, and logging throughout command processing. This pattern follows dependency injection, enabling easy testing with mocks and seamless composition by the applications DI container at startup.
Initializes a new `IrcCommandHandler` by injecting all required dependencies: [`IrcClientConnection`](IrcClientConnection.cs.md), [`IrcOptions`](IrcOptions.cs.md), [`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md), [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md), and `ILogger`. The constructor stores these in private fields so the command handler can coordinate chat, user, and channel operations, apply encryption, and log activity when processing IRC commands. This initialization pattern is typically used by the dependency injection container or in tests to assemble a fully wired, ready-to-run handler.
## Remarks
By aggregating these collaborators, the constructor centralizes the wiring of core capabilities—network I/O, configuration, domain services for chat, user and channel state, cryptographic operations, and observability—so command processing remains focused on business logic rather than setup. This design promotes testability, consistency, and clear separation of concerns within the IRC subsystem.
## Notes
- The constructor as shown does not perform null checks; ensure the DI container enforces non-null registrations or add guards in production code.
- Be mindful of lifetime management: the handler should typically share lifetimes with its collaborators or be disposed in tandem to avoid resource leaks.
The constructor serves as a wiring point that decouples `IrcCommandHandler` from concrete implementations, enabling substitution in tests and different runtime configurations. By wiring `_conn`, `_options`, `_chatService`, `_userService`, `_channelService`, `_encryption`, and `_logger`, it ensures the handler has immediate access to the resources needed to parse and route IRC commands, manage users and channels, apply encryption, and emit logs. It does not execute command logic itself; its purpose is to provide a fully initialized, ready-to-use instance for later operation.
---
@@ -106,16 +102,10 @@ private string ServerName => _options.ServerName
```
This private read-only property exposes the server name configured in the handlers options by forwarding to _options.ServerName. It should be used whenever the command handler needs the target IRC server name, offering a single indirection point if the source of that value changes in the future.
The `ServerName` property is a private, read-only accessor that forwards to `_options.ServerName` to obtain the configured IRC server name. It serves as an internal convenience within the `IrcCommandHandler` class, enabling consistent access to the server name without coupling to the `_options` object.
## Remarks
By wrapping the access in ServerName, you decouple usage from the underlying options data. This centralization makes future changes (like deriving the server name from a different config source or applying normalization) localized to this property. It also communicates that the server name is a configuration concern and not a computed field of the handler itself.
## Notes
- The property simply forwards to _options.ServerName; it does not perform validation or mutation.
- If _options.ServerName can change at runtime, callers may observe updates on subsequent accesses.
This private indirection isolates the `IrcCommandHandler` from changes to where the server name is stored. If `_options`' structure changes or the server name is sourced from elsewhere, update only this member and keep the rest of the class intact. It also clarifies intent by naming and exposing the concept of 'server name' as a single retrieval point for internal command handling.
---
@@ -136,15 +126,15 @@ private async Task HandleAuthenticateAsync(IrcMessage msg)
**Returns:** `Task`
Handles SASL authentication for a connected IRC client by processing the SASL-related AUTHENTICATE messages. It supports initiating SASL with PLAIN, aborting SASL, and performing the actual PLAIN payload verification, ultimately authenticating or registering the user, and then updating the connection state and sending appropriate IRC numeric replies.
Handles the SASL authentication flow for an IRC connection. When invoked, it interprets the first parameter to drive a SASL PLAIN exchange: it can prompt the client to provide credentials, abort SASL, or process a base64-encoded payload to authenticate or register a user, updating the connection state on success and replying with appropriate IRC numerics on failure. The method encapsulates the end-to-end SASL Plain handling, including error signaling and logging for traceability.
## Remarks
This method centralizes SASL negotiation within the command handler, bridging the IRC SASL protocol with the application's user store. It redacts the password in logs and relies on the user service to either authenticate or register the user, enabling a smooth first-time login flow. It validates payload structure and wraps the process in a catch block to translate unexpected errors into SASL failure feedback while preserving a consistent connection state.
This method centralizes the SASL PLAIN authentication handshake for a client, coordinating between the incoming [`IrcMessage`](IrcMessage.cs.md) payload, the server connection state (`_conn`), the user service (`_userService`), and server numerics. It performs decoding and validation of the SASL payload, derives a username from the payload, and attempts authentication first, then automatic registration as a fallback. On success, it binds the authenticated user to the connection (setting `Nickname`, `UserId`, and `IsAuthenticated`) and notifies the client with both `RPL_LOGGEDIN` and `RPL_SASLSUCCESS`. The structured exception handling ensures a consistent failure path with an `ERR_SASLFAIL` response and logging for operational visibility.
## Notes
- The SASL payload must decode to a null-delimited string yielding at least three parts; malformed payloads trigger an ERR_SASLFAIL response.
- The code derives the username from parts[1] when present, otherwise parts[0], and normalizes it to lowercase; the actual password is sourced from a redacted variable and is not logged.
- On success, the connection's Nickname and UserId are populated, the connection is marked authenticated, and the client receives both a LOGGEDIN notice and a SASL success reply; failures emit ERR_SASLFAIL and are logged for auditing.
- Malformed SASL payloads (e.g., payloads that do not yield at least three parts after decoding) trigger an authentication failure early, signaling to the client via `ERR_SASLFAIL`.
- If initial authentication fails, the flow transparently attempts to register a new user with the extracted credentials; if registration also fails, it reports the error back to the client and logs a warning.
- The password is sourced from the SASL PLAIN payload; ensure that credential handling complies with your security requirements and that secrets are managed appropriately within the `_userService`.
---
@@ -166,14 +156,13 @@ private async Task HandleAwayAsync(IrcMessage msg)
**Returns:** `Task`
HandleAwayAsync processes a user's away status in response to the IRC AWAY command. It first ensures the caller is registered; if not, it exits early. When a non-empty parameter is supplied, it stores that string as the away message on the connection, updates the user's status to Away via the chat service, and sends a RPL_NOWAWAY reply to the client. If no parameter is provided, it clears the away message, updates the status to Online with a null message, and sends a RPL_UNAWAY reply. The method is asynchronous, so it does not block the command handling path while performing persistence and network communication.
This private async method handles the AWAY command for the current connection. After confirming the user is registered via `RequireRegisteredAsync`, it checks for a non-empty first parameter on `msg.Parameters`; if present, it stores the away message on ``_conn`` (i.e., `_conn`), updates the user's status to `UserStatus.Away` via `_chatService`, and sends the `IrcNumericReply.RPL_NOWAWAY`. If no message is supplied, it clears the away message, updates the status to `UserStatus.Online`, and sends the `IrcNumericReply.RPL_UNAWAY`.
## Remarks
Consolidates away-state handling in a single place so all callers see the same effect on status and client notification. It keeps IrcCommandHandler lean by delegating away management and relies on _chatService to persist user state. It uses a simple, deterministic flow based on whether a message parameter is provided.
This method centralizes away-state management for the connected user by coordinating the connection state (`_conn`), persistence/update semantics (`_chatService`), and client feedback via numeric replies ([`IrcNumericReply`](IrcNumericReply.cs.md)). It ensures that providing an away message both reflects in server-side status and informs the client promptly.
## Notes
- Accessing _conn.UserId with the null-forgiving operator assumes RequireRegisteredAsync succeeded; calling this method without a valid registered session could throw a NullReferenceException.
- This method sends numeric replies (RPL_NOWAWAY / RPL_UNAWAY) to the connected client; ensure ServerName and _conn are valid at call time.
- Rapid, repeated calls may race with the chat service updates; consider sequencing on the caller side or adding concurrency guards.
---
@@ -194,15 +183,12 @@ private async Task HandleCapAsync(IrcMessage msg)
**Returns:** `Task`
HandleCapAsync processes CAP negotiation commands from the IRC server. It requires at least one parameter; if none are provided, it returns without action. It switches on the upper-cased first parameter to implement the SASL/capability handshake: on LS it requests the sasl capability and marks negotiation as in progress; on REQ it either acknowledges the 'sasl' request and enables SASL, or responds with NAK for the requested capability; on END it ends negotiation and, if identity information is available and the client is not yet registered, triggers a registration attempt via TryCompleteRegistrationAsync.
HandleCapAsync processes IRC CAP negotiation messages related to SASL authentication. It inspects the first element of `msg.Parameters` to drive a small, centralized CAP flow: starting negotiation with `LS`, acknowledging or declining a SASL request with `REQ`, and ending negotiation with `END`. The method updates internal connection state via `_conn.CapNegotiating`, `_conn.IsSasl`, and coordinates with registration by triggering `TryCompleteRegistrationAsync()` when appropriate. Messages are sent back to the server using `_conn.SendAsync`, built from the current `ServerName` (e.g. `":{ServerName} CAP * LS :sasl"`) and reflecting the outcome of each branch. The logic short-circuits on insufficient parameters and handles case-insensitive comparisons for SASL requests.
## Remarks
This method centralizes the CAP negotiation lifecycle for the IRC connection, coordinating with the connection state (_conn) to track whether a CAP negotiation is underway, whether SASL is engaged, and whether registration has completed. By encapsulating the protocol specifics here, it avoids scattering CAP handling logic across multiple handlers and ensures correct sequencing between CAP negotiation, SASL activation, and user registration.
## Notes
- The method short-circuits when there are no parameters, avoiding potential null-reference issues.
- The REQ path treats a missing or non-matching second parameter as a NAK for the requested capability, preserving protocol safety.
- END clears the negotiation flag and only triggers registration if Nickname and Username are non-null and the client is not already registered, preventing premature or repeated registration attempts.
The typical flow is:
- LS starts capability negotiation and marks the connection as negotiating.
- REQ sasl acknowledges SASL capability and enables SASL, while any other requested capability prompts a NAK with the requested name.
- END ends negotiation and, if credentials are present (non-null `Nickname` and `Username`) but the client is not yet registered, proceeds to complete registration via `TryCompleteRegistrationAsync()`.
---
@@ -224,22 +210,7 @@ private Task HandleCommandAsync(IrcMessage msg)
**Returns:** `Task`
Dispatches incoming IRC commands by normalizing the command to uppercase and routing to the corresponding per-command asynchronous handler, centralizing the IRC command handling logic (e.g., CAP -> HandleCapAsync, PRIVMSG -> HandlePrivmsgAsync). If the command is unknown, it responds with the ERR_UNKNOWNCOMMAND numeric back to the client.
## Remarks
Centralizes command dispatch behind a single switch expression, mapping command strings to their asynchronous handlers. This design makes it straightforward to extend support for new commands by adding a new case to the switch. It returns a Task to support asynchronous work and relies on the private _conn to send numeric replies back to the client; some branches return Task.CompletedTask to represent no-op work for certain commands (e.g., PONG).
## Example
```csharp
// Example: dispatch flow for a known command
IrcMessage msg = /* ... */;
await HandleCommandAsync(msg); // if msg.Command == "PRIVMSG" this path invokes HandlePrivmsgAsync(msg)
```
## Notes
- Unknown commands trigger an ERR_UNKNOWNCOMMAND reply, authored with the server name and the raw command.
- The PONG path is treated as a no-op by returning Task.CompletedTask, avoiding unnecessary asynchronous work.
Handles an incoming IRC command by normalizing the textual command to upper-case and dispatching to the corresponding asynchronous handler. As the central router, it maps pre-registration commands (such as `CAP`, `AUTHENTICATE`, `PASS`, `NICK`, [`USER`](../EchoHub.Core/Models/User.cs.md)) and post-registration commands (such as `PING`, `JOIN`, `PART`, `PRIVMSG`, `QUIT`, `NAMES`, `TOPIC`, `WHO`, `WHOIS`, `AWAY`, `LIST`, `MODE`, `MOTD`, and related aliases) to their dedicated `HandleXAsync` methods, returning the resulting `Task`. For the `PONG` case it completes synchronously with `Task.CompletedTask`; for any unknown command, it responds via `_conn.SendNumericAsync` using `IrcNumericReply.ERR_UNKNOWNCOMMAND` and the command text. This design provides a single, maintainable dispatch point that enforces consistent routing and error reporting across all IRC commands.
---
@@ -260,19 +231,7 @@ private async Task HandleJoinAsync(IrcMessage msg)
**Returns:** `Task`
Handles an IRC JOIN request for a registered user, performing parameter validation, channel-name mapping, and policy checks before joining the user to each requested channel. It delegates to backend services to perform the join, then updates the client with a JOIN confirmation, channel topic, NAMES list, and a decrypted history replay.
## Remarks
This function centralizes the join workflow for the IRC gateway and enforces privacy and policy constraints: end-to-end encrypted channels and server-managed system channels are blocked from IRC joins, ensuring the EchoHub client remains the source of truth for restricted channels. It coordinates with the connection object, channel service, and chat service to validate input, perform joins per channel (supporting RFC 1459-style per-channel keys), and synchronize the IRC client view (JOIN message, topic, NAMES, and history).
## Notes
- Requires the user to be registered; if not, the method exits early and no join is attempted.
- If there are fewer than one parameter, the gateway responds with ERR_NEEDMOREPARAMS to indicate insufficient input.
- For each channel, invalid channel names yield ERR_NOSUCHCHANNEL with an invalid channel notice.
- End-to-end encrypted channels are blocked from IRC joins; use the EchoHub client for such channels.
- System channels are blocked from IRC joins because they stream content over SignalR; use the EchoHub client for access.
- When a channel join requires a password and the provided key is incorrect or missing, the gateway responds with ERR_BADCHANNELKEY.
- History is replayed after joining, with Content and any embedded replies decrypted for proper IRC presentation.
`HandleJoinAsync` processes the IRC `JOIN` command for a connected user. It validates that the user is registered, requires at least one channel parameter, and then parses comma-separated channel names with optional per-channel keys; for each channel it translates the raw IRC channel name to the internal channel identifier, blocks end-to-end encrypted channels (which must be joined via the EchoHub client) and system channels (server-managed) from IRC, delegates the actual join to `_chatService.JoinChannelAsync` with the user's connection and identity, and on success updates the connection state, announces the join, and replays the channel topic, NAMES list, and decrypted history to the IRC client. On failure, it returns the appropriate IRC error (e.g. `ERR_BADCHANNELKEY` or `ERR_NOSUCHCHANNEL`).
---
@@ -293,15 +252,13 @@ private async Task HandleListAsync(IrcMessage msg)
**Returns:** `Task`
Implements the IRC LIST command for the server. After verifying the client is registered, it fetches the channel list from the channel service, filters to public channels, and sends one RPL_LIST reply per channel containing the channel name, online user count, and topic. If a channel is protected, a [+k] lock hint is prefixed to the topic. Private channels are intentionally hidden to match what the SignalR client sees. Once all public channels have been reported, it sends RPL_LISTEND to signal completion.
HandleListAsync processes the IRC LIST command by emitting the list of public channels to the connected client. It first ensures the caller is registered via `RequireRegisteredAsync()`; if not, it returns immediately. It then retrieves the channel collection from `_channelService.GetChannelListAsync()` and sends an `RPL_LIST` line for each channel that has `IsPublic` set to true, formatting the line as `#{ch.Name} {ch.OnlineCount} :{lockHint}{ch.Topic ?? ""}` where `lockHint` is `[+k] ` when `IsProtected` is true. After enumerating all public channels, it issues `RPL_LISTEND` with End of LIST to finish.
## Remarks
This handler encapsulates the server-side semantics of channel discovery separate from the client protocol encoding. By filtering to IsPublic channels, it keeps private channels from being exposed to clients, preserving privacy where appropriate. The lock indicator (+k) encodes channel protection state in the LIST output, while the Topic is plumbed directly into the listing, enabling clients to present useful metadata without additional requests. The approach keeps channel management in _channelService and I/O in _conn, promoting testability and a clean separation between data retrieval and protocol signaling.
By filtering to `IsPublic` channels, private channels are hidden from discovery, aligning the server's LIST output with the SignalR client's channel exposure. The `[+k]` indicator communicates a protected channel requiring a key and is propagated in the line alongside the channel's `Topic` (or an empty string if no topic is set). This method coordinates a read-only view of channel state and relies on [`IrcNumericReply`](IrcNumericReply.cs.md)-provided numeric codes (`RPL_LIST` and `RPL_LISTEND`).
## Notes
- The method requires a registered user; unauthenticated users will cause the method to return early without emitting LIST data due to the initial RequireRegisteredAsync check.
- Private channels are hidden by design via the IsPublic filter; modify the filter only if you intend to expose private channels and ensure client expectations are updated accordingly.
- The handler short-circuits if the user is not registered, so no LIST data is sent to unregistered users.
---
@@ -322,36 +279,7 @@ private async Task HandleModeAsync(IrcMessage msg)
**Returns:** `Task`
HandleModeAsync processes incoming MODE commands for channels and queries. It first ensures the caller is registered, then validates parameters and resolves the IRC target. For channel targets, it either returns the current channel mode or applies mode changes (notably +k to set a channel password and -k to clear it), persisting changes through the channel service and signaling results with the appropriate IRC numerics. When the target is not a channel, it responds with the user-mode indicator (+) to indicate no user modes are reported. If the channel cannot be resolved, it returns ERR_NOSUCHCHANNEL. When querying a channel's mode (MODE #channel with no extra parameters), it responds with RPL_CHANNELMODEIS and, if the channel is protected, indicates +k. For mode changes, it handles +k (requiring a key) and -k (clearing the key); unknown modes yield ERR_UNKNOWNMODE. A small, targeted behavior detail is that probing the ban list returns an empty list via RPL_ENDOFBANLIST to mirror common client expectations during join.
## Dependencies
- MODE
- Parameters
- IrcNumericReply
## Dependency APIs (verified signatures)
- property `Parameters` (`src/EchoHub.Server.Irc/IrcMessage.cs`)
- class [`IrcNumericReply`](IrcNumericReply.cs.md) (`src/EchoHub.Server.Irc/IrcNumericReply.cs`)
- field `string RPL_WELCOME`
- field `string RPL_YOURHOST`
- field `string RPL_CREATED`
- field `string RPL_MYINFO`
- field `string RPL_ISUPPORT`
- field `string RPL_MOTDSTART`
- field `string RPL_MOTD`
- field `string RPL_ENDOFMOTD`
- field `string ERR_NOMOTD`
- field `string RPL_NOTOPIC`
- field `string RPL_TOPIC`
- field `string RPL_NAMREPLY`
- …and 35 more member(s) not shown
## Symbol To Document
- Name: HandleModeAsync
- Kind: method
- File: src/EchoHub.Server.Irc/IrcCommandHandler.cs
- Language: csharp
- ID: 7207cb42-b229-42e0-9b0c-126018e8c975
Handles the IRC `MODE` command for a target in the gateway. It first ensures the caller is registered via `RequireRegisteredAsync` and returns user-mode information with `RPL_UMODEIS` when the target isnt a channel, emitting a leading `+` in that case. For channel targets, it resolves the internal channel name with `IrcToEchoHubChannel`, validates the channel, and then either reports the current mode with `RPL_CHANNELMODEIS` or processes mode changes such as ban-list probes (`b`/`+b`) and password changes (`+k`/`-k`) by delegating to `_channelService.SetChannelPasswordAsync` and broadcasting results through `_conn`; unknown modes yield `ERR_UNKNOWNMODE`.
---
@@ -372,14 +300,14 @@ private async Task HandleNamesAsync(IrcMessage msg)
**Returns:** `Task`
HandleNamesAsync processes an incoming NAMES-like query for the IRC command handler. It first ensures the caller is registered by awaiting RequireRegisteredAsync; if the user is not registered, the method exits early to prevent exposing channel membership information to unauthorized callers. It then requires at least one parameter; if none are provided, it returns without a response. It converts the first parameter to the internal EchoHub channel using IrcToEchoHubChannel; if this mapping yields null, the method again exits. When all preconditions succeed, it issues the names response for the mapped channel by calling SendNamesReplyAsync with that channel.
HandleNamesAsync is a private asynchronous method that processes a NAMES query. It begins by verifying the caller is registered using `RequireRegisteredAsync()`, returning early if not. It then validates that a parameter is provided (`msg.Parameters.Count < 1`); if not, it returns. The first parameter is converted to the internal channel name by `IrcToEchoHubChannel`, and if this conversion yields `null`, the method exits. Otherwise, it calls `SendNamesReplyAsync(channelName)` to emit the names list for the channel.
## Remarks
This method centralizes the NAMES query flow, isolating authentication, input validation, and channel-name resolution from the response formatting logic. It enforces that only authenticated, well-formed requests proceed to produce a response, contributing to predictable and secure command handling.
This method encapsulates the precondition checks for name-related queries and centralizes the channel-name translation, keeping the response logic contained in `SendNamesReplyAsync`.
## Notes
- Silent declines: if preconditions fail (not registered, missing parameters, or invalid channel mapping), the method returns without emitting a response.
- The mapping function (IrcToEchoHubChannel) determines whether an IRC channel reference has a corresponding internal EchoHub channel; a null result means no valid target was found, and no response is produced.
- If `IrcToEchoHubChannel` cannot map the input to a channel, no response is sent.
- The method relies on `Parameters` being provided by [`IrcMessage`](IrcMessage.cs.md) and uses early returns to avoid unnecessary work.
---
@@ -400,13 +328,10 @@ private async Task HandleNickAsync(IrcMessage msg)
**Returns:** `Task`
Handles the NICK command from a connected IRC client. It validates that a nickname parameter is supplied, enforces the server's username rules, stores a canonical lowercase nickname on the connection, and, when applicable, advances the registration flow by attempting to complete registration if a username is already present.
`HandleNickAsync` handles the NICK command by validating input and updating the connection state. If no nickname is supplied, it sends `ERR_NONICKNAMEGIVEN` with a "No nickname given" message. If the nickname fails the policy check against `ValidationConstants.UsernameRegex()`, it responds with `ERR_ERRONEUSNICKNAME` and a descriptive error like "Erroneous nickname (must be 3-50 chars: a-z, 0-9, _, -)". On success, it normalizes the nickname to lowercase via `ToLowerInvariant()` and assigns it to `_conn.Nickname`. Finally, if the connection is not yet registered but already has a `Username`, it advances the registration by calling `TryCompleteRegistrationAsync()`.
## Remarks
Centralizes nickname processing in the command handler to ensure consistent validation, normalization, and state progression. It uses numeric replies to communicate issues back to the client (missing nickname or invalid nickname) and coordinates with the registration logic via TryCompleteRegistrationAsync once the client is partially authenticated. Normalizing to lowercase provides a stable internal identity, independent of the client's casing.
## Notes
- The error text for invalid nicknames lists allowed characters and length; confirm that UsernameRegex() and the user-visible message remain in sync to avoid misleading users.
By encapsulating parameter validation, nickname syntax enforcement, normalization, and the progression toward registration, this method centralizes the Nick command workflow. It coordinates with `_conn` to store the chosen nickname, uses [`IrcNumericReply`](IrcNumericReply.cs.md) values to emit exact IRC error codes for invalid or missing nicknames, and triggers `TryCompleteRegistrationAsync()` when appropriate, ensuring a cohesive startup sequence.
---
@@ -427,15 +352,15 @@ private async Task HandlePartAsync(IrcMessage msg)
**Returns:** `Task`
Leaves one or more IRC channels as requested by an incoming IrcMessage. It first ensures the caller is registered; if not, it exits early without issuing any IRC traffic. It expects at least one parameter; the first parameter is a comma-separated list of raw channel names, and an optional second parameter carries a part message to be appended after PART. For each channel in the list, the method translates the raw channel into an internal channel name using IrcToEchoHubChannel; if mapping returns null, that channel is skipped. It then tells the chat service to leave the mapped channel, updates the local connection state by calling LeaveChannel, and finally emits the IRC PART command for that channel, including the optional message.
Handles a PART command from a registered IRC client by parsing a comma-separated list of channels from the first parameter and an optional part message from the second parameter, then leaving each channel both in the internal chat state and by sending an IRC PART message back to the client.
Channels are mapped from their raw IRC name to the internal Echo Hub channel via `IrcToEchoHubChannel`; invalid mappings are skipped. For each valid channel, the method first awaits `_chatService.LeaveChannelAsync(_conn.ConnectionId, _conn.Nickname!, channelName)`, then updates the local connection state with `_conn.LeaveChannel(channelName)`, and finally emits the IRC PART notice `":{_conn.Hostmask} PART #{channelName}"` with an optional payload appended if a part message was supplied.
## Remarks
Acts as a coordination boundary between the IRC protocol and the application's connection state. It encapsulates registration verification, channel translation, state mutation, and protocol emission in a single command path. Because it awaits each channel in sequence, multiple PARTs are issued in order rather than in parallel.
Coordinates internal state with the external IRC protocol to keep the users channel memberships in sync across both domains. The `IrcToEchoHubChannel` mapping acts as a guardrail, ensuring only recognized channels are processed and leaving others untouched.
## Notes
- Early returns ensure no actions occur if the user is not registered or if no channels are specified.
- Channels that cannot be translated via IrcToEchoHubChannel are skipped without error.
- The emitted PART command uses the hostmask and a '#channel' target, and appends an optional message if provided.
- If `IrcToEchoHubChannel` yields `null` for a channel, that channel is ignored rather than causing an exception.
---
@@ -456,15 +381,14 @@ private Task HandlePassAsync(IrcMessage msg)
**Returns:** `Task`
HandlePassAsync is a private helper in the IRC command handling flow that processes the PASS command for a connection. It blocks re-registration by sending ERR_ALREADYREGISTERED when the connection is already registered, and if a password parameter is provided, it routes that parameter to the connections password handling path (the actual value is redacted in this snippet). In cases where neither condition applies, it completes without performing additional work.
HandlePassAsync processes the IRC PASS command for the current connection. If the connection is already registered (`_conn.IsRegistered`), it replies with `ERR_ALREADYREGISTERED` by calling `_conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_ALREADYREGISTERED, ":You may not reregister")`; if a password parameter is provided, it stores the password on the connection (the exact storage is redacted in the source). The method always completes by returning a `Task`—the send task when replying, or `Task.CompletedTask` when no action is needed.
## Remarks
This abstraction centralizes PASS command handling within the command handler to ensure consistent protocol error signaling and password processing across the handshake sequence. It delegates state management and messaging to the underlying connection object, which keeps the command dispatch logic focused and testable. The explicit redaction of the password demonstrates a security-conscious approach to handling sensitive data, avoiding exposure in logs or snapshots. By returning a Task, the method remains composable with the asynchronous command pipeline.
Centralizes PASS command handling within the `IrcCommandHandler` and enforces the re-registration guard in one place. It updates the connection state when a parameter is present, separating command validation from the subsequent authentication flow.
## Notes
- If a PASS parameter is provided, ensure proper validation and secure handling of the credential; the actual value is redacted here, so verify correctness in your environment.
- The method relies on external state (_conn.IsRegistered) and may either complete synchronously or proceed asynchronously via SendNumericAsync; callers should await as appropriate to preserve command-ordering guarantees.
- This function does not perform full authentication itself; it coordinates with the connection object for state and output, acting as a gateway in the PASS handling path.
- The method is not declared `async`; it returns a `Task` and may complete synchronously via `Task.CompletedTask` when no password parameter is supplied.
- The password value is written to a connection field whose exact name is redacted; handling of this sensitive data should be reviewed in the surrounding authentication flow.
---
@@ -485,24 +409,7 @@ private async Task HandlePingAsync(IrcMessage msg)
**Returns:** `Task`
Responds to IRC PING messages by sending a corresponding PONG back to the server to keep the connection alive. It chooses the token to include in the PONG from the incoming message: if a parameter is present, that token is used; otherwise it falls back to the server name. The response is sent using the underlying connection with the format :<ServerName> PONG <ServerName> :<token>.
## Remarks
Internally, this method serves as the keep-alive handler for the IRC command flow. By basing the PONG on ServerName and the received parameters, it guarantees a consistent reply format and avoids leaking raw protocol details to higher layers. It relies on the ServerName and Parameters dependencies and on the underlying connection to transmit the response.
## Example
```csharp
// Example: a PING with a token results in a PONG containing that token
var token = "12345";
// Assuming ServerName is "irc.example.org"
var response = $":{ServerName} PONG {ServerName} :{token}";
// The actual send occurs via _conn.SendAsync in HandlePingAsync
```
## Notes
- If msg.Parameters is empty, token defaults to ServerName.
- The method is private; usage is internal to the IrcCommandHandler and not exposed publicly.
- Exceptions from SendAsync propagate; callers may need to log or retry as part of larger connection management.
`HandlePingAsync` handles an IRC `PING` by replying with a `PONG` to keep the connection alive. It reads the first parameter from the incoming `IrcMessage.Parameters` as the token, or falls back to `ServerName` if none is provided, and sends the response via `_conn.SendAsync` using the IRC format `":{ServerName} PONG {ServerName} :{token}"`.
---
@@ -523,16 +430,9 @@ private async Task HandlePrivmsgAsync(IrcMessage msg)
**Returns:** `Task`
Handles the PRIVMSG command by validating parameters, ensuring the sender is registered, and routing channel-targeted messages to the EchoHub chat service. It rejects private messages (targets that do not start with '#') with an appropriate error and surfaces delivery failures back to the IRC client.
Handles an IRC PRIVMSG from a connected user by first ensuring the user is registered, then validating that the message has enough parameters and targets a channel. If parameters are missing, it replies with `ERR_NEEDMOREPARAMS`; if the target is not a channel (does not start with `#`), it replies with `ERR_NOSUCHNICK` and instructs to use channels. It then maps the IRC channel to an internal EchoHub channel via `IrcToEchoHubChannel` and forwards the message content to the chat service through [`SendMessageAsync`](../EchoHub.Server/Services/ChatService.cs.md), supplying the current connection's user id, nickname, channel name, message content, and connection id. If the chat service reports an error, it communicates it back to the client with `ERR_CANNOTSENDTOCHAN` for the affected channel.
## Remarks
This method acts as a boundary between IRC protocol handling and the EchoHub chat system. It enforces channel-only messaging for PRIVMSG, consolidates parameter validation and error reporting via IRC numeric replies, and delegates the actual delivery to a dedicated chat service. By encapsulating channel-name translation (IrcToEchoHubChannel) and the delivery call (SendMessageAsync), it keeps command handling focused and testable, while remaining resilient to mapping failures and chat-service errors.
## Notes
- Requires the current connection to be registered; otherwise the operation is short-circuited.
- If the PRIVMSG target does not begin with '#', a private-message error is returned: ERR_NOSUCHNICK with a hint to use channels.
- If channel name mapping returns null, the method exits without performing delivery.
- If SendMessageAsync reports an error, the client receives ERR_CANNOTSENDTOCHAN to indicate delivery failure to the channel.
In short, it acts as the IRC surface to the EchoHub chat layer for channel-based private messages, performing parameter validation, channel resolution, and error propagation in a single, cohesive flow.
---
@@ -553,22 +453,7 @@ private async Task HandleQuitAsync(IrcMessage msg)
**Returns:** `Task`
Handles a quit event by sending an IRC ERROR line to the active connection that signals the closing of the link. It derives the quit reason from the first parameter of the incoming IrcMessage when provided, otherwise it uses 'Client quit' as a default.
## Remarks
Centralizes the termination messaging for quit scenarios, ensuring a consistent closing notice across paths that terminate a connection. It formats the message with the current nickname and the resolved quit reason, and delegates the actual network transmission to _conn.SendAsync, keeping the higher-level quit flow simple and testable.
## Example
```csharp
// Example usage within the same class (quit with a reason)
var msg = new IrcMessage { Parameters = new List<string> { "Server maintenance" } };
await HandleQuitAsync(msg);
```
## Notes
- This method only sends the closing line; it does not by itself terminate the connection. The caller should close the connection after the message is sent.
- It relies on msg.Parameters[0] as the quit reason; if there are multiple parameters, only the first is used.
- Assumes msg.Parameters is non-null; if it's null, this will throw a NullReferenceException.
HandleQuitAsync is a private async method that processes a quit request by sending an IRC `ERROR` message to close the client connection. It derives the quit reason from the first element of the [`IrcMessage`](IrcMessage.cs.md)'s `Parameters` (falling back to the literal `Client quit` if none is provided) and includes the nickname via `_conn.Nickname` in the response by calling `_conn.SendAsync` with the string `ERROR :Closing Link: <nickname> (<quitMessage>)`.
---
@@ -589,16 +474,11 @@ private async Task HandleTopicAsync(IrcMessage msg)
**Returns:** `Task`
HandleTopicAsync processes the IRC TOPIC command for a channel. It verifies the caller is registered, resolves the channel name from the command parameters, and either sends the current topic or updates it via the channel service using the provided topic text. On a successful update, it broadcasts the change to SignalR clients and echoes the topic back to the IRC client; on failure it returns an appropriate IRC numeric error.
HandleTopicAsync processes an IRC TOPIC command for a channel. It ensures the caller is registered, validates parameters, converts the IRC channel name to the internal channel via `IrcToEchoHubChannel`, and then either returns the current topic with `SendChannelTopicAsync` when only the channel is provided or updates the topic via `_channelService.UpdateTopicAsync` using the current user's ID, passing `null` for an empty topic. If the update fails, it replies with an IRC numeric error—`ERR_NOSUCHCHANNEL` when the channel is not found, otherwise `ERR_CHANOPRIVSNEEDED`—including the error message; on success, it broadcasts the updated channel to connected web clients via `_chatService.BroadcastChannelUpdatedAsync` and echoes the new topic back to the IRC client with `SendAsync` using the `TOPIC` command.
## Remarks
This method acts as the integration point between IRC command handling, domain services, and client notifications. It relies on the authentication check (RequireRegisteredAsync) and uses the channel service to persist topic changes while informing connected clients through the chat service. Numeric errors are produced through IrcNumericReply based on the nature of the failure (non-existent channel vs. insufficient privileges), ensuring correct IRC protocol behavior. When a topic is cleared, a whitespace topic is treated as null and passed to UpdateTopicAsync, signaling a topic removal.
This method centralizes the TOPIC command flow: it validates the caller, resolves the channel, performs the update, and coordinates notification to both SignalR clients and the IRC client. It also maps domain errors to IRC numeric replies to preserve protocol semantics across layers.
## Notes
- The method returns early if the caller is not registered or if there are insufficient parameters, preventing unintended state changes.
- It uses a null-forgiving operator on UserId when updating the topic; preconditions ensure a valid user context.
- Topic clearing is achieved by passing null to UpdateTopicAsync when the provided topic string is whitespace.
- Error handling maps ChannelError.NotFound to ERR_NOSUCHCHANNEL and all other failure cases to ERR_CHANOPRIVSNEEDED, aligning with IRC protocol expectations.
---
@@ -619,15 +499,7 @@ private async Task HandleUserAsync(IrcMessage msg)
**Returns:** `Task`
Handles the USER command as part of the IRC registration handshake. It first rejects re-registration attempts, then validates that enough parameters are present, stores the provided username and real name on the connection, and finally triggers registration completion if a nickname has already been supplied.
## Remarks
This method encapsulates the user-side portion of the registration flow, coordinating between the incoming command data (via IrcMessage.Parameters) and the connection state. By separating the completion trigger (TryCompleteRegistrationAsync) from initial USER parsing, it keeps the registration logic cohesive and allows the NICK/USER agreement to occur in any order. It relies on the server-generated numeric replies to communicate errors back to the client and uses the connection state to decide when registration can advance.
## Notes
- Parameter indexing assumes four parameters for a valid USER command; if fewer are provided, the handler responds with ERR_NEEDMOREPARAMS. The RealName is taken from Parameters[3], which is a potential source of off-by-one mistakes if the protocol is extended or parameters are reformatted.
- A full registration is only completed when a nickname is already present; otherwise, the method merely populates Username and RealName and leaves completion to a later trigger when Nickname arrives.
- The code does not validate that Username or RealName are non-empty; additional validation may be needed if stricter user data integrity is required.
HandleUserAsync is the private asynchronous handler for processing the IRC USER command as part of the client registration flow. It first guards against re-registration by sending the IRC numeric `ERR_ALREADYREGISTERED` via `_conn.SendNumericAsync` when `_conn.IsRegistered` is true, and then returns. If there are fewer than four parameters, it responds with `ERR_NEEDMOREPARAMS` and terminates early. When invoked with a valid parameter set, it assigns the username from `msg.Parameters[0]` to `_conn.Username` and the real name from `msg.Parameters[3]` to `_conn.RealName`. Finally, if a nickname has already been established (`_conn.Nickname` is not null), it awaits `TryCompleteRegistrationAsync()` to advance the registration process.
---
@@ -648,14 +520,35 @@ private async Task HandleWhoAsync(IrcMessage msg)
**Returns:** `Task`
Responds to an IRC WHO request for a channel by listing online users and signaling completion. It is invoked when a registered client asks for the current participants of a channel; it maps the supplied channel parameter to EchoHub's channel, retrieves online users via the chat service, and streams RPL_WHOREPLY rows followed by RPL_ENDOFWHO to the client.
HandleWhoAsync processes a WHO request for a channel by validating the caller and translating the IRC channel into the EchoHub channel, then streaming the current online users. It first ensures the client is registered, validates that a channel parameter is provided, and derives the internal channel name with `IrcToEchoHubChannel`. If any of these steps fail, it exits without emitting data. When a valid channel is obtained, it fetches online users via `_chatService.GetOnlineUsersAsync(channelName)` and, for each user, sends a `RPL_WHOREPLY` using [`IrcNumericReply`](IrcNumericReply.cs.md) data, encoding the channel, user, host (`echohub`), server, user nickname, away state, hop count, and display name (falling back to the username when necessary). After listing all users, it signals completion with `RPL_ENDOFWHO`.
This method is the IRC-facing surface that translates EchoHub's online-user model into IRC protocol replies, making it the point of integration for WHO-style channel listings. The flow is fully asynchronous and relies on the [`UserStatus`](../EchoHub.Core/Models/UserStatus.cs.md) enum to determine the away flag, as well as the defined `RPL_WHOREPLY`/`RPL_ENDOFWHO` numeric replies for protocol correctness.
---
### HandleWhoisAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleWhoisAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleWhoisAsync processes the IRC WHOIS command by ensuring the requester is registered, validating the target nick parameter, and then assembling and sending the standard WHOIS information for that user. It fetches the user profile, emits the appropriate WHOIS numeric replies (and the away/idle data when available), and gracefully reports when the target nick does not exist.
## Remarks
By translating EchoHub's channel membership into IRC WHO semantics, this method acts as the bridge between the IRC protocol and the chat model. It performs early guards (registration and parameter validation) before querying the chat service, ensuring consistent behavior and preventing unnecessary work for unauthenticated callers. Each user is emitted with a RPL_WHOREPLY line containing their nick, username, server, and away/here flag, followed by a final EndOfWho line to signal completion.
This method acts as a protocol adapter that wires together user data and channel memberships to produce a coherent WHOIS response. It coordinates between `_userService` for profile data, `_chatService` for channel membership, and `_conn` for sending IRC numerics, encapsulating the protocol-specific choreography in a single, testable unit. The logic defensively handles missing profile data and optional information (channels, away message) to align with RFC-like WHOIS expectations while keeping the flow readable and isolated from business rules.
## Notes
- Always emits an End of WHO line even if the channel has no online users.
- Away vs. here status is encoded as 'G' for away and 'H' for present, matching IRC conventions.
---
@@ -676,15 +569,7 @@ private static string? IrcToEchoHubChannel(string ircChannel)
**Returns:** `string?`
Converts a raw IRC channel into a canonical EchoHub channel name by stripping the leading '#', lowercasing, and trimming the remainder, returning null if the result does not satisfy ValidationConstants.ChannelNameRegex. This is used when bridging IRC channels to EchoHub to obtain a policy-compliant channel identifier.
## Remarks
Centralizes the logic for translating IRC-style channels into EchoHub identifiers and enforces channel naming policy via ValidationConstants.ChannelNameRegex. It returns a lowercase, trimmed name when valid, or null when the input cannot be mapped, allowing callers to handle non-mappable channels explicitly.
## Notes
- If ircChannel is null, this method will throw a NullReferenceException; callers should ensure a non-null value before calling.
- Results are always lowercase due to ToLowerInvariant, providing a consistent channel namespace.
- A non-matching input yields null rather than an exception, signaling an unmapped channel to the caller.
IrcToEchoHubChannel converts a raw IRC channel name into EchoHub's internal channel identifier, returning null when the input cannot be mapped. It requires the input to start with the '#' prefix and to be at least two characters long; it then drops the leading '#', lowercases the remainder invariantly, trims whitespace, and validates the result against the central channel-name pattern provided by `ValidationConstants.ChannelNameRegex()`. If the name matches, the canonical, lowercased name is returned; otherwise null. This function is typically invoked when translating IRC channel references into EchoHub's normalized channel namespace, ensuring downstream logic always works with validated, consistent channel names rather than arbitrary IRC inputs.
---
@@ -699,25 +584,10 @@ private async Task<bool> RequireRegisteredAsync()
**Returns:** `Task<bool>`
RequireRegisteredAsync is a small helper that enforces a precondition: the client connection must be registered before proceeding with commands that require registration. It returns true when the connection is already registered; otherwise it sends the IRC error reply ERR_NOTREGISTERED and returns false. Callers await this method to guard subsequent operations and avoid duplicating boilerplate checks across command handlers.
`RequireRegisteredAsync` checks whether the IRC connection is registered and returns true when it is. If not registered, it sends the standard `ERR_NOTREGISTERED` reply using `SendNumericAsync` with `ServerName`, `IrcNumericReply.ERR_NOTREGISTERED`, and the message `":You have not registered"`, then returns false.
## Remarks
This abstraction centralizes the registration precondition and the associated user feedback. It guarantees consistent behavior by issuing the standard ERR_NOTREGISTERED along with the message You have not registered, matching the IRC protocol's expectations, and it short-circuits command execution when the precondition isnt met.
## Example
```csharp
// Usage: ensure the user is registered before issuing a command that requires registration
if (!await RequireRegisteredAsync())
{
return; // bail out if not registered
}
// proceed with the operation that requires registration
```
## Notes
- Ensure the caller returns immediately when RequireRegisteredAsync() returns false to avoid sending duplicate replies.
- This helper assumes the underlying connection (_conn) and the server name (ServerName) are initialized; null references may occur if called too early.
Conceptually, this method centralizes the precondition for commands that require a registered session, avoiding duplicated checks across handlers. It relies on `_conn` to inspect `IsRegistered`, and on `ServerName` and `IrcNumericReply.ERR_NOTREGISTERED` to deliver a consistent IRC-compliant error. By returning a boolean, it makes the caller's flow straightforward: proceed when true, bail when false.
---
@@ -738,15 +608,15 @@ public async Task RunAsync(CancellationToken ct)
**Returns:** `Task`
Runs an asynchronous loop that continuously reads lines from the IRC connection, trims trailing CR/LF, ignores blank lines, and dispatches each non-empty message to the IRC command handler until cancellation is requested. This is the core IO loop for processing incoming IRC traffic in the command handler lifecycle; you start it to begin processing and cancel it to stop.
Runs an asynchronous, cancellation-aware loop that reads lines from the IRC connection via `_conn.ReadLineAsync(ct)`, stops when `line` is `null`, trims trailing CR/LF, skips blank lines, logs each received line with `_logger.LogDebug("IRC < {Id}: {Line}", _conn.ConnectionId, line)`, parses the line into an [`IrcMessage`](IrcMessage.cs.md) using `IrcMessage.Parse(line)`, and dispatches the resulting message to `HandleCommandAsync(msg)`. This method is the central inbound processor for an IRC connection: it bridges the raw socket input to the higher-level command handling logic and continues running until the provided `CancellationToken ct` signals cancellation or the connection ends.
## Remarks
RunAsync is the primary lifecycle loop for the IRC command processor. It reads a raw line via _conn.ReadLineAsync(ct), cleans trailing CR/LF, and skips empty lines before turning the line into an IrcMessage with IrcMessage.Parse. The resulting message is passed to HandleCommandAsync for per-command processing, and any exceptions thrown during that processing are caught and logged to avoid tearing down the loop. Only the HandleCommandAsync call is wrapped in the try-catch; errors in reading, parsing, or line pre-processing may bubble up if they throw, which means callers should supervise the task accordingly.
This method is the primary inbound processor for a single IRC connection, isolating IO, parsing, and command dispatch from higher-level application logic. It logs critical diagnostic information: per-line debugging via `_logger.LogDebug` and per-command failures via `_logger.LogError`, including the command name and the nick when available. By catching exceptions only around `HandleCommandAsync(msg)` it ensures that a failure in handling one command does not crash the entire loop, preserving resilience.
## Notes
- Exceptions from ReadLineAsync or IrcMessage.Parse are not caught here; they could terminate the loop.
- The loop ends when a null line is read (end of stream) or when the cancellation token is canceled.
- Whitespace-only lines are ignored; lines are trimmed before parsing.
- The call to `IrcMessage.Parse(line)` occurs outside the `try` block that guards `HandleCommandAsync(msg)`; a parsing error could bubble up and terminate the loop. Consider moving parsing inside the try/catch or adding its own guard.
---
@@ -767,14 +637,40 @@ private async Task SendChannelTopicAsync(string channelName)
**Returns:** `Task`
Fetches the current topic for the specified channel and sends the corresponding IRC numeric to the client. It queries the channel service for (topic, exists) and, if the channel exists, emits RPL_TOPIC when a topic is set or RPL_NOTOPIC when no topic is configured; if the channel doesn't exist, it returns without replying.
This private helper fetches the current topic for a channel and, if the channel exists, delivers the appropriate IRC numeric reply to the connected client. It calls `_channelService.GetChannelTopicAsync(channelName)` to obtain `(topic, exists)` and, depending on the result, returns early when the channel doesn't exist, sends `RPL_TOPIC` with `#<channelName> :<topic>` when a topic is set, or sends `RPL_NOTOPIC` with `#<channelName> :No topic is set` when there is no topic.
## Remarks
By centralizing the topic-resolution and numeric-emission logic in a single private method, this symbol encapsulates the IRC topic-response behavior for channel-related command flow. It hides the implementation details of IrcNumericReply mappings behind a concise interface and ensures consistent message formatting (channel name prefixed with '#', topic payload prefixed with ':') when interacting with the connection and channel services.
This method acts as a small integration point between channel-data access and IRC protocol messaging. By encapsulating the topic-notification logic, it coordinates `_channelService` (data) and `_conn` (connection) to produce consistent numeric replies via [`IrcNumericReply`](IrcNumericReply.cs.md) constants, reducing duplication across the command-handling code. Its private scope signals it's an internal helper used by higher-level IRC commands, keeping the channel-topic flow centralized.
## Notes
- If exists is false, the method returns early with no notification to the client.
- When a channel exists but has no topic, a RPL_NOTOPIC reply is sent with the message "No topic is set".
- If the channel does not exist (`exists` is false), the method returns without sending any reply, which can appear as a missing response to the client; callers should ensure channel existence or handle this case.
## Dependencies
- IrcNumericReply
## Dependency APIs (verified signatures)
- class [`IrcNumericReply`](IrcNumericReply.cs.md) (`src/EchoHub.Server.Irc/IrcNumericReply.cs`)
- field `string RPL_WELCOME`
- field `string RPL_YOURHOST`
- field `string RPL_CREATED`
- field `string RPL_MYINFO`
- field `string RPL_ISUPPORT`
- field `string RPL_MOTDSTART`
- field `string RPL_MOTD`
- field `string RPL_ENDOFMOTD`
- field `string ERR_NOMOTD`
- field `string RPL_NOTOPIC`
- field `string RPL_TOPIC`
- field `string RPL_NAMREPLY`
- …and 35 more member(s) not shown
## Symbol To Document
- Name: `SendChannelTopicAsync`
- Kind: `method`
- File: `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
- Language: `csharp`
- ID: `d23ca663-2c48-4576-9b5f-759527f87f1c`
---
@@ -796,14 +692,7 @@ private async Task SendModeErrorAsync(string channelName, ChannelOperationResult
**Returns:** `Task`
Translates a channel operation error into the corresponding IRC numeric response and sends it to the client for the specified channel. It chooses the numeric based on result.Error (NotFound -> ERR_NOSUCHCHANNEL, Forbidden -> ERR_CHANOPRIVSNEEDED, otherwise ERR_KEYSET) and delivers a message containing the channel (prefixed with '#') and the human-readable error via _conn.SendNumericAsync(ServerName, numeric, `#${channelName} :${result.ErrorMessage}`). This method centralizes the error reporting for channel-mode operations so callers don't duplicate the mapping and formatting logic.
## Remarks
By centralizing the error-to-numeric mapping, this method ensures consistent client feedback and prevents duplication of channel-name formatting and error-message construction across callers. It relies on the surrounding classs _conn and ServerName being available; changes to the mapping or messaging format would affect all mode-error reports produced by this helper.
## Notes
- The error mapping is not exhaustive: any ChannelError value not explicitly NotFound or Forbidden will default to ERR_KEYSET.
- This method is private and intended solely for internal command-handling use; it is not part of the public API.
Translates a channel-mode operation failure into the appropriate IRC numeric for the target channel and forwards it to the server. When a mode operation fails for the given `channelName`, the method maps the domain error to an IRC numeric using a switch over [`ChannelError`](../EchoHub.Core/DTOs/CommonDtos.cs.md) (NotFound -> `IrcNumericReply.ERR_NOSUCHCHANNEL`, Forbidden -> `IrcNumericReply.ERR_CHANOPRIVSNEEDED`, otherwise `IrcNumericReply.ERR_KEYSET`) and sends a message using `_conn.SendNumericAsync(ServerName, numeric, `$"#{channelName} :{result.ErrorMessage}"`)`.
---
@@ -818,17 +707,15 @@ private async Task SendMotdAsync()
**Returns:** `Task`
SendMotdAsync is an internal helper that transmits the server's Message of the Day (MOTD) to the connected client. It validates the configured Motd; if it is missing or whitespace it replies with ERR_NOMOTD and stops. Otherwise it sends a MOTD banner with RPL_MOTDSTART, then each newline-delimited line as an RPL_MOTD, trimming CR characters, and ends with RPL_ENDOFMOTD.
This method consolidates MOTD delivery behind a private surface, so higher-level Irc command handlers don't need to know the exact numeric codes or line-breaking semantics. It depends on _conn for transport and _options for the Motd value, and it's a private method intended to be invoked by the MOTD-related command flow.
SendMotdAsync is a private asynchronous helper that delivers the server's Message of the Day to the current IRC connection. It checks the configured `Motd` on `_options` and, if missing, responds with the IRC error code `ERR_NOMOTD`; otherwise it streams the MOTD lines between `RPL_MOTDSTART` and `RPL_ENDOFMOTD` using `RPL_MOTD` for each line. This method formats each line by trimming a trailing carriage return and sends one line per message, adhering to the IRC protocol expectations.
## Remarks
Encapsulates the formatting and transport of MOTD to ensure consistent behavior across the server. By isolating the MOTD delivery, it keeps the command-handling code focused on protocol logic rather than presentation details.
`SendMotdAsync` centralizes MOTD delivery to ensure consistent IRC protocol formatting and behavior. It relies on `_conn` to emit numeric replies and on the [`IrcNumericReply`](IrcNumericReply.cs.md) constants to signal the start, each line, and the end of the MOTD, while consulting the configured `Motd` via the `_options` object. This encapsulation prevents duplication and makes it straightforward to adjust MOTD formatting in one place.
## Notes
- If Motd is null or whitespace, the method sends ERR_NOMOTD and returns without sending any MOTD lines.
- Each MOTD line is sent as a separate RPL_MOTD message; the code splits on '\n' and trims a trailing '\r' from each line to normalize Windows-style endings. A trailing newline in Motd may produce an empty MOTD line.
- All sends are awaited asynchronous calls to the connection; exceptions propagate to the caller.
- The method trims trailing carriage returns (`'\r'`) from each MOTD line to gracefully handle Windows-style line endings when sending lines via `RPL_MOTD`.
- If `_options.Motd` is null or whitespace, the method short-circuits and emits `ERR_NOMOTD` before attempting any `RPL_MOTD` messages.
- MOTD lines are sent individually in order, one `RPL_MOTD` message per line, followed by `RPL_ENDOFMOTD` to mark completion.
---
@@ -849,15 +736,14 @@ private async Task SendNamesReplyAsync(string channelName)
**Returns:** `Task`
Sends the channels NAMES list to the IRC client by querying the chat service for online users in the channel, producing a space-separated set of nicknames, and then emitting two standard IRC numerics: RPL_NAMREPLY with the channel and nicklist, and RPL_ENDOFNAMES to mark completion. This method is invoked when handling a NAMES request for a channel, and it centralizes the formatting and numeric-codes so callers don't have to build the response themselves.
Responds to an IRC NAMES request for a channel by collecting the currently online users and emitting the standard numeric replies that enumerate channel members. It calls `_chatService.GetOnlineUsersAsync(channelName)` to obtain user objects, builds a space-separated list of their `Username`s, and sends two numeric replies: first `IrcNumericReply.RPL_NAMREPLY` with the channel's nick list via `_conn.SendNumericAsync`, and then `IrcNumericReply.RPL_ENDOFNAMES` to mark the end of the list.
## Remarks
This keeps NAMES formatting centralized and aligns with the IRC protocol surface exposed by IrcNumericReply. It delegates data retrieval to _chatService and transmission to _conn, making the implementation resilient to channel naming and user list changes. It also ensures the end-of-list is always signaled after the list is sent, which is essential for IRC clients to know the response is complete.
This method encapsulates the protocol details of responding to the IRC `NAMES` command for a channel. It isolates the discovery of online users from the formatting and emission of the numeric replies, ensuring consistent NAMES responses and simplifying the caller's responsibilities.
## Notes
- If no online users are found, the NAMES reply will carry an empty nicklist while still issuing EndOfNames; clients should handle an empty list gracefully.
- Any exceptions raised by GetOnlineUsersAsync or SendNumericAsync bubble up to the caller, so this method assumes the surrounding command handler will decide how to respond to errors.
- If there are no online users, the constructed `nicks` string will be empty, but an `RPL_NAMREPLY` line will still be emitted followed by `RPL_ENDOFNAMES`.
- The method is private and relies on `_chatService` and `_conn` being available; callers must ensure the surrounding context handles validation and errors appropriately.
---
@@ -872,14 +758,13 @@ private async Task SendWelcomeBurstAsync()
**Returns:** `Task`
Sends the IRC welcome burst to a newly connected client by issuing the standard numeric replies (RPL_WELCOME, RPL_YOURHOST, RPL_CREATED, RPL_MYINFO, RPL_ISUPPORT) and then starts the MOTD flow via SendMotdAsync. It uses the current connection's nickname and the server name to populate the messages, and awaits each dispatch to preserve the canonical handshake order.
This private async method emits the initial IRC handshake to the connected client by sending a series of standard numeric replies. It reads the nickname from `_conn.Nickname`, uses `ServerName` as the server identity, and dispatches the numerics `RPL_WELCOME`, `RPL_YOURHOST`, `RPL_CREATED`, `RPL_MYINFO`, and `RPL_ISUPPORT` via `_conn.SendNumericAsync`. After sending these banners, it calls `SendMotdAsync` to deliver the MOTD and complete the handshake.
## Remarks
It centralizes the initial handshake, ensuring a consistent greeting sequence for every new user. By consuming IrcNumericReply codes and composing messages with the live nickname, server name, and current time, it guarantees the client receives both identification and capability information before proceeding. The method delegates the final output of the MOTD to SendMotdAsync, keeping the handshake concerns isolated from the MOTD generation.
This method centralizes the handshake so every connection receives a consistent welcome, isolating IRC protocol formatting from higher-level command handling. It relies on the [`IrcNumericReply`](IrcNumericReply.cs.md) constants to produce the standard numerics and on `_conn` to transmit messages, keeping the transport details out of the handshake logic. The method assumes `_conn.Nickname` is non-null at the time it runs, as evidenced by the null-forgiving read.
## Notes
- Relies on _conn and Nickname being non-null; the null-forgiving operator means a null nickname could yield a greeting with an empty nickname.
- RPL_CREATED uses DateTimeOffset.UtcNow; this stamps the handshake time rather than the server creation date, which may be intentional for the MOTD moment but can be misleading if interpreted as server age.
- Be aware that `_conn.Nickname` is read with a null-forgiving operator; if nickname isn't set yet, a runtime `NullReferenceException` could occur. Ensure the nickname is established earlier in the connection sequence before calling this method.
---
@@ -894,40 +779,12 @@ private async Task TryCompleteRegistrationAsync()
**Returns:** `Task`
Finalizes the user's registration by completing the authentication handshake and establishing an active session. It guards against concurrent registration work by returning early if capability negotiation is still in progress or the user is already registered. If SASL-based authentication has already succeeded (IsAuthenticated and UserId is not null), it marks the connection as registered, notifies the chat service of the connected user, and sends the welcome burst to complete onboarding.
If SASL authentication is not yet complete, it enforces a password-based login: a missing password results in an IRC error and authentication failure. When a password is supplied, it delegates to the user service to authenticate; if that fails, it attempts to register a new user with the provided nickname and password. On a successful outcome, it stores the resulting UserId and Username on the connection, marks the connection as authenticated and registered, signals the chat service that the user has connected, and sends the welcome burst.
The method is a private helper used during the IRC session setup to ensure the connection transitions to a fully authenticated and registered state before normal chat activity begins.
---
## HandleWhoisAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleWhoisAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles an IRC WHOIS command by querying the target nick's user profile and returning the standard WHOIS information to the requester. It validates the connection is registered, extracts the nick from the message, fetches the user profile via _userService, and then dispatches a sequence of numeric replies: WHOIS user, WHOISSERVER, and optionally WHOISCHANNELS, RPL_AWAY if the user is away, and RPL_WHOISIDLE with idle and sign-on times, finishing with RPL_ENDOFWHOIS. If no profile exists for the nick, it replies with ERR_NOSUCHNICK. The method relies on asynchronous services and formats times using the profile's LastSeenAt and CreatedAt to populate idle and sign-on data.
Asynchronously completes a client's registration by deciding whether SASL authentication has already succeeded, or whether to perform PASS-based login to complete or create the user. If SASL is already authenticated ( `_conn.IsAuthenticated` and `_conn.UserId` is not null ), it marks the connection as registered, notifies the chat subsystem via [`UserConnectedAsync`](../EchoHub.Server/Services/ChatService.cs.md), and then triggers the welcome sequence with `SendWelcomeBurstAsync`. If not SASL-authenticated, it requires a password; if missing, it returns `ERR_PASSWDMISMATCH` and a generic error. Otherwise it calls `_userService.AuthenticateUserAsync(_conn.Nickname, _conn.Password)` and, on failure, falls back to `_userService.RegisterUserAsync(_conn.Nickname, _conn.Password)`; on success it binds the resulting user to the connection, updates `_conn.UserId`, `_conn.Nickname`, and flags `_conn.IsAuthenticated` and `_conn.IsRegistered`, then notifies the chat service and sends the welcome burst.
## Remarks
This method centralizes the WHOIS response logic for a given nickname, encapsulating the sequence of IRC numeric replies required to convey user information. It coordinates multiple collaborators (the connection, user service, and chat service) to assemble a consistent, standards-compliant response stream without leaking implementation details to callers. The precondition that the connection must be registered is enforced up front, ensuring WHOIS handling only occurs in an appropriate session context.
This method encapsulates the end-to-end registration/authentication handoff, coordinating between the connection state, the user service, and the chat subsystem. It guards against re-entrancy by exiting early when capability negotiation is in progress or the connection is already registered, and it ensures a consistent welcome sequence is delivered once authentication or registration succeeds.
## Notes
- If the target profile cannot be found, the handler emits ERR_NOSUCHNICK and aborts further replies.
- Idle time is calculated from LastSeenAt and sign-on time from CreatedAt; both are emitted via RPL_WHOISIDLE when available.
- The RPL_WHOISCHANNELS reply is sent only when the user belongs to one or more channels; otherwise this section is omitted.
- Away status (RPL_AWAY) is emitted only if the profile.Status is Away and a StatusMessage exists.
- Be mindful that the initial logging emits user-identifying state (e.g. `_conn.Nickname`, `_conn.Username`); ensure logging remains appropriate for your privacy and security policy.
---
@@ -8,28 +8,41 @@
```mermaid
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
flowchart TB
start(("Start")) --> checkOpt{"Check IrcOptions.Enabled?"}
checkOpt -- "false" --> logDisabled["Log #quot;IRC gateway is disabled#quot;"]
logDisabled --> end1(("End"))
checkOpt -- "true" --> init["Initialize listeners list and add RunListenerAsync(Options.Port, useTls:false)"]
init --> checkTls{"Are Options.TlsEnabled and Options.TlsCertPath set?"}
checkTls -- "true" --> addTls["Add RunListenerAsync(Options.TlsPort, useTls:true)"]
checkTls -- "false" --> awaitAll
addTls --> awaitAll["Await Task.WhenAll(listeners)"]
awaitAll --> runListener["RunListenerAsync: start TcpListener and loop AcceptTcpClientAsync"]
runListener --> acceptClient["On accept: fire-and-forget HandleClientAsync(tcpClient, useTls)"]
acceptClient --> runListener
acceptClient --> createConn["Create new IrcClientConnection and add to _connections"]
createConn --> handleClient{"HandleClientAsync: useTls?"}
handleClient -- "true" --> tlsHandshake["Load cert from IrcOptions and AuthenticateAsServerAsync"]
tlsHandshake --> handshakeOk{"TLS handshake succeeded?"}
handshakeOk -- "false" --> closeTcp["Log error and close tcpClient"]
closeTcp --> endConn(("End connection setup"))
handshakeOk -- "true" --> proceedConn["Assign SslStream and continue"]
handleClient -- "false" --> proceedConn
proceedConn --> addConn["Add connection to _connections dictionary (IrcClientConnection)"]
addConn --> startProcessing["Start message processing with IrcCommandHandler and required services (IChatService, IUserService, IChannelService, IMessageEncryptionService)"]
startProcessing --> endConn
Start["Start ExecuteAsync in IrcGatewayService"] --> CheckEnabled{"Check IrcOptions Enabled"}
CheckEnabled -->|"no"| LogDisabled["Log 'IRC gateway is disabled' and return"] --> End["End ExecuteAsync"]
CheckEnabled -->|"yes"| BuildListeners["Create listeners list"] --> AddPlainListener["Add RunListenerAsync for plain port (starts Task)"]
AddPlainListener -->|"starts Task"| RunListenerPlain["RunListenerAsync(port, useTls=false)"]
BuildListeners --> CheckTls{"TLS enabled and cert path set"}
CheckTls -->|"no"| WaitAll["Await Task.WhenAll(listeners)"] --> End
CheckTls -->|"yes"| AddTlsListener["Add RunListenerAsync for TLS port (starts Task)"]
AddTlsListener -->|"starts Task"| RunListenerTls["RunListenerAsync(port, useTls=true)"]
RunListenerPlain --> RunListenerCore
RunListenerTls --> RunListenerCore
RunListenerCore["RunListenerAsync body"] --> StartListener["Start TcpListener and log listening"]
StartListener --> RegisterCancel["Register ct to stop listener"] --> ListenerLoop{"ct.IsCancellationRequested"}
ListenerLoop -->|"no"| AcceptClient["AcceptTcpClientAsync"] --> SpawnHandle["Spawn HandleClientAsync(tcpClient, useTls) as fire and forget"] --> ListenerLoop
ListenerLoop -->|"yes"| StopListener["Stop listener and return from RunListenerAsync"]
SpawnHandle --> HandleClientStart["HandleClientAsync: get stream"] --> UseTls{"useTls"}
UseTls -->|"yes"| TLSHandshakeTry["Attempt TLS handshake"]
TLSHandshakeTry -->|"handshake failed"| TLSHandshakeFail["Log TLS handshake failed and close client, return"]
TLSHandshakeTry -->|"handshake succeeded"| AfterTls
UseTls -->|"no"| AfterTls["Proceed with plain stream"]
AfterTls --> CreateConnection["Create IrcClientConnection instance"] --> AddConnection["Add connection to _connections"] --> EndHandle["Return from HandleClientAsync"]
%% Simple getters
GetAll["GetAllConnections returns authenticated IrcClientConnection entries"]
GetInChannel["GetConnectionsInChannel(channelName) returns authenticated IrcClientConnection in channel"]
EndHandle --> End
LogDisabled --> End
StopListener --> End
AddConnection --> EndHandle
```
```csharp
@@ -37,12 +50,12 @@ public sealed class IrcGatewayService : BackgroundService
```
Provides a hosted IRC gateway that listens for incoming TCP (and optional TLS) client connections and dispatches each to an IrcCommandHandler that bridges IRC protocol traffic to the application's chat, user and channel services. Start this BackgroundService when you want the application to accept IRC client connections without manually managing TcpListeners, TLS handshakes, or per-connection handler wiring.
An always-on hosted gateway that accepts raw TCP (optionally TLS) connections and exposes an IRC-compatible surface backed by the EchoHub services. `IrcGatewayService` reads configuration from [`IrcOptions`](IrcOptions.cs.md), listens on the configured ports, accepts incoming `TcpClient` connections, wraps them in [`IrcClientConnection`](IrcClientConnection.cs.md) objects, and hands each connection to an [`IrcCommandHandler`](IrcCommandHandler.cs.md) that bridges IRC commands to the application services ([`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md), [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)). Reach for `IrcGatewayService` when you want to run an IRC-facing adapter for the EchoHub system rather than implementing socket handling and protocol dispatch yourself.
## Remarks
This BackgroundService reads configuration from IrcOptions and opens one or two listeners (plain and optionally TLS) for the ports configured. For every accepted TcpClient it creates an IrcClientConnection, stores it in an internal ConcurrentDictionary keyed by ConnectionId, and constructs an IrcCommandHandler (using IChatService, IUserService, IChannelService and IMessageEncryptionService from DI) to drive the connection. The service centralizes lifecycle concerns: listener startup/shutdown, TLS handshake and per-connection dispatching so higher-level application code can focus on chat/user/channel logic implemented in the injected services.
`IrcGatewayService` is a long-running `BackgroundService` that centralizes network-level concerns for the IRC gateway: socket listening, optional TLS handshake, acceptance of clients, and registration of active connections in the concurrent `_connections` map. It delegates protocol parsing and business-logic handling to [`IrcCommandHandler`](IrcCommandHandler.cs.md), resolving the required domain services from the DI `IServiceProvider` per connection so the gateway stays thin and focused on I/O and lifecycle. The service uses [`IrcOptions`](IrcOptions.cs.md) to control whether the gateway is enabled, which ports to bind, and whether to offer TLS; listeners are run as independent tasks and shut down when the host cancellation token is triggered.
## Notes
- If IrcOptions.Enabled is false the service logs and returns immediately; no listeners are started.
- TLS is only attempted when TlsEnabled is true and TlsCertPath is provided; TLS handshake failures are logged and the client connection is closed.
- The Connections collection is a ConcurrentDictionary and entries are added when clients connect. Public helper methods (GetAllConnections, GetConnectionsInChannel) filter by IrcClientConnection.IsAuthenticated — use those to obtain the set of active, authenticated clients rather than inspecting the raw dictionary directly.
- TLS requires a valid `IrcOptions.TlsCertPath` and password when `IrcOptions.TlsEnabled` is true; a failed TLS handshake will be logged and the connection closed (the code logs "TLS handshake failed" on exception).
- Active connections are tracked in the `ConcurrentDictionary` `_connections` and can be inspected via `GetConnectionsInChannel` and `GetAllConnections`; the dictionary makes concurrent adds/removes safe, but callers should expect the set to change while enumerating.
- The provided source was truncated inside `HandleClientAsync` in the task payload; I could not verify whether each [`IrcClientConnection`](IrcClientConnection.cs.md) is always removed from `_connections` and whether streams/clients are always disposed on disconnect. If you rely on deterministic cleanup, inspect the full `HandleClientAsync` implementation to confirm that connections are removed and resources are disposed on normal disconnect and on error.
@@ -8,10 +8,21 @@ public sealed class IrcMessage
```
IrcMessage is a parsed representation of an IRC protocol line that exposes the optional Prefix, the Command, and the Parameters that form the line's arguments; if a trailing payload is present, Trailing provides access to it. Use IrcMessage.Parse to convert a raw line into a structured object and inspect the command and its arguments without manual parsing.
IrcMessage is a parsed representation of a single IRC protocol line. It exposes an optional `Prefix`, the `Command`, and the list of `Parameters` extracted from the line, with `Trailing` representing the last parameter when present; use `Parse` to convert a raw IRC line into this structured form so you can inspect the command and its arguments without manual parsing.
## Remarks
IrcMessage encapsulates the parsing result and keeps IRC-logic separate from application code. Its properties are immutable (init-only), which makes parsed messages safe to share across components after parsing. Trailing is a derived convenience that reflects the trailing payload via the Parameters collection, aligning with the IRC grammar without introducing extra mutable state.
IrcMessage centralizes IRC line parsing by translating the textual format into explicit properties. The `Prefix` is optional, `Command` is the verb, and `Parameters` preserve order, with the final parameter commonly used as the trailing content in IRC messages. Accessing `Trailing` provides a convenient single point for the trailing payload without scanning the list; be mindful that `Parameters` is a `List<string>` and can be mutated if you obtain a reference.
## Example
```csharp
var line = ":server PRIVMSG #channel :Hello, world!";
var msg = IrcMessage.Parse(line);
var cmd = msg.Command; // "PRIVMSG"
var target = msg.Parameters[0]; // "#channel"
var trailing = msg.Trailing; // "Hello, world!"
```
## Notes
- Trailing property returns the last parameter when any parameters exist; it's a convenience for the trailing payload and assumes a leading ':' in the raw line to populate it. If there was no trailing parameter in the line, Trailing will reflect the final parameter but may not be semantically a trailing payload.
- The `Parameters` collection is a mutable `List<string>`; if you need a stable, immutable view, clone it before usage.
- If there are no parameters, `Trailing` will be `null`; the property simply reflects the last entry of `Parameters` when any parameters exist.
@@ -8,14 +8,4 @@ public static class IrcMessageFormatter
```
IrcMessageFormatter is a small, focused helper that converts a MessageDto into IRC PRIVMSG lines suitable for delivery in an IRC channel. It handles plain text and CTCP ACTION content, prefixes replies with the standard '> nick: snippet | ' format, and renders attachments as separate URL lines with concise type tags, using an absolute URL when a public base URL is supplied.
## Remarks
It centralizes the IRC-specific formatting and line-breaking logic used by the server when presenting messages to IRC clients, shielding callers from the quirks of the IRC protocol (such as per-line length limits and CTCP wrapping). The private FormatReplyPrefix creates a consistent context string for replies, including redaction of room ciphertext when needed and truncating long snippets to a safe length. Attachments are surfaced as individual lines with a small tag ([Image: ...], [Audio: ...], or [File: ...]) followed by an absolute URL, aligning with common IRC client behavior and improving link reliability. Embeds are appended using FormatEmbed, enabling rich previews where supported.
## Notes
- The FormatMessage path enforces line-length constraints via MaxIrcLineContentBytes, causing long content to be split across multiple PRIVMSG lines as needed.
- Encrypted-reply content is masked by the ciphertext-detection logic (e.g., [encrypted]) to avoid leaking room ciphertext in IRC.
- Absolute URL generation relies on ToAbsoluteUrl and the optional publicBaseUrl; without a base URL, attachments may render with their original (potentially relative) URLs.
IrcMessageFormatter formats a [`MessageDto`](../EchoHub.Core/DTOs/ChatDtos.cs.md) into IRC `PRIVMSG` lines for an EchoHub channel. It splits content into IRC-friendly chunks (up to 400 bytes per line), handles CTCP ACTION content, and prefixes replies with the compact ``> nick: snippet | `` prefix when a reply exists. Attachments are emitted as separate lines with absolute URLs (constructed from the optional `publicBaseUrl`) and labeled by kind (Image, Audio, or File). If present, embeds are appended via the embed formatter. When a reply references encrypted room content, the snippet is shown as `[encrypted]` and non-encrypted snippets are truncated to 80 characters to fit IRC constraints.
@@ -8,17 +8,11 @@ public static class IrcNumericReply
```
IrcNumericReply is a static container of string constants that encode the standard IRC protocol numeric replies. It centralizes the protocols numeric codes so developers can reference them by name (e.g., RPL_WELCOME, ERR_UNKNOWNCOMMAND) instead of sprinkling literal strings throughout the codebase. The constants are organized by functional areas such as registration, MOTD, channel operations, list operations, WHO/WHOIS, away status, mode, errors, and SASL.
IrcNumericReply is a centralized, static container of IRC protocol numeric reply codes represented as strings. It defines constants for common server replies and errors, organized by category (Connection registration, MOTD, Channel operations, LIST, WHO/WHOIS, AWAY, MODE, Errors, SASL). Developers reference these constants, such as `IrcNumericReply.RPL_WELCOME` or `IrcNumericReply.ERR_NOSUCHNICK`, when constructing or interpreting IRC protocol messages instead of hard-coding literals. This reduces repetition, prevents typos, and makes maintenance safer if the IRC spec evolves or expands the set of recognized replies.
## Remarks
Having all codes in one static class provides a single source of truth and makes it straightforward to update or extend the set as the IRC spec evolves. It also clarifies intent at call sites: emitting an IRC reply uses the corresponding constant rather than a magic string, and parsing branches can compare against these constants with confidence. This abstraction keeps server and client code aligned on canonical codes without duplicating literals.
## Example
```csharp
// Example: emit a welcome reply using the canonical code
string code = IrcNumericReply.RPL_WELCOME; // "001"
string reply = $":server {code} Welcome to the IRC network";
```
IrcNumericReply provides a canonical reference for IRC numeric codes, solving the problem of scattered, magic string literals across message handling, parsing, and logging. It fits with any component that reads or writes server messages, allowing consistent checks for `IrcNumericReply.RPL_WELCOME` and other replies without duplicating numeric literals.
## Notes
- The constants are strings, not integers; avoid parsing them as numbers if you need to preserve leading zeros (e.g., "001").
- The constants are string values representing the IRC wire codes; use `IrcNumericReply.*` wherever you compare or emit these codes to avoid accidental mismatches.
- This class contains no behavior beyond constants; place any parsing or dispatch logic elsewhere.
@@ -8,12 +8,7 @@ public sealed class IrcOptions
```
IrcOptions is a simple configuration container that aggregates the settings controlling EchoHub's IRC bridge. It exposes toggles and values for enabling IRC, selecting ports for non-TLS and TLS connections, TLS certificate details, the server identity, an optional MOTD, and how attachment URLs are resolved via a public base URL. An application binds this object from configuration to influence how the IRC integration is started and how clients connect securely.
IrcOptions is a lightweight configuration container for the EchoHub IRC integration. It groups together all IRC-related settings that govern whether the IRC feature is active, which ports to listen on for plain and TLS connections, optional TLS credentials, the IRC server identity, an optional Motd, and how attachment URLs are resolved for IRC clients. This class is typically populated from the `Irc` configuration section (as indicated by the `SectionName` constant) and consumed by the startup logic that initializes the IRC subsystem, allowing developers to tailor IRC behavior without touching runtime code.
## Remarks
This class acts as a plain data container that centralizes IRC-related settings, separating configuration concerns from connection logic. The SectionName constant indicates the configuration section used when binding settings, while PublicBaseUrl affects how attachment URLs are translated for IRC clients—absolute URLs when set, otherwise relative paths. It is designed to be a simple DTO bound from configuration rather than responsible for validation or side effects.
## Notes
- If TLS is enabled but a certificate path or password is missing or invalid, TLS connections may fail; ensure a valid certificate and credentials are supplied when TlsEnabled is true.
- PublicBaseUrl, when set, makes attachment URLs absolute for IRC clients; if left unset, attachment lines fall back to the relative path.
- The defaults describe typical behavior: Port = 6667, TlsPort = 6697, and ServerName = "echohub".
IrcOptions is a pure data carrier with defaults that reflect common IRC conventions: `Port` defaults to 6667, `TlsPort` to 6697, and [`ServerName`](IrcCommandHandler.cs.md) to `echohub`. TLS-related fields (`TlsEnabled`, `TlsPort`, `TlsCertPath`, `TlsCertPassword`) indicate TLS support is optional and configured here; the runtime code uses these values to establish TLS-protected connections when enabled. The `PublicBaseUrl` property governs how attachment URLs are rendered for IRC clients: when set, it converts relative paths to absolute links using the provided base URL; when unset, attachments fall back to their relative paths. The `Motd` field exposes an optional IRC message of the day that can be surfaced to connected clients if the IRC subsystem is started.
@@ -8,12 +8,11 @@ public static class IrcServiceExtensions
```
Extends WebApplicationBuilder with AddIrcGateway to wire up IRC gateway support. It reads a configuration flag to enable or disable the gateway and wires the necessary services when enabled, returning the builder for fluent startup configuration.
Extends `WebApplicationBuilder` with `AddIrcGateway` to wire IRC gateway support into an ASP.NET Core app. It configures [`IrcOptions`](IrcOptions.cs.md) from configuration and, when `Irc:Enabled` is true, registers [`IrcGatewayService`](IrcGatewayService.cs.md) as a singleton, wires [`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) to [`IrcBroadcaster`](IrcBroadcaster.cs.md), and adds the gateway as a hosted service, returning the original builder for fluent chaining.
## Remarks
Centralizes startup concerns for the IRC gateway: the extension reads IrcOptions from a configured section and conditionally registers the gateway components, enabling the feature via configuration. It keeps startup code concise and tests-focused by encapsulating the wiring behind a single extension method.
This extension encapsulates opt-in startup logic and centralizes the wiring of the IRC gateway, ensuring consistent DI lifetimes and configuration handling across the app. It coordinates the lifecycle of [`IrcGatewayService`](IrcGatewayService.cs.md) and the broadcaster ([`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implemented by [`IrcBroadcaster`](IrcBroadcaster.cs.md)) by hosting the gateway as a background service.
## Notes
- IrcGatewayService and IrcBroadcaster registrations are conditional on Irc:Enabled; if false, IRC components are not registered.
- Ensure IrcOptions.SectionName matches your configuration so there is a valid section to bind from.
- Returning the builder enables fluent chaining like builder.AddIrcGateway().<other extensions>()
- Calling `AddIrcGateway` multiple times can register multiple hosted services and singletons; call it once during startup to avoid duplicate registrations.
- The extension only activates when `Irc:Enabled` is true. If the flag is false or missing, it will configure [`IrcOptions`](IrcOptions.cs.md) but will not start or register the gateway components. Ensure configuration sources are loaded before invocation.
@@ -8,11 +8,4 @@ public class JwtTokenService
```
JwtTokenService centralizes the creation of JSON Web Tokens used for authenticating API requests. It reads the signing secret, issuer, and audience from configuration and exposes two overloads of GenerateAccessToken for User and UserProfileDto, returning the token string along with its expiration timestamp. Each generated token includes standard claims such as sub (the user/profile id), username, display_name (falling back to username if not provided), role, and a unique jti, and is signed with HmacSha256 using the configured secret. Access tokens expire after 15 minutes, while a companion refresh token can be generated with GenerateRefreshToken and hashed with HashToken for secure storage.
## Remarks
JwtTokenService centralizes token creation, ensuring consistent signing, claims, and expiry semantics across authentication flows. By loading Jwt:Secret, Jwt:Issuer, and Jwt:Audience from configuration in one place, it reduces the risk of mismatched values and scattered configuration access. The two overloads for GenerateAccessToken allow tokens to be produced from either a User or a UserProfileDto while preserving a uniform JWT shape and claims set, including a unique jti for traceability.
## Notes
- If Jwt:Secret, Jwt:Issuer, or Jwt:Audience is missing from configuration, the constructor throws an InvalidOperationException with a clear message, preventing startup with a misconfigured token engine.
- GenerateRefreshToken produces a cryptographically random 64-byte value and returns it as a base64 string; HashToken provides a SHA-256-based digest suitable for secure, persisted storage. The class itself does not persist refresh tokens, so you should implement storage and revocation logic in your authentication flow if needed.
JwtTokenService centralizes creation of JWTs for API authentication: it issues short-lived access tokens via `GenerateAccessToken` (from a [`User`](../../EchoHub.Core/Models/User.cs.md) or a [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md)) and provides a cryptographically secure refresh token generator via `GenerateRefreshToken` (and `HashToken` to store a hashed form). It reads its secret, issuer, and audience from configuration and enforces token lifetimes defined by `AccessTokenLifetime` and `RefreshTokenLifetime`.
@@ -8,4 +8,11 @@ public sealed class ServerLogsOptions
```
Live server-log room configuration is encapsulated by this strongly-typed options class. It binds to the ServerLogs config section and supports environment overrides, controlling whether the live streaming channel is created and who can view it. When Enabled is true, a read-only system channel is auto-created and log events are streamed in real time; log lines themselves are not stored as messages in the database, with persistence remaining in the rolling Serilog log files.
`ServerLogsOptions` is a configuration model that binds to the `ServerLogs` configuration section (and supports environment overrides via keys like `ServerLogs__Enabled`). When `Enabled` is true, the system automatically creates a read-only system channel named from `RoomName` (default `server-logs`) and streams log events to that channel in real time; log lines are never persisted as messages in the database, with persistence limited to the rolling `Serilog` log files. It exposes several tunables: `RoomName` sets the auto-created channel name and must satisfy the normal channel-name rules; the name is reserved so users cannot create a channel with it. `MinRole` defines the minimum server role that can see and join the log room (default `ServerRole.Mod`); `MinLevel` selects the minimum log event level to stream (default `LogEventLevel.Information`); `BacklogLines` controls how many recent log entries are replayed from the log files when the room is opened (default 100); `LogDirectory` and `LogFilePattern` point to where the rolling log files live and how they are named (defaults `logs` and `echohub-server-*.log`). The derived `NormalizedRoomName` provides a lowercased, trimmed variant of `RoomName` for comparisons.
## Remarks
The `ServerLogsOptions` abstraction centralizes live-log streaming behind a configuration object, separating real-time visibility from persistent message storage. It ensures a consistent, auto-created channel for server logs (named by `RoomName`, default `server-logs`) and uses `MinRole`/`MinLevel` to control who and what they can see, without requiring code changes to enable the feature. The `NormalizedRoomName` aids robust comparisons elsewhere in the system.
## Notes
- Live-streaming may expose sensitive information; ensure `MinRole` and `MinLevel` align with privacy expectations.
- Backlog replay relies on the Serilog file sink configuration; ensure `LogDirectory` exists and matches `LogFilePattern`.
@@ -8,12 +8,30 @@ public sealed class SpamOptions
```
SpamOptions is a configuration object that encapsulates the anti-spam thresholds used by the server. It is bound from the Spam config section and exposes the toggles and numeric limits that govern how the system enforces per-user rate limits, duplicate message handling, auto-muting behavior, and the protections around first-time channel joins and channel creation. The defaults are intentionally lenient so a fast typist wont trip them, and moderators (and above) are exempt from these protections. Use this class to adjust spam-protection policy without changing code.
SpamOptions is a configuration class bound to the 'Spam' config section that stores all anti-spam thresholds. It centralizes rate limits, duplicate suppression, auto-mute behavior, and onboarding quotas so enforcement logic can apply consistent rules; adjust these values here rather than hard-coding them throughout.
## Remarks
SpamOptions centralizes policy decisions for anti-spam enforcement, serving as a single source of truth for the thresholds consumed by the spam protection subsystem. By binding to configuration, it keeps rules out of hard-coded logic and enables runtime tuning via the Spam section. The design separates concerns across rate limiting (per-user messages), duplicate detection, auto-mute behavior, and early channel-join/channel-create protections, making it easier to tune each facet without collateral impact. The auto-mute behavior ties into the existing moderation tooling (MuteExpirationService), illustrating cohesive behavior with the broader user-suspension lifecycle. The note about end-to-end encrypted rooms clarifies that identical plaintext can yield different ciphertext, so the duplicate-detection rule may not apply in those contexts.
SpamOptions acts as the configuration contract for anti-spam behavior. It centralizes all thresholds so the enforcement and moderation subsystems can apply consistent rules without hard-coded values scattered through the codebase. It coordinates rate limiting, duplicate suppression, auto-mute behavior, and first-join/channel-creation limits via a single, testable object that can be configured at startup.
## Example
```csharp
var options = new SpamOptions
{
Enabled = true,
MaxMessagesPerWindow = 12,
WindowSeconds = 10,
MaxDuplicateMessages = 2,
AutoMuteMinutes = 10,
ViolationThreshold = 6,
ViolationWindowMinutes = 3,
MaxJoinsPerWindow = 30,
JoinWindowSeconds = 20,
MaxChannelCreatesPerWindow = 2,
ChannelCreateWindowMinutes = 15
};
```
## Notes
- Auto-mute is controlled by AutoMuteMinutes. Setting AutoMuteMinutes to 0 disables auto-mute (rejections still apply if thresholds are reached).
- MaxMessagesPerWindow and WindowSeconds govern per-user message rate; adjust them with awareness of your typical user pacing to avoid false positives.
- MaxJoinsPerWindow and JoinWindowSeconds apply to first-time channel joins; joins to channels the user already belongs to do not count toward the limit, ensuring normal reconnects dont trigger protections.
- Auto-mute is disabled when `AutoMuteMinutes` is 0; rejections still apply.
- The first-join burst behavior relies on `MaxJoinsPerWindow` being large enough for your public channel count.
- These values are loaded from the config and may be adjusted to balance user experience against protection needs.
@@ -8,12 +8,7 @@ public sealed class StatsOptions
```
StatsOptions is a bound configuration object that governs the periodic server-stats reporter. When Enabled is true, a background job periodically snapshots server activity, logs the snapshot as pretty-printed JSON, and persists it to the database; IntervalHours controls cadence, and RetentionDays controls how long reports are kept. The environment override Stats__Enabled allows turning the reporter on or off via environment configuration without changing code.
StatsOptions is a configuration-bound class that governs the periodic server-stats reporting behavior of the application. It binds from the Stats config section (with environment overrides like Stats__Enabled) and, when Enabled is true, drives a background job that periodically snapshots server activity, logs the snapshot as pretty-printed JSON, and persists it to the database. Developers would adjust IntervalHours to change how often reports are generated and RetentionDays to control how long reports are kept, or toggle Enabled to enable/disable the reporting; defaults are Enabled = true, IntervalHours = 6, and RetentionDays = 90.
## Remarks
StatsOptions serves as a simple, sealed data contract that the configuration system binds to at startup, providing a single source of truth for the reporter settings. Centralizing these knobs here avoids scattering config keys throughout the code and makes it easy to swap configuration providers or add validation in one place. The defaults (Enabled = true, IntervalHours = 6, RetentionDays = 90) define the out-of-the-box behavior and can be overridden by environment or configuration.
## Notes
- RetentionDays: 0 means keep reports indefinitely; any positive number prunes older entries.
- IntervalHours is a double; fractional values (e.g., 1.5) are allowed, but scheduling resolution depends on the hosting environment.
- Enabled acts as the master switch for the background job; disabling it stops snapshots until re-enabled.
This class serves as the configuration object consumed by the background stats collection service, isolating configuration from implementation and enabling the Stats job to be controlled entirely via config.
@@ -8,24 +8,21 @@ public sealed class UploadLimits
```
UploadLimits is a configuration-bound value object that centralizes the admin-defined upload size caps. It reads sizes in megabytes from the Uploads configuration and exposes corresponding byte-sized properties used during enforcement. When the Uploads section is missing or incomplete, the defaults mirror HubConstants to preserve the historical built-in limits.
UploadLimits provides the admin-configurable ceilings for uploads, bound from the `Uploads` configuration section and converted to bytes for enforcement. Values are expressed in megabytes in configuration and exposed as byte-based properties for the enforcement layer; if the `Uploads` section is absent or partial, defaults mirror [`HubConstants`](../../EchoHub.Core/Constants/HubConstants.cs.md) to preserve historical limits.
The class exposes MB-based properties for each category (MaxFileSizeMB, MaxImageSizeMB, MaxAudioSizeMB, MaxAvatarSizeMB) and a per-message attachment cap (MaxAttachmentsPerMessage). It also exposes computed byte-based counterparts (MaxFileSizeBytes, MaxImageSizeBytes, MaxAudioSizeBytes, MaxAvatarSizeBytes) derived from the MB properties. The per-kind limit is exposed via `MaxForKind(AttachmentKind)`, which returns the corresponding byte limit for images, audio, or the general file size for other kinds. Finally, `MaxRequestBodyBytes` represents the absolute ceiling for a single message request body, calculated as `MaxFileSizeBytes * MaxAttachmentsPerMessage`, ensuring that increased configuration actually scales the request payload footprint.
## Remarks
UploadLimits centralizes the policy governing uploads (files, images, audio, avatars) and the maximum number of attachments per message. The MB-based properties feed their byte-sized counterparts (MaxFileSizeBytes, MaxImageSizeBytes, etc.) for enforcement. MaxForKind provides a per-kind ceiling, while MaxRequestBodyBytes computes the overall request-body cap (largest file size multiplied by the attachment limit) to ensure configuration changes actually take effect at the HTTP boundary.
UploadLimits serves as a focused bridge between configuration and enforcement. By centralizing unit conversion (MB to bytes) and collating per-kind and per-message constraints, it reduces the risk of inconsistent bounds across the upload pipeline and makes it straightforward to adjust limits in one place. The design anticipates future extension to additional attachment kinds without altering enforcement sites, while preserving backward-compatible defaults when the configuration is incomplete.
## Example
```csharp
var limits = new UploadLimits
{
MaxFileSizeMB = 64,
MaxAttachmentsPerMessage = 4
};
long maxImageBytes = limits.MaxImageSizeBytes;
long imageCeiling = limits.MaxForKind(AttachmentKind.Image);
long requestBody = limits.MaxRequestBodyBytes;
var limits = new UploadLimits();
long imageBytes = limits.MaxImageSizeBytes;
long imageCapForKind = limits.MaxForKind(AttachmentKind.Image);
```
## Notes
- Changing MaxAttachmentsPerMessage scales the MaxRequestBodyBytes non-linearly; the request-body cap will constrain multipart uploads even if per-file size increases.
- Defaults are tied to HubConstants; if those constants change, the default limits change too unless overridden in the Uploads configuration.
- The `MaxRequestBodyBytes` computation ties the per-attachment cap to the file-size ceiling, so increasing `MaxAttachmentsPerMessage` scales the maximum allowed request body accordingly.
- All byte-based properties are derived from their MB counterparts, so changes to the configuration flow through to enforcement automatically.
- If [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md) includes kinds beyond Image and Audio, those other kinds fall back to the general `MaxFileSizeBytes` in `MaxForKind`.
@@ -11,11 +11,4 @@ public class AuthController : ControllerBase
```
AuthController is the API surface that coordinates user authentication. It exposes endpoints for registering, logging in, refreshing tokens, and logging out under /api/auth, and ties together user management, JWT token generation, and refresh-token persistence.
## Remarks
AuthController centralizes authentication concerns to enable consistent security policies such as token lifetimes and rotation. It orchestrates between user management (IUserService), token generation (JwtTokenService), and persistence of refresh tokens (EchoHubDbContext), including rotation semantics to revoke old tokens on each refresh.
## Notes
- Refresh token rotation: on a successful refresh, the old token is revoked (RevokedAt is set) and a new token pair is issued. Clients should replace the old token with the new one and avoid reusing the former.
- Security handles: access tokens have shorter lifetimes, refresh tokens are hashed in storage, and all token exchanges occur over HTTPS. Treat tokens as highly sensitive data and store them securely on the client side.
AuthController is an API controller that hosts authentication endpoints under `/api/auth`, handling user registration, login, token refresh, and logout. It coordinates user management via [`IUserService`](../../EchoHub.Core/Contracts/IUserService.cs.md), issues access tokens with [`JwtTokenService`](../Auth/JwtTokenService.cs.md), and persists refresh tokens through the application's EF Core context [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md).

Some files were not shown because too many files have changed in this diff Show More