diff --git a/docs/auriondocs/Agent/repo-map.md b/docs/auriondocs/Agent/repo-map.md index 04dc773..9558732 100644 --- a/docs/auriondocs/Agent/repo-map.md +++ b/docs/auriondocs/Agent/repo-map.md @@ -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` | diff --git a/docs/auriondocs/Agent/symbol-graph.json b/docs/auriondocs/Agent/symbol-graph.json index 6831c4c..847d05c 100644 --- a/docs/auriondocs/Agent/symbol-graph.json +++ b/docs/auriondocs/Agent/symbol-graph.json @@ -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, diff --git a/docs/auriondocs/Code/index.md b/docs/auriondocs/Code/index.md index 4e3f006..07cf01d 100644 --- a/docs/auriondocs/Code/index.md +++ b/docs/auriondocs/Code/index.md @@ -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 diff --git a/docs/auriondocs/Code/src/EchoHub.Client/AppOrchestrator.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/AppOrchestrator.cs.md index 93e42c6..370f15f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/AppOrchestrator.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/AppOrchestrator.cs.md @@ -13,10 +13,17 @@ - [BuildOutgoingAttachmentAsync](#buildoutgoingattachmentasync) - [CleanupPastedTempFiles](#cleanuppastedtempfiles) - [ClearPendingReply](#clearpendingreply) + - [ClearSavedToken](#clearsavedtoken) + - [DedupPath](#deduppath) - [Dispose](#dispose) + - [DownloadAttachmentAsync](#downloadattachmentasync) - [EnsureRoomUnlockedForSendAsync](#ensureroomunlockedforsendasync) + - [FetchAndUpdateOnlineUsers](#fetchandupdateonlineusers) + - [GetDownloadDir](#getdownloaddir) + - [HandleAudioPlayRequested](#handleaudioplayrequested) - [HandleChannelJoinFromMessage](#handlechanneljoinfrommessage) - [HandleChannelSelected](#handlechannelselected) + - [HandleCheckForUpdatesRequested](#handlecheckforupdatesrequested) - [HandleCmdAssignRole](#handlecmdassignrole) - [HandleCmdBanUser](#handlecmdbanuser) - [HandleCmdChangeRoomPassword](#handlecmdchangeroompassword) @@ -32,6 +39,7 @@ - [HandleCmdMeta](#handlecmdmeta) - [HandleCmdMuteUser](#handlecmdmuteuser) - [HandleCmdNukeChannel](#handlecmdnukechannel) + - [HandleCmdOpenProfile](#handlecmdopenprofile) - [HandleCmdOpenServers](#handlecmdopenservers) - [HandleCmdQuit](#handlecmdquit) - [HandleCmdRevokeInvite](#handlecmdrevokeinvite) @@ -41,6 +49,7 @@ - [HandleCmdSetAsciiSize](#handlecmdsetasciisize) - [HandleCmdSetAvatar](#handlecmdsetavatar) - [HandleCmdSetColor](#handlecmdsetcolor) + - [HandleCmdSetDownloadPath](#handlecmdsetdownloadpath) - [HandleCmdSetNick](#handlecmdsetnick) - [HandleCmdSetStatus](#handlecmdsetstatus) - [HandleCmdSetTheme](#handlecmdsettheme) @@ -50,18 +59,24 @@ - [HandleCmdUnmuteUser](#handlecmdunmuteuser) - [HandleConnect](#handleconnect) - [HandleCreateChannelRequested](#handlecreatechannelrequested) + - [HandleDeleteChannelRequested](#handledeletechannelrequested) - [HandleDeleteMessageRequested](#handledeletemessagerequested) - [HandleDisconnect](#handledisconnect) - [HandleEditProfile](#handleeditprofile) + - [HandleFileDownloadRequested](#handlefiledownloadrequested) - [HandleFilesStaged](#handlefilesstaged) + - [HandleImageOpenRequested](#handleimageopenrequested) - [HandleImagePasted](#handleimagepasted) + - [HandleImageSaveRequested](#handleimagesaverequested) - [HandleLoadMoreRequested](#handleloadmorerequested) - [HandleLogout](#handlelogout) - [HandleMessageSubmitted](#handlemessagesubmitted) - [HandleProfileRequested](#handleprofilerequested) - [HandleReplyCancelRequested](#handlereplycancelrequested) - [HandleReplyRequested](#handlereplyrequested) + - [HandleRollbackRequested](#handlerollbackrequested) - [HandleSavedServersRequested](#handlesavedserversrequested) + - [HandleSearchRequested](#handlesearchrequested) - [HandleStatusRequested](#handlestatusrequested) - [HandleThemeSelected](#handlethemeselected) - [HandleViewProfile](#handleviewprofile) @@ -69,17 +84,21 @@ - [JoinChannelWithPasswordPromptAsync](#joinchannelwithpasswordpromptasync) - [NeedsUnlockPrompt](#needsunlockprompt) - [NormalizeAsciiSize](#normalizeasciisize) + - [PersistLastReads](#persistlastreads) + - [PromptPassword](#promptpassword) + - [RefreshStagingTray](#refreshstagingtray) - [RunAsync](#runasync) + - [SaveServerToConfig](#saveservertoconfig) - [SendStagedMessage](#sendstagedmessage) + - [SetDownloadPath](#setdownloadpath) + - [UnlockRoomKeyAsync](#unlockroomkeyasync) - [UnlockTrackedChannelAsync](#unlocktrackedchannelasync) + - [UpdateServerConfig](#updateserverconfig) - [WireCommandHandlerEvents](#wirecommandhandlerevents) - [WireConnectionManagerEvents](#wireconnectionmanagerevents) - [WireMainWindowEvents](#wiremainwindowevents) -- [HandleCmdOpenProfile](#handlecmdopenprofile) -- [HandleSearchRequested](#handlesearchrequested) -- [PromptPassword](#promptpassword) -- [RefreshStagingTray](#refreshstagingtray) -- [UnlockRoomKeyAsync](#unlockroomkeyasync) + - [ImageOpenExtensions](#imageopenextensions) + - [SafeOpenExtensions](#safeopenextensions) --- @@ -92,17 +111,16 @@ public sealed class AppOrchestrator : IDisposable ``` -Central coordinator for the EchoHub terminal UI client: it wires MainWindow UI events and command input to the underlying services (connection manager, message manager, audio/update services, etc.), maintains ephemeral UI-facing state (staged attachments, per-channel user lists, temporary pasted files, declined E2E unlocks, pending reply targets), and exposes a small host-facing surface (MainWindow and PendingUpdate). Reach for AppOrchestrator when you need a single place to orchestrate cross-cutting app behavior rather than wiring UI components and services together manually. +Central coordinator that wires the Terminal UI to the client services and connection layer. Reach for `AppOrchestrator` when hosting the EchoHub TUI: it attaches `MainWindow` events to service calls, routes incoming connection events back into the UI, manages message staging/attachments, and exposes lifecycle hooks such as `PendingUpdate` and `Dispose` so the host can integrate cleanly with the application loop. ## Remarks -AppOrchestrator exists to centralize responsibilities that span UI, networking and local client state so the rest of the codebase can remain focused: UI widgets raise events and the orchestrator translates those into service calls, and connection/service events are translated back into UI updates. It owns short-lived caches (case-insensitive channel user lists), staging buffers (attachments and temporary pasted files), and user-interaction policies (for example, remembering which E2E unlock prompts were declined so the user isn't repeatedly nagged). It also exposes PendingUpdate so the host can perform an in-place restart safely after the TUI's main loop exits. +`AppOrchestrator` exists to decouple the UI surface (`MainWindow`) from the lower-level services ([`ConnectionManager`](Services/ConnectionManager.cs.md), `ChatMessageManager`, [`UpdateChecker`](Services/UpdateChecker.cs.md), [`NotificationSoundService`](Services/NotificationSoundService.cs.md), [`AudioPlaybackService`](Services/AudioPlaybackService.cs.md), etc.). It centralizes event wiring and background task scheduling (via the `RunAsync` helper) so UI code can remain thin and reactive while the orchestrator handles async operations, error reporting, and state such as `_channelUsers`, `_stagedAttachments`, and `_tempPastedFiles`. The class exposes `MainWindow` and `PendingUpdate` so the host can present the TUI and then perform post-loop actions (for example applying an in-place updater) after the terminal is restored. ## Notes -- PendingUpdate must be executed by the host only after the Terminal.Gui main loop and TUI have exited (console restored); running the updater while the TUI is still active can conflict with terminal state and in-place restart behavior. -- Temporary PNG files created for clipboard-image pastes are tracked and removed when their message is sent or when the staging tray is cleared—do not assume pasted screenshots persist on disk indefinitely. -- The channel user cache is case-insensitive (StringComparer.OrdinalIgnoreCase) and is guarded by an internal lock; callers should not bypass the orchestrator to modify that state directly. - - +- Call `Dispose` when shutting down the TUI so the orchestrator can perform its teardown work (the source notes it should "capture read positions before tearing down"). +- `PendingUpdate` is intended to be executed after the Terminal.Gui main loop exits (the host must run it once the terminal is restored); running an updater while the TUI is still active can conflict with the console state. +- The class tracks temporary pasted files in `_tempPastedFiles` and staged attachments in `_stagedAttachments`; these are cleaned when messages are sent or the staging tray is cleared — failing to clear them may leave temporary files in the system temp directory. +- Threading: use `InvokeUI` to marshal UI updates back onto the `IApplication` thread and prefer the orchestrator's `RunAsync` helper for fire-and-forget background work to avoid blocking the UI thread. --- @@ -122,15 +140,13 @@ public AppOrchestrator(IApplication app, ClientConfig config) | `config` | [`ClientConfig`](Config/ClientConfig.cs.md) | — | -The AppOrchestrator constructor initializes the client by capturing the application context and configuration, creating core UI and service collaborators, wiring their inter-component events, and starting the update checker. It then initializes the main window with a disconnected state, readying the app for user interaction. +Initializes the application orchestration by constructing the core UI and services from the provided `IApplication` and [`ClientConfig`](Config/ClientConfig.cs.md), creating `ChatMessageManager`, `MainWindow`, `CommandHandler`, [`NotificationSoundService`](Services/NotificationSoundService.cs.md), and [`UpdateChecker`](Services/UpdateChecker.cs.md), wiring their events, starting the update checker, and finally setting the initial status to Disconnected. ## Remarks -As the composition root for the EchoHub client, AppOrchestrator centralizes the creation and wiring of the UI and service layer. It ensures that all collaborators exist before the app becomes interactive and that the event pipelines are in place so user actions, commands, and connection state changes flow through a single, predictable lifecycle. The initial 'Disconnected' status communicates to users that the app is not yet connected, and will transition as connections and updates occur. +AppOrchestrator acts as the composition root for the client, ensuring that UI and operational subsystems are created with the correct dependencies and interconnected before the application becomes interactive. By wiring event handlers before starting background services, it centralises lifecycle management and guarantees predictable startup sequencing. ## Notes -- The constructor immediately starts the update checker via Start(); this can trigger asynchronous network activity during startup. -- The initialization sequence assumes non-null app and config; null values will throw during assignment. -- Be mindful of multiple AppOrchestrator instances: event subscriptions are established in the constructor and may accumulate if the object is created more than once. +- Startup work runs on the creating thread; long-running initialization (such as the work kicked off by `UpdateChecker.Start()`) may block startup in some hosting environments. Consider ensuring the constructor runs in a context that allows background work to proceed without delaying UI readiness. --- @@ -143,15 +159,7 @@ public MainWindow MainWindow => _mainWindow ``` -Exposes the application's main window as a read-only property. It returns the private _mainWindow field, allowing callers to access the current MainWindow instance without exposing a setter or the backing field directly. Use this property when UI or orchestration code needs to interact with the main window in a controlled way, e.g., to trigger window-bound actions or dialogs, while preserving encapsulation. - -## Remarks -This property serves as an abstraction boundary between the app orchestrator and the UI layer. It decouples consumers from the concrete storage of the main window and provides a stable access point that can be replaced or mocked in tests without changing call sites. It also communicates ownership: the orchestrator owns and manages the MainWindow reference. If initialization order guarantees the value, callers can rely on its presence; otherwise, guard against nulls. - -## Notes -- Access before initialization may yield null; ensure initialization before first use. -- If the main window can be swapped at runtime (for testing or multi-window scenarios), consider adding a controlled mechanism to rebind the reference or introduce an interface the rest of the code depends on. -- This property is a simple passthrough of the backing field; for testability, consider introducing an abstraction (e.g., IMainWindow) if you need to mock interactions with the window. +Read-only accessor `MainWindow` exposes the underlying `_mainWindow` instance to callers. It provides a convenient way to access the application's main window from the `AppOrchestrator` without exposing the backing field directly, keeping encapsulation intact while enabling coordinated UI interactions. --- @@ -164,25 +172,13 @@ public Func? PendingUpdate => _updateService.PendingUpdate ``` -PendingUpdate is a nullable `Func` that becomes non-null once the user confirms an in-app update. The host should invoke this function after the Terminal.Gui main loop exits and the console has been restored, allowing the updater to restart in-place without fighting the TUI. This property simply proxies the update action from the update service, decoupling the UI lifecycle from the restart logic. +PendingUpdate is a nullable `Func` that becomes set when the user confirms an update. The host should invoke this delegate after the `Terminal.Gui` main loop exits to perform the updater's in-place restart without fighting the TUI, since it simply forwards to `_updateService.PendingUpdate`. ## Remarks -PendingUpdate exists to separate the moment of user confirmation from the actual restart work. It ensures the update runs after the UI has been torn down, avoiding conflicts with the Terminal.Gui lifecycle. By delegating to the update service's PendingUpdate, the hosting code remains agnostic of the specifics of how updates are performed. - -## Example -```csharp -// After the Terminal.Gui loop completes and the UI is torn down -var pending = appOrchestrator.PendingUpdate; -if (pending != null) -{ - await pending.Invoke(); -} -``` +PendingUpdate acts as a thin bridge between the UI decision and the update lifecycle. By exposing the updater hook via `_updateService.PendingUpdate`, the design keeps the UI layer decoupled from the restart mechanics while ensuring the host sequences console restoration prior to starting the update. ## Notes -- PendingUpdate may be null; guard against null before invoking. -- Because it returns a Task, await the invocation to ensure the update process completes (as appropriate for your app lifecycle). -- Call this only after the UI teardown to avoid interfering with the TUI. +- Check for null before invocation; the property is nullable and may be absent if no update is pending. --- @@ -203,38 +199,7 @@ private void ApplyAsciiSize(string flag) **Returns:** `void` -Applies a user-selected ASCII size by updating the in-memory configuration, persisting it to disk, and refreshing the staging UI. If a current channel is active, it also broadcasts a system message to inform users that the image ASCII size has been updated, formatting the value via AsciiSizeLabel(flag). - -## Remarks -This method centralizes the side effects of changing the ASCII size: it updates the config, persists the change, updates the staging tray, and notifies the channel. It relies on AsciiSizeLabel to render a user-friendly label and on ConfigManager.Save to persist settings; callers should be aware exceptions from these operations propagate to the caller. - -## Notes -- Assumes _config is non-null; a null _config would cause a NullReferenceException. -- No input validation: the flag string is stored directly and passed to AsciiSizeLabel; ensure downstream code handles invalid values. -- Synchronous IO: ConfigManager.Save(_config) may block if disk IO is slow; consider invoking from a background task if called from UI event handlers. - -## Dependencies -- ConfigManager - -## Dependency APIs (verified signatures) - -- class [`ConfigManager`](Config/ConfigManager.cs.md) (`src/EchoHub.Client/Config/ConfigManager.cs`) - - field `string ConfigDir` - - field `string ConfigPath` - - property `string ConfigDirectory` - - field `Lock FileLock` - - field `JsonSerializerOptions JsonOptions` - - `ClientConfig Load()` - - `void Save(ClientConfig config)` - - `void SaveServer(SavedServer server)` - - `void RemoveServer(string url)` - -## Symbol To Document -- Name: `ApplyAsciiSize` -- Kind: `method` -- File: `src/EchoHub.Client/AppOrchestrator.cs` -- Language: `csharp` -- ID: `db251379-6f70-4217-ba11-e574fbf08e9d` +Updates the in-memory `DefaultAsciiSize` to the provided `flag`, saves the updated config with `ConfigManager.Save`, refreshes the staging tray via `RefreshStagingTray`, and, if a `CurrentChannel` exists, emits a system message announcing the new size using `AsciiSizeLabel(flag)`. --- @@ -255,20 +220,15 @@ private static string AsciiSizeLabel(string flag) => flag switch **Returns:** `string` -Converts a single-character size flag into a human-readable ASCII size label used by the UI. This private helper centralizes the mapping so callers don't duplicate string literals across the codebase. For flag \"s\" it yields \"Small (40x40)\", for flag \"l\" it yields \"Large (120x120)\", and for any other value it yields \"Medium (80x80)\". +Converts a compact ASCII size flag into a human-readable label for display in the UI. Given the `flag` argument, it returns `Small (40x40)` for `s`, `Large (120x120)` for `l`, and `Medium (80x80)` for any other value. This centralizes the mapping so the rest of the ASCII rendering logic uses consistent strings rather than duplicating literals. ## Remarks -By keeping the label logic in one private, static method, the rest of the codebase benefits from a single source of truth for size labels. The switch expression makes the mapping straightforward to extend if new flags are introduced. Its private scope keeps coupling low and makes intent explicit within its containing type. -## Example -```csharp -// Example usage within the same class -string small = AsciiSizeLabel("s"); // "Small (40x40)" -string defaultLabel = AsciiSizeLabel("x"); // "Medium (80x80)" -``` +This small helper encapsulates the mapping between the size flag and its display label, making future changes to the labels centralized. Its private static scope signals it's an internal detail of the hosting class and should not be relied on from outside. ## Notes -- Access scope: AsciiSizeLabel is private to its declaring type, so it cannot be called from outside the class. If external callers need the same mapping, expose a public wrapper or move the method to a shared utility. + +- The switch is case-sensitive; values other than `s` or `l` result in `Medium (80x80)`. --- @@ -291,15 +251,15 @@ private static async Task BuildOutgoingAttachmentAsync(strin **Returns:** `Task` -Reads a staged file into an OutgoingAttachment. If a roomKey is provided, the method reads the file into memory, determines whether it is a valid image, and for images generates a local ASCII preview using ImageToAsciiService at the requested size; the preview text is then encrypted with roomKey. The file bytes are encrypted with RoomCrypto and encapsulated in the attachment; the attachment also records a declaredKind of image, audio, or file, and includes the (encrypted) preview when available. If no roomKey is supplied, the method returns an OutgoingAttachment that streams the file directly with its name, skipping encryption and preview generation. This design enables end-to-end protection in encrypted channels while keeping behaviors for non-encrypted channels simple. +BuildOutgoingAttachmentAsync reads a file from disk and returns an [`OutgoingAttachment`](Services/OutgoingAttachment.cs.md) suitable for transmission. If a `roomKey` is provided, it encrypts the file bytes with that key, and for image files it also renders a local ASCII preview (at the size indicated by `size`) and encrypts the preview; the server never sees the original file or image contents. If `roomKey` is null, it returns a plain attachment backed by the raw file stream. ## Remarks -This method acts as the orchestration point for turning a local file into the transportable OutgoingAttachment used by the client’s outbound pipeline. It encapsulates the conditional encryption and, for images, an on-device ASCII preview to surface a visual cue without exposing the raw image. By delegating validation, ASCII rendering, and crypto to dedicated helpers, it keeps the code focused on assembling the attachment rather than the details of how each piece works. +By centralizing the attachment preparation for encrypted channels, this method hides the intricacies of file type detection, client-side preview rendering, and encryption behind a single helper. It relies on [`RoomCrypto`](../EchoHub.Core/Security/RoomCrypto.cs.md) for encryption and on [`ImageToAsciiService`](../EchoHub.Core/Services/ImageToAsciiService.cs.md) to generate human-friendly previews, ensuring consistent behavior across callers when dealing with encrypted attachments. ## Notes -- If roomKey is null, the file is streamed directly without encryption or a preview. -- Large files will be fully loaded into memory for encryption; consider streaming or chunking if this is a concern. -- The preview is only produced for valid images; non-image files skip the preview. +- When `roomKey` is null, the method returns a non-encrypted attachment without a preview. +- The ASCII preview is generated only for valid images; non-image files produce no `preview` (it remains null). The declared kind is set to `image` for images, `audio` for audio files, and `file` otherwise. + --- @@ -320,14 +280,13 @@ private static void CleanupPastedTempFiles(IReadOnlyList files) **Returns:** `void` -Best-effort cleanup of pasted-image temp files and their per-paste folders. The method iterates the provided file paths, deletes each file, and then attempts to remove the containing directory if present; any failures are caught and logged at Debug level, so cleanup does not disrupt the calling workflow. +Best-effort cleanup of pasted-image temp files and their per-paste folders. For each path in the supplied `IReadOnlyList`, it deletes the file with `File.Delete` and, if available, deletes the directory that contains the file (via `Path.GetDirectoryName` and `Directory.Delete`). Any exceptions are caught and logged with `Log.Debug` so cleanup does not propagate to the caller. ## Remarks -Encapsulates the cleanup behavior so the paste pathway remains focused on its primary task. It uses a forgiving error-handling strategy: delete what you can, swallow failures, and report them only via debug logs. Because the directory deletion happens after the file removal, the directory will be removed only if it is empty, aligning with typical per-paste directory semantics. +This helper favors safety and simplicity: it never throws from the cleanup loop; failures are swallowed to avoid impacting the user flow after a paste. It provides diagnostic visibility through debug logging, aiding investigation if artifacts persist after cleanup. ## Notes -- Directory.Delete is non-recursive by default; the directory will be removed only if empty after file deletion. -- Exceptions are swallowed; failures are only logged at Debug level; callers can't rely on exceptions to signal cleanup success. +- `Directory.Delete(dir)` will only delete an empty directory; if the per-paste folder still contains files, the delete will fail and be swallowed, potentially leaving artifacts. --- @@ -342,13 +301,60 @@ private void ClearPendingReply() **Returns:** `void` -Clears the active pending-reply state by assigning null to the internal _pendingReply field and informing the UI that there is no current reply in progress by calling _mainWindow.SetReplyingTo(null). As a private helper, it's used within the orchestrator to finalize or abort the reply workflow and keep both the data and the user interface in sync. +Clears the currently pending reply by setting the private field `_pendingReply` to `null`. It also notifies the UI by calling `_mainWindow.SetReplyingTo(null)` to reflect that there is no active reply target. + +## Remarks +By centralizing this two-step reset, the method guarantees that both the internal model (`_pendingReply`) and the UI state (`_mainWindow.SetReplyingTo(null)`) stay in sync when a reply is canceled or completed. It provides a single, discoverable place to revert to idle state, reducing the chance of stale state leaking into the user interface. + +--- + +### ClearSavedToken +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void ClearSavedToken(string serverUrl) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `serverUrl` | `string` | — | + +**Returns:** `void` + + +Clears the locally saved refresh token for a specific server by mutating the client configuration. It loads the current configuration via `ConfigManager.Load()`, searches the `SavedServers` collection for an entry whose `Url` matches the provided `serverUrl` using `StringComparison.OrdinalIgnoreCase`, and if found sets its `RefreshToken` to `null`. The updated config is then persisted with `ConfigManager.Save(config)` and the in-memory `_config` reference is refreshed to the latest object. ## Remarks -Centralizes reply-cleanup logic to avoid duplicating state-management across multiple code paths. By updating both the internal _pendingReply and the main window's replying-to state in one place, it guarantees consistent behavior whenever the reply flow ends. This abstraction helps decouple the decision to clear a reply from the specific UI or flow that triggers it, making future changes to the cleanup process easier. +This private helper centralizes the token-clearing mutation to a single server URL, ensuring both the persisted configuration and the in-memory copy remain in sync. It encapsulates a security-sensitive operation (token removal) behind a tidy, reusable unit to avoid duplicating credential-clearing logic across call sites. ## Notes -- Must run on the UI thread due to SetReplyingTo updating the UI; calling from a background thread may cause cross-thread exceptions or UI glitches. +- Only affects the server that matches `serverUrl`; if no matching entry exists, no action is taken. +- The URL comparison uses `StringComparison.OrdinalIgnoreCase` to tolerate casing differences in server URLs. + +--- + +### DedupPath +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private static string DedupPath(string dir, string fileName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `dir` | `string` | — | +| `fileName` | `string` | — | + +**Returns:** `string` + + +DedupPath is a private static helper that returns a non-colliding file path within `dir` for a given `fileName` by appending a numeric suffix in the form ` (n)` before the extension when the initial name already exists. It relies on `Path.GetFileNameWithoutExtension`, `Path.GetExtension`, `Path.Combine`, and `File.Exists` to probe candidate names—starting with the original, then variants that insert ` (1)`, ` (2)`, etc.—until a path that does not exist is found, so the caller can save without overwriting existing files. --- @@ -363,16 +369,36 @@ public void Dispose() **Returns:** `void` -Disposes EchoHub client resources in a defined teardown order. On disposal, it persists the last reads, then synchronously disposes the underlying connection, and finally disposes the update service. Call this when the orchestrator is shutting down to ensure all resources are released and the connection is cleanly closed. +Disposes the orchestrator's resources synchronously. When invoked, it first persists the last read positions via `PersistLastReads()`, then synchronously disposes the underlying connection by awaiting `_conn.DisposeAsync()` through `AsTask().GetAwaiter().GetResult()`, and finally disposes `_updateService`. ## Remarks -This Dispose implements a synchronous disposal flow that bridges an asynchronous disposal of the connection. The explicit GetAwaiter().GetResult() ensures the connection is fully closed before proceeding, but it can introduce deadlocks if called on a synchronization context that blocks. The order of operations — persist reads, close connection, then dispose updates — ensures state is captured before teardown and that dependent services are torn down only after the connection is terminated. +This ordering guarantees that in-flight read state is captured before tearing down the connection and that dependent resources are released in a safe sequence. Bridging the asynchronous disposal of the connection into the synchronous `Dispose` method via `AsTask().GetAwaiter().GetResult()` is a pragmatic pattern for the classic `IDisposable` contract, but it can introduce deadlock risk if called from certain synchronization contexts; callers should ensure an appropriate execution context (e.g., non-UI threads) when disposing. ## Notes -- Blocking on asynchronous disposal can lead to deadlocks in certain synchronization contexts; prefer calling this from a non-UI thread or consider an asynchronous disposal pattern if possible. -- If an exception is thrown during PersistLastReads, DisposeAsync, or _updateService.Dispose, disposal may be interrupted and not all resources may be released. -- This method is not guarded for multiple invocations; repeated calls may encounter disposed resources. +- Bridging `DisposeAsync()` with `GetAwaiter().GetResult()` can deadlock in some synchronization contexts; prefer disposing from a context without a synchronization trap or consider a fully asynchronous disposal pattern if needed. + + +--- + +### DownloadAttachmentAsync +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private async Task DownloadAttachmentAsync(string attachmentUrl, string fileName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `attachmentUrl` | `string` | — | +| `fileName` | `string` | — | + +**Returns:** `Task` + +Downloads an attachment to a temporary file via `_conn.Api!.DownloadFileToTempAsync`, and, if the current channel has a room key available in `_conn.RoomKeys`, decrypts the file locally with `RoomCrypto.DecryptBytes` using that key. If no key is available or decryption fails, the temp file remains with the downloaded bytes and a warning is logged. The method returns the path to the temporary file for downstream usage. --- @@ -393,15 +419,71 @@ private async Task EnsureRoomUnlockedForSendAsync(string channelName) **Returns:** `Task` -This private guard ensures outbound messages to a channel are only sent when the room is accessible: if the channel is not end-to-end encrypted or already has a key, sending is allowed immediately. If the channel is encrypted but locked, it triggers the unlock flow and returns true only when unlocking succeeds; otherwise it reports an error and blocks the send. +Ensure sending on an end-to-end encrypted channel is allowed only when the room is unlocked. It checks the encryption state via `_conn.RoomKeys.IsChannelEncrypted(channelName)` and `_conn.RoomKeys.HasKey(channelName)`, attempts to unlock with `UnlockTrackedChannelAsync(channelName)`, and surfaces an error via `InvokeUI(() => _mainWindow.ShowError(...))` if unlocking isn’t possible, returning false in that case. This guard centralizes the encryption-state handling so callers don’t leak plaintext or duplicate unlock logic when sending on protected channels. + +--- + +### FetchAndUpdateOnlineUsers +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void FetchAndUpdateOnlineUsers() +``` + +**Returns:** `void` + + +FetchAndUpdateOnlineUsers is a private helper that fetches the online users for the current channel and refreshes the UI, but only when there is a non-empty channel and the connection is active (`_conn.IsConnected`). It runs on a background thread via `Task.Run`, calls `_conn.GetOnlineUsersAsync(channel)` to obtain the list, stores the result in the shared cache `_channelUsers` under the lock `_channelUsersLock`, and then marshals the update to the UI with `InvokeUI(() => _mainWindow.UpdateOnlineUsers(users))`; any exceptions are caught and logged with `Log.Debug`. + +## Remarks +This private helper isolates data retrieval, cache synchronization, and UI refresh from the foreground path, keeping the UI responsive and ensuring thread-safety when updating the `_channelUsers` collection. It coordinates with `_mainWindow` and `_conn` collaborators to reflect current online users for the active channel. + +--- + +### GetDownloadDir +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private string GetDownloadDir() +``` + +**Returns:** `string` + + +Private helper `GetDownloadDir()` determines where downloads should be written by preferring the configured `ClientConfig.DownloadPath`; if that is not set, it resolves to the OS Downloads folder derived from the current user's profile. It then ensures the directory exists by calling `Directory.CreateDirectory` and, if an exception occurs, logs a warning via `Log.Warning` and falls back to the system temp folder from `Path.GetTempPath()`. + +## Remarks +Centralizes download-path resolution in one place, shielding callers from platform differences and misconfigurations. By performing the directory creation with `Directory.CreateDirectory` and handling failures with a graceful fallback and warning log via `Log.Warning`, this method provides a robust foundation for all download operations and keeps higher-level code focused on business logic. + +--- + +### HandleAudioPlayRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleAudioPlayRequested(string attachmentUrl, string fileName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `attachmentUrl` | `string` | — | +| `fileName` | `string` | — | + +**Returns:** `void` + + +Handles a request to play an audio attachment by validating authentication, downloading the file, and presenting playback UI. If authenticated, it runs an asynchronous operation that posts a system message to the current channel indicating download progress, downloads the attachment to a temporary path via `DownloadAttachmentAsync(attachmentUrl, fileName)`, and then marshals back to the UI thread to display the playback dialog with `AudioPlayerDialog.Show(_app, _audioPlayback, tempPath, fileName)`. If anything goes wrong, the operation is reported via the RunAsync error handler with the message `Failed to play audio`. ## Remarks -By centralizing the gating logic here, the sending path consistently enforces encryption constraints and user interaction for unlocking. It bridges the connection manager's RoomKeys state, the unlocking prompt, and the UI feedback loop so callers don't have to duplicate this logic. Keeping this logic in this dedicated helper reduces the risk of inconsistent encryption checks scattered across the codebase. +This method encapsulates the full user flow for playing an audio attachment, combining authentication guard, background download, and UI orchestration into a single, testable unit. By offloading IO to `RunAsync` and marshaling UI work with `InvokeUI`, it keeps the orchestrator responsive while the file downloads, and then hands off to `AudioPlayerDialog.Show` to present playback. This separation makes it easy to swap the underlying playback UI or the download mechanism without changing the caller. ## Notes -- Short-circuits and returns true if the channel doesn't require unlocking or already has a key; otherwise it may present an unlock prompt and potentially abort the send. -- If unlocking is required, it calls UnlockTrackedChannelAsync and may present a prompt to the user; a declined or failed unlock results in a false return and an error message. -- It relies on the RoomKeys state and uses UI feedback (via _mainWindow.ShowError) to communicate failures to the user; callers should respect the returned boolean and refrain from sending when false. +- Silent no-op when `_conn.IsAuthenticated` is false; there is no user-visible feedback in that case. --- @@ -422,14 +504,13 @@ private void HandleChannelJoinFromMessage(string channelName) **Returns:** `void` -Responds to a request to join a channel coming from a message by first ensuring the connection is active; if not connected, it exits early. When connected, it marshals execution to the UI thread, guarantees the channel is present in the UI channel list, switches the UI to that channel, and then delegates to HandleChannelSelected to trigger further channel-activation handling. +HandleChannelJoinFromMessage processes a channel-join request derived from an incoming message. If the connection is active, it marshals the UI work to the main thread via `InvokeUI` to ensure the channel is present in the list (`_mainWindow.EnsureChannelInList(channelName)`) and switches to the channel (`_mainWindow.SwitchToChannel(channelName)`), then delegates to `HandleChannelSelected(channelName)` to perform any downstream selection logic. ## Remarks -Acts as the glue between the messaging flow and the UI state. By marshaling to the UI thread and centralizing the sequence of ensuring the channel exists and becomes active, it helps prevent cross-thread issues and keeps the UI in sync with inbound join requests. The subsequent call to HandleChannelSelected ensures that any additional, per-channel activation logic is applied in a single, well-defined step. +This method acts as a small UI-facing bridge that keeps channel state in sync when a join event arrives from a message. It enforces UI-thread affinity by wrapping updates in `InvokeUI` and centralizes the ordering: connectivity check, UI update, then downstream selection logic via `HandleChannelSelected`. ## Notes -- No input validation is performed on channelName here; downstream UI methods may interpret empty or invalid values in different ways. Validate as needed at call-sites if that matters for your scenario. -- The method is a no-op when not connected, so callers relying on its side effects must ensure connection lifecycle is managed appropriately. +- Early return on `_conn.IsConnected` being false means join events are ignored until a live connection is established; callers should anticipate that messages about channel joins may be deferred. --- @@ -450,12 +531,38 @@ private void HandleChannelSelected(string channelName) **Returns:** `void` -HandleChannelSelected is invoked when a user selects a channel in the client UI. It first verifies the client is connected and returns immediately if it is not; it also clears any pending reply that does not belong to the newly selected channel and persists the last-read positions before switching context. +HandleChannelSelected coordinates the client’s response when a user selects a channel. If the connection is active, it clears any pending reply for a different channel, persists last-read positions, and then performs the join/unlock/history-refresh flow in the background, updating the UI and online user list. + +## Remarks +`HandleChannelSelected` serves as the central orchestrator for channel switching, merging connection state, channel-tracking behavior, and UI updates. By centralizing this logic, it ensures that selecting a channel results in a consistent state transition: joining with a password when required, unlocking tracked channels when prompted, loading the channel history, and refreshing the list of online users. It also guards against cross-channel carryover of pending replies and read markers to avoid confusing user experiences. + +## Notes +- History retrieval failures are swallowed (the `catch` block is empty), so history may be unavailable without crashing the UI. +- If a user cancels the password prompt for a join, the method untracks the channel and returns the user to the default channel via `HubConstants.DefaultChannel` (and the UI is switched accordingly). + +--- + +### HandleCheckForUpdatesRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleCheckForUpdatesRequested() +``` + +**Returns:** `void` + + +It handles a request to check for updates by starting an asynchronous check via `_updateService.CheckNowAsync`, wrapped in the shared `RunAsync` error-handling helper with the failure message "Failed to check for updates". This method is invoked by the orchestrator in response to a user action or system event requesting an update check, ensuring the call runs asynchronously and failures are surfaced consistently. + +## Remarks -In the asynchronous workflow that follows, the method attempts to track the selected channel. If tracking succeeds, it prompts for a password as needed and, on a successful join, clears the left-channel exclusion for the joined channel. If the user cancels the password prompt, the channel is untracked and the UI switches back to the default channel. If tracking is not required but an unlock is needed, it triggers an unlock of the tracked channel. +This method encapsulates the common pattern of executing an asynchronous operation with centralized error handling. By delegating the actual work to `_updateService` and using the `RunAsync` helper for orchestration, the orchestrator remains decoupled from the details of the update mechanism while still providing a uniform failure experience to the user. It promotes consistent UX for update checks and keeps the orchestration logic tidy by avoiding direct await/try-catch boilerplate in the caller. -After handling join/unlock, the method tries to retrieve the channel history and load it into the UI; failures to fetch history are ignored. Finally, it refreshes the list of online users. +## Notes +- The method is private; it is not part of the public API. +- It does not return a value; it fires off the asynchronous operation and relies on `RunAsync` to handle completion and errors. --- @@ -477,15 +584,15 @@ private async Task HandleCmdAssignRole(string username, string roleStr) **Returns:** `Task` -Processes a user-issued command to assign a role to a specific user. It short-circuits when the current connection is not authenticated, preventing unauthorized role changes. It normalizes the incoming role string into a ServerRole enum (admin maps to Admin, mod to Mod, and any other value defaults to Member) and then calls the server API to apply that role to the given username. +Processes the client-side assign-role command by translating a textual role into the corresponding [`ServerRole`](../EchoHub.Core/Models/ServerRole.cs.md) and invoking the server API to apply it, but only when the connection is authenticated. It resolves `roleStr` via a switch: "admin" becomes `ServerRole.Admin`, "mod" becomes `ServerRole.Mod`, and all other inputs default to `ServerRole.Member`, before calling `_conn.Api!.AssignRoleAsync(username, role)`. ## Remarks -Acts as a small wrapper around the underlying API to centralize access control and input normalization for role assignment commands. By guarding the operation behind the authentication check, it ensures only authenticated sessions can modify roles. The conservative default to Member for unknown role strings provides a safe fallback that avoids elevating privileges due to unrecognized input. +It exists as a small, internal command handler that enforces authentication and translates user input into a server mutation. It centralizes the role-mapping policy and delegates the actual mutation to the server, keeping the command-text parsing separate from server interaction. ## Notes -- The operation does nothing if the client is not authenticated. -- Despite the authentication check, Api is accessed with a null-forgiving operator; if _conn.Api is null, this will throw at runtime. Ensure the API client is initialized alongside authentication. -- The role default to Member for unknown values can mask invalid input; if explicit validation is desired, this behavior may need to be revisited. +- Silent no-op when not authenticated may mask misuse; consider returning a status or throwing an exception to signal that authentication is required. +- Unrecognized role strings default to `ServerRole.Member` — this can hide input errors; consider explicit validation or user feedback. +- The method uses null-forgiving `_conn.Api!` — ensure `_conn.Api` is initialized post-authentication to avoid a `NullReferenceException`. --- @@ -507,13 +614,14 @@ private async Task HandleCmdBanUser(string username, string? reason) **Returns:** `Task` -Handles the ban-user command by ensuring the client is authenticated and then delegating to the API to perform the ban. It accepts a target username and an optional reason; if the current connection is not authenticated, it returns without taking action. When authenticated, it invokes BanUserAsync on the API, passing along the username and reason. +`HandleCmdBanUser` is a private async command handler that translates a ban command into a backend action. It first ensures the current connection is authenticated (`_conn.IsAuthenticated`). If authenticated, it calls [`BanUserAsync`](Services/ApiClient.cs.md) on `_conn.Api` with the target `username` and optional `reason` to perform the ban. ## Remarks -This method acts as a focused command handler: it enforces authentication and delegates the ban operation behind a single, well-defined API boundary. By isolating the authentication gate here, the rest of the command processing remains decoupled from the specifics of how a ban is performed. Any failures from BanUserAsync propagate to the caller, enabling centralized error handling or user feedback at a higher level. +This symbol serves as the orchestration layer between command input and moderation API calls. By encapsulating the authentication check and the API invocation, it keeps the higher-level command handling clean and focused on input parsing. It relies on the surrounding connection context (`_conn`) to route the request to the moderation API, making it easy to swap or mock the API in tests. ## Notes -- Silent return when not authenticated means callers may not observe a failure path for unauthenticated ban attempts; consider logging or surfacing a result if caller feedback is required. +- The call uses the null-forgiving operator on `_conn.Api` (as shown in `_conn.Api!.BanUserAsync(...)`); if `_conn.Api` can be null after authentication, this may throw at runtime. +- Exceptions from [`BanUserAsync`](Services/ApiClient.cs.md) will propagate to the caller of `HandleCmdBanUser`; ensure the returned `Task` is awaited to observe failures. --- @@ -535,15 +643,17 @@ private async Task HandleCmdChangeRoomPassword(string oldPassphrase, string newP **Returns:** `Task` -Handles the user command to change the end-to-end encryption passphrase for the currently active channel. It validates that the client is authenticated and connected, that a channel is selected and encrypted, and then re-derives the join credentials for both the old and new passphrases. It then asks the backend to re-key the channel by wrapping the cached room key with the new derived key. History remains readable; the room key itself does not change, so previously encrypted content stays decryptable by existing history, while new members/devices must use the new passphrase. +Changes the current encrypted channel's passphrase by re-deriving the join credential and re-wrapping the cached room content key under the new passphrase. History is never re-encrypted — the room key itself doesn't change. + +This private method is the UI action invoked when a user requests to rotate the passphrase for the currently selected end-to-end encrypted channel. It derives the old and new join credentials, re-wraps the room key with the new derived key, and applies the update via the channel API, leaving historical content intact. ## Remarks -This function coordinates client state (the current channel and the cached room key) with a server-side RekeyChannelAsync call to effect a passphrase transition. It guards against common failure modes (not authenticated, no channel selected, channel not end-to-end encrypted, missing room key) by showing targeted UI errors. The derived key material (old and new) and a fresh salt ensure that only someone with the new passphrase can wrap/unwrap the room key going forward. +This logic centralizes the rekey flow in the client orchestrator, ensuring proper preconditions (authenticated and connected, with a current channel selected) and using the existing crypto surface ([`RoomCrypto`](../EchoHub.Core/Security/RoomCrypto.cs.md)) to derive keys and wrap the room key. The actual server-side mutation is performed through `RekeyChannelAsync` on the channel API, which applies the new passphrase consistently for future access while preserving existing history. It relies on `RoomKeys` to locate the channel key before proceeding. ## Notes -- If the channel is not end-to-end encrypted or lacks an encryption salt, the operation aborts with a user-facing error. -- The action requires that the room key for the channel is cached; otherwise the user is prompted to unlock/rejoin the channel. -- The change does not re-encrypt historical messages; it only re-wraps the room key under the new passphrase, so access for existing history remains intact while access for new participants depends on the new passphrase. +- It aborts early if the client is not authenticated or not connected, or if no channel is selected. +- It requires the channel to be end-to-end encrypted and to have a non-null encryption salt; otherwise it reports that the channel is not encrypted. +- If the channel key cannot be retrieved (e.g., the channel is not unlocked), it prompts the user to unlock the channel first and retry. --- @@ -558,27 +668,14 @@ private Task HandleCmdClearAttachments() **Returns:** `Task` -Clears the currently staged attachments, removing any files that are also present in the temporary pasted set, cleans up their temporary copies, clears the staging collection, and refreshes the staging UI. Invoke this when the user requests to discard all attachments added during the current operation; it centralizes the cleanup logic to keep in-memory state and the UI in sync. +Clears the currently staged attachments as part of handling the 'Clear Attachments' command, removing any matching temporary pasted files, performing cleanup, and resetting the in-memory staging state. It returns a completed `Task` to align with asynchronous call patterns while performing the work synchronously. ## Remarks - -This method coordinates between in-memory state (_stagedAttachments), the temporary-file tracking (_tempPastedFiles), and the UI update path (InvokeUI(RefreshStagingTray)). By encapsulating the cleanup sequence, callers avoid duplicating disposal and UI-refresh logic, and it guarantees that temporary files created for pasted content are removed in tandem with the in-memory state. - -## Example - -```csharp -// Example usage within the same class -private async Task DemoClearAttachmentsUsage() -{ - await HandleCmdClearAttachments(); -} -``` +This method centralizes the cleanup sequence for the attachment staging area: it computes the intersection of `_stagedAttachments` with `_tempPastedFiles`, removes those temps from `_tempPastedFiles`, calls `CleanupPastedTempFiles(temps)`, clears `_stagedAttachments`, and refreshes the UI via `InvokeUI(RefreshStagingTray)`. +By coupling these steps in one place, the rest of the UI command handlers benefit from a consistent cleanup path, reducing risk of leaks or stale UI state. It also makes the intent explicit: clearing attachments is a single, atomic operation from the user's perspective. ## Notes - -- The method returns Task.CompletedTask, so from a caller's perspective it is effectively synchronous; awaiting it does not introduce real asynchronous work. -- If other code mutates _stagedAttachments or _tempPastedFiles concurrently, results may be inconsistent; this method assumes a single-threaded (UI) invocation context. -- If CleanupPastedTempFiles(temps) throws, the task will fault; callers should consider exception handling around the command invocation. +- This operation discards all staged attachments; any unsaved items will be removed and cannot be recovered. --- @@ -600,16 +697,13 @@ private Task HandleCmdCreateInvite(int? maxUses, int? expiresHours) **Returns:** `Task` -Creates a channel-scoped invite by calling the API with the optional maxUses and expiresHours and displays the resulting code in the current channel; it includes uses, an optional expiry, and a tip to revoke with a slash command. If the user is not authenticated or there is no current channel, it exits early and performs the work asynchronously to keep the UI responsive. +HandleCmdCreateInvite is a private command handler that orchestrates the creation of a channel invite. When invoked, it first checks that the user is authenticated (`_conn.IsAuthenticated`) and that there is a current channel (`_mainWindow.CurrentChannel`); if both preconditions hold, it runs an asynchronous flow that calls the API (`_conn.Api!.CreateInviteAsync(maxUses, expiresHours)`) to obtain an invite, and upon success formats an optional expiry from `invite.ExpiresAt` and posts a system message to the channel with the invite code, allowed uses, expiry if present, and a reminder to revoke the invite via `/invite revoke {invite.Code}`. ## Remarks -This method orchestrates authentication state, UI context, and remote API to provide a seamless command experience. It isolates the network call and UI update from the main thread using RunAsync, reducing UI latency and potential blocking, while presenting the user with a clear revoke path to manage the created invite. +HandleCmdCreateInvite encapsulates the end-to-end flow of invite generation in the UI, coordinating authentication state, channel context, API interaction, and user notification. It relies on the components `_conn`, `_mainWindow`, `_messageManager`, and the helper `RunAsync` to perform the work without blocking the UI thread, keeping the command-handling logic focused and testable. ## Notes -- Relies on _conn.Api being non-null after authentication; if that invariant is violated, the null-forgiving operator may lead to a NullReferenceException. -- The expiry timestamp is formatted in local time when available, via ToLocalTime with the pattern "yyyy-MM-dd HH:mm". -- The outer method returns Task.CompletedTask immediately; the actual invite creation and UI update run asynchronously, so callers should not expect any awaitable work from this method. - +- If the API becomes unexpectedly null while authenticated, the null-forgiving usage `_conn.Api!` could throw; ensure API initialization is tightly coupled with the authentication state. --- @@ -624,9 +718,10 @@ private Task HandleCmdDeleteAccount() **Returns:** `Task` -Deletes the currently authenticated user’s account from the server as part of the user command workflow. It first ensures the client is authenticated, presents a destructive confirmation dialog explaining that the account will be permanently removed (including profile, sessions, and uploaded files, with messages remaining attributed to 'deleted-user'), and only proceeds if the user confirms. If confirmation is given, it prompts for the account password, then asynchronously performs the deletion via the server API, clears saved tokens for the server, and performs cleanup. On success, the UI is reset (main window cleared, status set to Disconnected) and the user is informed that the account and uploaded data have been deleted. +HandleCmdDeleteAccount coordinates the user-initiated account-deletion workflow. It first ensures the client is authenticated via `_conn.IsAuthenticated` and aborts with `Task.CompletedTask` if not; it then presents a warning dialog explaining that deleting the account permanently removes the user on this server (including profile, sessions, and uploads) while leaving messages attributed to 'deleted-user'. If the user confirms and provides a `password`, the handler proceeds asynchronously: it calls `_conn.Api!.DeleteMyAccountAsync(password)`, clears the saved token for the current `baseUrl` with `ClearSavedToken(baseUrl)`, performs cleanup via `_conn.CleanupAsync()`, and updates the UI to reflect a disconnected state and to display an "Account Deleted" confirmation." -The operation is designed to run asynchronously to keep the UI responsive and to encapsulate the end-to-end flow of a high-risk action within a single command handler. +## Remarks +HandleCmdDeleteAccount centralizes the end-to-end destructive flow in a single, private handler, ensuring a consistent user experience from confirmation to disconnection. The operation is carried out asynchronously (via `RunAsync` and `InvokeUI` for UI thread marshalling), preserving UI responsiveness while performing server-side deletion and thorough cleanup of session state. This tight coupling of authentication check, user confirmation, credential handling, server API invocation, token invalidation, and UI refresh helps avoid partial/inconsistent states around a critical, irreversible action. --- @@ -641,15 +736,7 @@ private Task HandleCmdExportData() **Returns:** `Task` -Exports the authenticated user\'s data by calling the server API and writing the resulting JSON to a timestamped file in the local downloads directory. The file is named echohub-export-{username}-{yyyyMMdd-HHmmss}.json to avoid collisions. The operation runs asynchronously so the UI remains responsive. After the file is created, a system message is posted to the current channel (if any) indicating the destination path and clarifying that the content is ciphertext and the server never had plaintext. - -## Remarks -The method centralizes the data-export workflow behind a command handler, encapsulating authentication gating, local persistence, and user feedback. It ensures the UI remains responsive by offloading work to an asynchronous runner and then dispatching a UI update once the export completes. This abstraction keeps the channel-communication logic separate from the networking and file-system concerns, making the export action reusable from the UI command surface. - -## Notes -- The final user notification is sent only when a non-empty channel is present; otherwise, no in-channel message is posted. -- If authentication fails, the method completes without performing the export, avoiding unnecessary work or side effects. -- The exported payload is treated as ciphertext in the UI message, reflecting that the server-side data is not plaintext within the export artifact. +HandleCmdExportData exports the authenticated user's data to a local file by guarding against unauthenticated invocations, then asynchronously fetching the data via [`ExportMyDataAsync`](Services/ApiClient.cs.md), writing it to a timestamped file named `echohub-export-{_session.Username}-{DateTime.Now:yyyyMMdd-HHmmss}.json` in the download directory after deduplication with `DedupPath(GetDownloadDir(), fileName)`, and finally posting a system message in the current channel with the file path and a ciphertext disclaimer. --- @@ -671,16 +758,16 @@ private async Task HandleCmdJoinChannel(string channelName, string? password) **Returns:** `Task` -Handles the user command to join a channel by validating the connection, prompting for a password when required, removing any left-channel exclusion for the channel on success, updating the UI to include and switch to the channel, and loading previous history if available. It is invoked as part of the command execution flow when a user requests to join a channel, encapsulating the join logic and related UI updates in one place. +HandleCmdJoinChannel is the central handler for a user-initiated join-channel action. It ensures the client is connected, invokes a password prompt via `JoinChannelWithPasswordPromptAsync`, and, if a non-null history is returned, clears any prior left-channel exclusion from `LeftChannels` before updating the UI to list and switch to the channel and loading its history. ## Remarks -It centralizes the channel-join flow in AppOrchestrator, coordinating connection state, password prompting, server configuration, and UI navigation. By clearing the left-channel exclusion on join (RemoveAll with a case-insensitive match), it ensures a fresh entry for the channel. UI updates are dispatched to the main thread via InvokeUI, and history loading is triggered when available, keeping the user experience cohesive even when past sessions exist. +This method coordinates cross-cutting concerns across connection state, server configuration, and the user interface. It encapsulates the complete join flow: validating connectivity, obtaining necessary credentials, mutating the server-side exclusion list so the channel can be rejoined, and driving UI navigation and history display. By handling errors centrally (showing a user-facing error via the main window), it provides a consistent UX and keeps the join logic isolated from higher-level command parsing. ## Notes -- Skips work if there is no active connection; the method exits early when _conn.IsConnected is false. -- If the password prompt is canceled (history is null), the join is aborted without altering server state or UI. -- The cleanup of LeftChannels uses a case-insensitive match to ensure the channel name is removed regardless of casing. -- Exceptions are caught and shown to the user via the main window, which prevents the app from crashing but may hide internal details. +- If `history` is `null`, the operation is considered cancelled (e.g., the user dismissed the password prompt) and no side effects occur. +- The removal from `LeftChannels` uses `StringComparison.OrdinalIgnoreCase`, making the operation robust to channel name casing. +- All UI updates are dispatched via `InvokeUI` to marshal work to the UI thread; exceptions during this path surface to the user through [`ShowError`](UI/MainWindow.cs.md). + --- @@ -702,15 +789,14 @@ private async Task HandleCmdKickUser(string username, string? reason) **Returns:** `Task` -Handles the Kick User command by delegating to the underlying API to remove a user from the session. Before performing the kick, it checks that the connection is authenticated; if not, it exits without taking action. When authenticated, it calls KickUserAsync on the API, passing the target username and the optional reason. This method centralizes the guard logic and the actual kick call under a single handler, so higher-level command processing can rely on a single moderation action point rather than invoking the API directly. +Kick a user command handling is performed by this private asynchronous method. It first checks the current authentication state via `_conn.IsAuthenticated` and exits early if the client is not authenticated; when authenticated, it delegates the kick operation to the remote API by calling `_conn.Api!.KickUserAsync(username, reason)` with the target `username` and an optional `reason`. ## Remarks -Acts as a boundary between command parsing and server-side moderation actions. It ensures that only authenticated sessions can initiate a kick, preventing unauthorized disruptions. The use of the null-forgiving operator on Api implies the API instance is expected to be non-null once authentication is established; this contract should be preserved by the surrounding code. +By encapsulating the command-handling path and delegating the actual removal to `_conn.Api!.KickUserAsync`, this method keeps the orchestrator focused on command flow while isolating API interaction behind a single channel. It enforces an authentication check before issuing the kick, preventing unauthorized attempts from progressing in the command pipeline. The private scope signals that this logic is internal to the orchestrator and should not be invoked directly from external callers. ## Notes -- Be aware that Api is accessed with the null-forgiving operator, so a non-null Api is required after authentication; otherwise a NullReferenceException may be thrown. -- Exceptions from KickUserAsync propagate to the caller; callers may need to catch and handle errors (e.g., log failures or surface user-visible errors). -- The reason parameter is optional; passing null means no reason will be recorded. +- The code uses the null-forgiving operator on `_conn.Api` to assume a non-null API client after authentication. If `_conn.Api` could be null in certain states, this could throw a `NullReferenceException` when calling [`KickUserAsync`](Services/ApiClient.cs.md). + --- @@ -725,16 +811,13 @@ private async Task HandleCmdLeaveChannel() **Returns:** `Task` -Handles the user command to leave a channel. It exits early when not connected or when no channel is selected, blocks leaving the default channel, calls LeaveChannelAsync, records the leave in the server configuration's LeftChannels so auto-join won't pull you back in, and posts a system message confirming the departure. If something goes wrong, it reports the error to the UI. +HandleCmdLeaveChannel is the asynchronous command handler that leaves the currently selected channel when the client is connected and the channel is not the default one (`HubConstants.DefaultChannel`). It delegates the actual leave to `_conn.LeaveChannelAsync(channel)`, and on success persists the departure by adding the channel to the server configuration’s `LeftChannels` (via `UpdateServerConfig`) using a case-insensitive check (`StringComparer.OrdinalIgnoreCase`) to avoid duplicates, then informs the user with a system message through `_messageManager.AddSystemMessage`. If an error occurs, it surfaces a failure message via `_mainWindow.ShowError`. ## Remarks -Coordinates UI feedback, connection state, and user preferences. It separates the transient action of leaving a channel from the persistent config, ensuring the user's choice is remembered and doesn’t get undone by auto-join logic on reconnect. The UI thread marshalling via InvokeUI and the defensive checks guard against invalid states and repeated leaves. +This method acts as the orchestrator between the connection layer, persistent configuration, and the UI. By recording the channel in `LeftChannels`, it preserves the user’s intent across restarts and prevents automatic re-entry during subsequent connections. UI updates are marshalled to the main thread with `InvokeUI`, ensuring thread-safety when the operation completes asynchronously. ## Notes -- If the user is not connected or no channel is selected, the method returns without side effects. -- Leaving the DefaultChannel is explicitly blocked to avoid breaking the core chat experience. -- LeftChannels is updated using a case-insensitive check (OrdinalIgnoreCase) to deduplicate and persist the user's choice across sessions. -- Any exception during LeaveChannelAsync is surfaced to the user via ShowError. +- `LeftChannels` updates use `StringComparer.OrdinalIgnoreCase` to avoid duplicates regardless of channel name casing. --- @@ -749,17 +832,10 @@ private Task HandleCmdListInvites() **Returns:** `Task` -After verifying authentication and a current channel, this method fetches invite codes from the API, formats each invite’s usage and state into a readable list, and posts the result as a system message to the active channel. If no invites exist, it guides the user on how to create one. +If the user is authenticated and a channel is selected, `HandleCmdListInvites` fetches invite codes with `_conn.Api!.GetInvitesAsync()` and builds a human-readable summary listing each invite's `Code` and `UseCount`/`MaxUses`, annotating state as `used up`, `expired`, `expires `, or `active` depending on expiry and usage. The resulting text is then posted to the current channel as a system message via `InvokeUI(() => _messageManager.AddSystemMessage(channel, text))`. ## Remarks - -Serves as the UI-facing adapter for the invite-list feature by coordinating API access, formatting, and UI updates. Each invite is categorized as used up, expired, or active based on UseCount, MaxUses, and ExpiresAt: used up when UseCount >= MaxUses; expired when ExpiresAt has a value and is in the past; otherwise active. If ExpiresAt has a value, the expiration date is shown in local time using the format yyyy-MM-dd HH:mm. The multi-line message is posted to the channel via InvokeUI to ensure thread-safety, and failures are surfaced by RunAsync with the message Failed to list invites. - -## Notes - -- Early exits guard: returns Task.CompletedTask if not authenticated or there is no current channel, preventing API calls or UI updates. -- The method accesses _conn.Api using the null-forgiving operator, which assumes Api is non-null after authentication. -- Expiration handling relies on ExpiresAt being present to show an expiration timestamp and formats it using local time; if ExpiresAt is null, the invite is considered active unless UseCount/MaxUses dictate otherwise. +This symbol acts as a presentation conduit for invites and keeps UI-thread interaction isolated from the data fetch by wrapping the operation in `RunAsync`. It enforces simple preconditions via `_conn.IsAuthenticated` and a non-empty `channel` to avoid unnecessary network calls. The per-invite state logic encodes the business rules for visibility: `used up` when `i.UseCount >= i.MaxUses`, `expired` when `i.ExpiresAt` exists and is in the past, `expires ` when a future expiry exists (formatted in local time as `yyyy-MM-dd HH:mm`), or `active` otherwise. --- @@ -774,15 +850,15 @@ private async Task HandleCmdListUsers() **Returns:** `Task` -HandleCmdListUsers retrieves and displays the list of online users for the currently selected channel when the client is connected. It requests the online user list from the underlying connection and then updates the UI to present each user with their display name (falling back to username) and their status, including any status message. If there is no active connection or no channel selected, the method exits without performing work; any error during retrieval or UI update is caught and reported to the user. +HandleCmdListUsers fetches the online users for the currently selected channel when the client is connected and renders them as system messages in the channel UI. It guards against missing connection or channel, retrieves the users via `_conn.GetOnlineUsersAsync(channel)`, and uses `InvokeUI` to add a header line and a per-user entry that shows the user’s `DisplayName` (falling back to `Username`) and their `Status` (plus optional `StatusMessage`). ## Remarks -By centralizing the command handling for listing users, this method cleanly separates networking concerns from UI rendering. It uses InvokeUI to marshal updates to the main thread, ensuring thread-safety when modifying the UI. The rendered output displays a header indicating the channel, followed by one line per user that shows a display name and a textual status, including any optional status message when present. +This method acts as a small bridge between the connection service and the UI layer. By performing an asynchronous fetch and then marshaling the results onto the UI thread, it isolates the command's intent—listing channel presence—from the details of how messages are rendered, promoting consistency across similar commands and making it easier to reason about threading and error handling in the UI-bound flow. ## Notes -- Guards against no connection or empty channel by returning early, making the call effectively a no-op in those cases. -- All exceptions during retrieval or UI updates are caught and surfaced to the user via a dialog, preventing crashes. -- The rendering relies on Status.ToString(); ensure Status is non-null in your UserSession model or guard accordingly to avoid potential NullReferenceException. +- The status line for each user uses `Status` (converted to string) and conditionally appends `StatusMessage` when it is present, providing richer presence information without clutter when there is no message. +- The method gracefully handles scenarios where there is no active connection or no channel selected by returning early, avoiding unnecessary work or exceptions. +- All UI updates are performed through `InvokeUI` to ensure they execute on the UI thread, preserving thread-safety for `_messageManager` and the main window. --- @@ -798,15 +874,15 @@ private async Task HandleCmdMeta() **Returns:** `Task` -Fetches the current channel's metadata from the API and renders a concise room-info block in the chat UI. The method only runs when the client is connected and a channel is selected, and it gracefully handles missing metadata or API errors by reporting them to the user. +Fetches and displays room metadata for the currently selected channel when a connection is active. It retrieves the channel name from `_mainWindow.CurrentChannel`, calls `_conn.Api.GetChannelMetaAsync(channel)` to obtain metadata, and, if found, renders a concise room-info panel including topic, room ID, creation time, message count, unique users, estimated size, and protection status; if not found or an error occurs, it reports an error to the user. ## Remarks -This symbol acts as the UI-facing bridge between the network layer and the chat view, encapsulating the logic to fetch channel metadata and translate raw values into human-friendly text. It centralizes formatting decisions (such as the estimated size display and the protection status label) and ensures all UI updates occur on the UI thread through InvokeUI, preventing cross-thread UI mutations. By using early returns for preconditions and clear error signaling, it keeps the UI responsive and predictable even when the channel information cannot be retrieved. +This method encapsulates the UI-driven flow for presenting server-provided channel metadata. It coordinates the network fetch, null-checks, and UI updates via `InvokeUI` to ensure the information is shown on the correct thread, and it formats the display values (for example, the estimated size using `ChatMessageManager.FormatFileSize` and the creation time via `CreatedAt.ToLocalTime()`). ## Notes -- If meta.EstimatedSizeBytes <= 0, the size is displayed as "0 B"; otherwise the value is converted via FormatFileSize to a human-friendly string. -- When the API returns null for the channel metadata, the method surfaces a user-facing error {Channel #} not found to avoid throwing. -- All UI updates are dispatched to the UI thread via InvokeUI to maintain thread safety during cross-thread operations. +- The method guards its core path with `_conn.IsConnected` and a non-null `_conn.Api`, returning early otherwise. +- If `GetChannelMetaAsync` returns `null`, a user-facing error like "Channel # not found." is shown. +- Exceptions are caught and surfaced to the user through a UI error message, so callers should not rely on exceptions propagating from this method. --- @@ -829,15 +905,15 @@ private async Task HandleCmdMuteUser(string username, int? duration) **Returns:** `Task` -Handles the mute-user command by delegating to the API after ensuring the client is authenticated. If the client is not authenticated, it exits early without performing any action. +Handles the command to mute a user by validating authentication and delegating to the server-side API to apply the mute for the optional duration. The operation short-circuits when the client is not authenticated and, when authenticated, forwards the `username` and `duration` to the server via `_conn.Api!.MuteUserAsync(username, duration)`. ## Remarks -This method serves as a small glue layer between command-handling logic and the backend API. It enforces the precondition that only authenticated sessions can mute users and centralizes that guard in one place. The actual mute action is performed by the Api.MuteUserAsync call; the code uses the null-forgiving operator on Api, which implies Api must be non-null when IsAuthenticated is true. +This method is a focused wrapper around the server mutation that enforces a simple client-side security boundary: authentication must be established before muting. It coordinates between the orchestrator's connection state (`_conn`) and the server API (`_conn.Api`). If the invariant that `_conn.IsAuthenticated` implies a non-null `_conn.Api` is violated, this method can throw a `NullReferenceException`. ## Notes -- Silent no-op when not authenticated means callers may not see feedback; consider surfacing an explicit denial to the user at the call site. -- The null-forgiving operator on Api implies Api must be initialized for authenticated sessions; a mismatch could throw NullReferenceException. -- There is no cancellation support in this path; the caller cannot cancel the operation here. +- The operation short-circuits on unauthenticated state: if `!_conn.IsAuthenticated`, the method returns immediately without calling the server. +- It uses the null-forgiving operator on `_conn.Api` (`_conn.Api!`) which assumes the API is non-null after authentication; if this invariant is violated, a `NullReferenceException` may be thrown. +- No validation is performed on `username` or `duration` at this layer; callers or the server should enforce any required constraints. --- @@ -852,21 +928,41 @@ private async Task HandleCmdNukeChannel() **Returns:** `Task` -This private async method performs a destructive operation on the currently selected channel by calling NukeChannelAsync on the authenticated API. It first ensures there is an authenticated connection and a non-empty channel; if either condition is not met, it returns early. Otherwise, it invokes the server-side NukeChannelAsync with the selected channel. +HandleCmdNukeChannel is a private asynchronous command handler that triggers a channel-nuke operation for the currently selected channel. It validates preconditions by ensuring the user is authenticated and a channel is selected, then delegates to `_conn.Api!.NukeChannelAsync(channel)` to perform the action; if preconditions are not met, it exits without issuing a request. ## Remarks -This method encapsulates the preconditions required to perform a channel-nuke, keeping the UI-orchestrator code free from direct API calls. It acts as a gatekeeper, ensuring that only an authenticated user with a selected channel can trigger the destructive server-side operation. The actual deletion is delegated to the API via NukeChannelAsync; thus, changes to the nuking behavior should be made at the API level. +Centralizes precondition checks for a destructive action and encapsulates the invocation pattern for nuking a channel, aiding consistency across UI commands and simplifying testing. It reads the target channel from `_mainWindow.CurrentChannel` and relies on `_conn.IsAuthenticated` to gate the API invocation, delegating the actual work to the API client. This separation makes unit testing easier by isolating the orchestration logic from the network call. + +## Notes +- Be aware that the call uses `_conn.Api!`, so if the API client is not initialized even when authenticated, a `NullReferenceException` can occur. +- Exceptions from [`NukeChannelAsync`](Services/ApiClient.cs.md) are not handled here and will bubble to the caller. + +--- + +### HandleCmdOpenProfile +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method -## Example ```csharp -// Example usage from within the same class -await HandleCmdNukeChannel(); +private Task HandleCmdOpenProfile(string? username) ``` +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `username` | `string?` | — | + +**Returns:** `Task` + + +Acts as a compact command handler that opens the profile view for the given `username`. It forwards the request to the UI layer by scheduling `HandleViewProfile(username)` through `InvokeUI`, and returns `Task.CompletedTask` to satisfy its asynchronous contract. + +## Remarks +This method acts as a UI-thread orchestration boundary: it guarantees that `HandleViewProfile` executes on the UI thread via `InvokeUI`, while keeping the command logic separate from view navigation. By returning a completed `Task`, it preserves an async-compatible signature without awaiting the UI action, which helps keep the caller's flow linear and testable. + ## Notes -- The method relies on _conn.IsAuthenticated and a non-empty current channel; if _conn.Api is null at this point, the null-forgiving access (_conn.Api!) could produce a NullReferenceException. Ensure the API surface is initialized after authentication. -- The operation is destructive; it intentionally performs a server-side action only when preconditions are satisfied, and it swallows precondition failures by returning early without throwing. -- This method does not guard against changes to authentication or channel state between the precondition checks and the API call, so consider synchronization if those state changes can occur concurrently. +- The returned `Task` is completed immediately; the actual profile opening occurs on the UI thread and is not awaited by the caller. --- @@ -881,15 +977,13 @@ private Task HandleCmdOpenServers() **Returns:** `Task` -Dispatches a UI action to open the saved servers UI by invoking HandleSavedServersRequested on the UI thread, and returns a completed Task. It acts as a command-handling bridge that does not perform long-running work itself, delegating the actual UI work to the UI layer. +Handles the Open Servers command by dispatching the UI operation to show saved servers. It forwards to `HandleSavedServersRequested` via `InvokeUI` and returns `Task.CompletedTask`, making it a minimal adapter in the command pipeline that ensures the UI logic runs in the proper context. ## Remarks -Helps keep command-handling code decoupled from UI-thread specifics. This abstraction lets the orchestrator trigger UI flows without assuming any UI state or thread affinity, and it centralizes the UI-dispatch pattern via InvokeUI. +It acts as a small bridge between the command-handling path and the UI flow, ensuring the actual UI logic runs on the appropriate UI thread via `InvokeUI`. Because it returns `Task.CompletedTask`, the method itself does not perform asynchronous work; any long-running processing must be offloaded inside `HandleSavedServersRequested` or its downstream actions. ## Notes -- The returned Task is completed immediately; the actual UI work runs asynchronously on the UI thread. -- Exceptions raised by HandleSavedServersRequested are not propagated through the returned Task and should be handled within the UI path. -- This method is private and intended for internal orchestration; callers should not rely on its Task representing the completion of the UI action. +- Avoid UI-thread blocking: this wrapper returns immediately; long-running work should not run on the UI thread; if `HandleSavedServersRequested` starts long tasks, ensure they are offloaded appropriately. --- @@ -904,15 +998,13 @@ private Task HandleCmdQuit() **Returns:** `Task` -HandleCmdQuit is a private command handler that initiates application shutdown by marshaling a stop request to the UI thread and returning a completed Task. It is intended for quit commands where the actual stop must be executed within the UI context. +Handles the quit command by marshaling a stop request to the UI thread through `InvokeUI`, which executes `_app.RequestStop()`. It then returns `Task.CompletedTask`, allowing callers to continue asynchronously while the application shutdown proceeds on the UI thread. ## Remarks -By funneling the stop through InvokeUI, this symbol ensures the shutdown sequence runs on the UI thread, preventing cross-thread access issues. It centralizes quit behavior in the orchestrator, keeping UI concerns isolated from the command-processing path. The method returns a completed Task to maintain an asynchronous signature, while the actual stopping occurs asynchronously on the UI thread via RequestStop. If the UI thread handling fails, the shutdown may surface as an exception in that context. +This tiny wrapper ensures the quit action is performed on the UI thread, preventing cross-thread access issues when altering the application's stopping state. By returning a completed `Task`, it preserves an async-friendly signature without awaiting the (potentially long) shutdown sequence. ## Notes -- It does not await the stop; the caller should not assume the application is fully stopped after this method returns. -- Exceptions during UI marshaling or in the UI thread may propagate outside this method. -- Assumes _app is non-null and that RequestStop is safe to call on the UI thread. +- The returned `Task` completes immediately; the actual stop is performed on the UI thread via `_app.RequestStop()`, so awaiting this method does not wait for shutdown to finish. --- @@ -933,10 +1025,13 @@ private Task HandleCmdRevokeInvite(string code) **Returns:** `Task` -HandleCmdRevokeInvite revokes an invite by code when the user issues the revoke command. It first ensures the client is authenticated and that a channel is currently selected; if either check fails, it completes without performing any action. When both conditions are met, it runs the revoke operation asynchronously against the API and, after revocation, inserts a system message into the active channel confirming that the invite has been revoked, using the code formatted in upper-case via ToUpperInvariant(). +HandleCmdRevokeInvite processes the revoke invite command by validating authentication (`_conn.IsAuthenticated`) and the current channel (`_mainWindow.CurrentChannel`); if both conditions are met, it revokes the invite via `_conn.Api!.RevokeInviteAsync(code)` and posts a channel system message: `Invite {code.ToUpperInvariant()} revoked.`. ## Remarks -Encapsulates the command-handling concerns for invites by coordinating authentication state, channel context, API invocation, and UI feedback. The early returns prevent unnecessary API calls and potential exceptions in invalid contexts. Using RunAsync ensures the UI remains responsive and that failures surface with a clear error message ("Failed to revoke invite"). +Conceptually, it serves as a bridge between user input, network action, and channel feedback. It uses `RunAsync` to perform the revoke without blocking the UI thread and `InvokeUI` to surface the confirmation via `_messageManager.AddSystemMessage` in the active channel. It relies on `_conn.Api` being non-null after authentication and on `_mainWindow.CurrentChannel` providing a valid destination; if those preconditions are missing, the method is a no-op. + +## Notes +- Uses `_conn.Api!` (null-forgiving) to call [`RevokeInviteAsync`](Services/ApiClient.cs.md); if `Api` is null, this can throw. --- @@ -957,14 +1052,10 @@ private Task HandleCmdSendAction(string text) **Returns:** `Task` -HandleCmdSendAction orchestrates sending a CTCP ACTION message to the currently selected channel. It guards against being disconnected or lacking a channel, and when invoked, schedules a background send that first unlocks the room for sending and then dispatches the formatted action payload through the established connection, preserving the encryption used for ordinary messages. +HandleCmdSendAction processes a CTCP ACTION command (the /me action) by routing it through the standard message path. It validates that there is an active `_conn` connection and a non-empty current channel, returning immediately otherwise; when both exist, it uses `RunAsync` to asynchronously ensure the room is unlocked via `EnsureRoomUnlockedForSendAsync` and then sends the formatted action with `_conn.SendMessageAsync(channel, MessageConventions.FormatAction(text))`, so CTCP content benefits from the same encryption and sending pipeline as regular messages. ## Remarks -This method centralizes the CTCP ACTION sending logic: it validates the connection and channel state, formats the action text using the project’s action conventions, and routes the payload through the same encrypted send path used for regular messages. By gating the unlock step with EnsureRoomUnlockedForSendAsync, it respects per-channel locking and avoids sending actions to rooms that are not ready for sending. The CTCP ACTION content therefore benefits from the same encryption, routing, and error handling as normal messages. - -## Notes -- The actual network send is performed asynchronously inside RunAsync; failures surface through the RunAsync error path with the message 'Send failed'. -- If EnsureRoomUnlockedForSendAsync(channel) returns false, no message is sent; the operation aborts gracefully. +This abstraction centralizes CTCP ACTION formatting via `MessageConventions.FormatAction` and guarantees that action content is subject to the same encryption and room-state checks as normal messages, preventing edge cases where an action could bypass the standard safeguards. --- @@ -985,16 +1076,15 @@ private Task HandleCmdSendBanner(string text) **Returns:** `Task` -Handles the user command to render and send an ASCII banner to the currently selected channel. It validates connectivity and a non-empty channel, renders the banner via AsciiBannerService.Render, and, if rendering succeeds, dispatches a background task to ensure the room is unlocked before sending the banner over the active connection. If rendering yields nothing, it surfaces a UI error detailing the allowed characters and the maximum input length sourced from AsciiBannerService.MaxInputLength. +Handles the banner sending workflow: it returns early if `_conn.IsConnected` is false or the current channel (obtained from `_mainWindow.CurrentChannel`) is null or empty; otherwise it renders the ASCII banner from the input text with `AsciiBannerService.Render(text)` and, if rendering yields `null`, shows an error via `InvokeUI` about allowed characters and the maximum input length (`AsciiBannerService.MaxInputLength`) before returning. When rendering succeeds, it schedules an asynchronous operation with `RunAsync` that first awaits `EnsureRoomUnlockedForSendAsync(channel)` and, if that succeeds, sends the banner with `_conn.SendMessageAsync(channel, banner)`. ## Remarks -Consolidates the end-to-end flow from user command to network transmission by coordinating the UI layer, the ASCII banner renderer, and the messaging connection. It delegates permission checks to EnsureRoomUnlockedForSendAsync and offloads the actual send to a background task via RunAsync, preserving UI responsiveness and isolating concerns among rendering, validation, and transport. +This method acts as a focused orchestrator bridging UI feedback, banner rendering, and network dispatch. By delegating rendering to [`AsciiBannerService`](../EchoHub.Core/Services/AsciiBannerService.cs.md) and gating the actual send behind `EnsureRoomUnlockedForSendAsync`, it keeps concerns separated and avoids blocking the UI thread. It relies on `InvokeUI` to marshal error messages to the UI thread for a responsive user experience. ## Notes -- Early exits (not connected or no channel) return immediately without user-facing feedback. -- Rendering failures produce a user-facing error rather than throwing, tying the UX to the banner rendering service. -- The error message reflects AsciiBannerService.MaxInputLength, tying user feedback to the configured rendering limits. -- The banner is sent only after EnsureRoomUnlockedForSendAsync(channel) confirms the room is unlocked. +- Early exits occur when `_conn.IsConnected` is false or the current channel is empty; the method completes with no exception. +- If rendering fails, the user receives a UI error explaining the allowed characters and the maximum input length via `AsciiBannerService.MaxInputLength`. +- The actual send is performed asynchronously within `RunAsync` and is labeled with the failure caption "Send failed" to aid troubleshooting. --- @@ -1016,17 +1106,7 @@ private Task HandleCmdSendFile(string target, string? size) **Returns:** `Task` -Handles the /send command to attach local files or transmit a URL to the current chat channel, validating authentication and a selected channel; URLs are sent immediately for non-encrypted channels while local files are staged for a single subsequent message up to the attachment limit, and any provided size flag updates the default ASCII size for future sends. - -## Remarks - -URLs are sent immediately when allowed by the channel's encryption state, bypassing the staging area; local files are queued in a staging collection and dispatched together with a caption when the user confirms. This separation ensures encrypted channels remain protected and enforces HubConstants.MaxAttachmentsPerMessage to prevent oversized messages; UI refresh via the staging tray keeps the user informed of what will be sent. - -## Notes - -- URL sends are blocked in encrypted channels; users see an error guiding them to download the file and send it instead. -- If the user is not authenticated, not connected, or no channel is selected, the method returns a completed task without user feedback. -- The ASCII size flag updates a global default for subsequent sends rather than applying to the current staged set. +HandleCmdSendFile orchestrates sending a target from the UI by validating preconditions, distinguishing between a URL and a local file, and routing the action accordingly. It first bails out early if the connection is not authenticated or not active, and if there is no current channel selected. If the target resolves to a HTTP(S) URL (detected via `Uri.TryCreate(target, UriKind.Absolute, out var uri)` with a scheme of `http` or `https`), the method attempts to send the URL through the API by invoking `_conn.Api!.SendUrlAsync(channel, target, size)` inside a background `RunAsync` task, but only when the current channel has no room keys and is not encrypted; otherwise it shows an error message stating that sending by URL isn\'t available in encrypted channels. For local files, it enforces a maximum per-message attachment limit using `HubConstants.MaxAttachmentsPerMessage`, and if the limit is reached it displays an error. The function also respects an optional ASCII size hint by evaluating `NormalizeAsciiSize(size)` and, if present, updating `_config.DefaultAsciiSize`. Finally, it stages the target by adding it to `_stagedAttachments` and triggers a UI refresh of the staging tray via `RefreshStagingTray`, so that pressing Enter sends all staged items as one message with the typed caption. --- @@ -1047,16 +1127,13 @@ private Task HandleCmdSetAsciiSize(string args) **Returns:** `Task` -Opens or sets the ASCII-art size for attached images. If a recognizable argument is supplied, the size is applied immediately; otherwise, a UI dialog prompts the user to choose Small, Medium, or Large, and the chosen size is persisted as a preference for future attachments. +Handles the command to configure the ASCII rendering size for attached images. If an explicit size is provided via the `args` parameter, it parses and applies it immediately with `NormalizeAsciiSize` and `ApplyAsciiSize`, marshaling the update to the UI thread via `InvokeUI`. If no argument is given, it prompts the user with a `MessageBox.Query` to pick between Small, Medium, or Large, and applies the chosen size (the selection is persisted as a preference for future attachments). ## Remarks -This method centralizes the user experience for configuring ASCII art size by handling both direct argument parsing and an interactive picker. Parsing is delegated to NormalizeAsciiSize, and the actual update is performed by ApplyAsciiSize, with UI-affecting work marshaled through InvokeUI to ensure thread-safety. The two-path design supports quick scripted usage and an explicit, user-driven selection while keeping behavior consistent. +This method centralizes the ASCII size configuration for the image-attachment flow. It cleanly separates argument-based updates from the interactive prompt while ensuring all size changes run on the UI thread through `InvokeUI`. The persistent preference is applied to future attachments, providing a consistent rendering size across sessions. ## Notes -- If the argument does not yield a non-null flag, the interactive picker is shown; pressing Cancel results in no change. -- The mapping from the picker result to the internal values is explicit (Small -> "s", Medium -> "m", Large -> "l"). -- The method completes its Task synchronously from the caller's perspective; UI updates are executed asynchronously via InvokeUI. - +- The method returns `Task.CompletedTask` in both branches; awaiting it does not guarantee that the size change has completed, since the actual application happens asynchronously on the UI thread via `InvokeUI`. --- @@ -1077,16 +1154,16 @@ private async Task HandleCmdSetAvatar(string target) **Returns:** `Task` -Handles the Set Avatar command by updating the user's avatar when the client is authenticated. It uploads the specified target avatar via AvatarHelper.UploadAsync using the current API client, and on success posts a system message in the current channel to indicate that the avatar was updated. If an error occurs, the method logs the exception and shows a user-facing error dialog with the exception message. +Handles the command to set the avatar by first short-circuiting when `_conn.IsAuthenticated` is false. When authenticated, it calls `AvatarHelper.UploadAsync(_conn.Api!, target)` to upload the avatar. If the upload succeeds and there is a current channel (`_mainWindow.CurrentChannel` is non-empty), it uses `InvokeUI` to add a system message `Avatar updated.` to that channel via `_messageManager.AddSystemMessage`. If an exception occurs during upload, it is logged with `Log.Error(ex, "Avatar upload failed for {Target}", target)` and a UI error is shown via `InvokeUI` calling `_mainWindow.ShowError(`Avatar upload failed: {ex.Message}`)`. ## Remarks - -This method acts as an orchestration boundary between authentication, server communication, and UI feedback. It delegates the actual upload to AvatarHelper.UploadAsync and uses InvokeUI to marshal UI updates onto the UI thread. Because the method ignores the UploadAsync result, its success is determined solely by the absence of an exception. +Remains focused on bridging authentication state, backend interaction, and user feedback. It delegates the actual upload to [`AvatarHelper`](Services/AvatarHelper.cs.md), while UI feedback is mediated through `InvokeUI` and `_messageManager` to keep the user informed of results. The method demonstrates a pattern of gating backend calls behind authentication and marshaling UI updates to the correct thread. ## Notes +- The call is a no-op if `_conn.IsAuthenticated` is false, ensuring unauthenticated commands cannot trigger avatar uploads. +- Exceptions are caught and surfaced to the user via a UI error message, while also being logged for diagnostics; consider safer messaging in production to avoid leaking internal details. +- The success notification is emitted only when there is an active channel (`_mainWindow.CurrentChannel` not empty); in a channelless context, the only effect is the backend upload. -- If not authenticated, the method returns early with no side effects. -- The error path surfaces ex.Message to the user, which may reveal internal details; consider masking sensitive information in production. --- @@ -1107,91 +1184,94 @@ private async Task HandleCmdSetColor(string color) **Returns:** `Task` -Handles a color-setting command by updating the current user's nickname color via the profile API, but only when the client connection is authenticated. If not authenticated it exits early without performing any update. When authenticated, it constructs an UpdateProfileRequest with NicknameColor set to the supplied color and invokes UpdateProfileAsync on the API client to persist the change. +HandleCmdSetColor is a command handler that, when the client is authenticated, updates the current user's nickname color on the server. If the client is not authenticated, it returns early without issuing any API calls; when authenticated, it builds an [`UpdateProfileRequest`](../EchoHub.Core/DTOs/ProfileDtos.cs.md) with `NicknameColor: color` and awaits `_conn.Api!.UpdateProfileAsync(...)` to persist the change. ## Remarks -Acts as a small command handler within AppOrchestrator that translates a user-issued color command into a persistence operation. It cleanly separates command validation (authentication check) from the transport to the backend profile service, delegating the actual update to the API layer. +Acting as a small facade around the profile update API, this symbol centralizes the handling of a user-facing color-change command and the corresponding server update. It relies on `_conn.IsAuthenticated` to gate the operation and on `_conn.Api` being non-null after authentication (the null-forgiving operator signals this expectation). This keeps color customization logic isolated in one place, simplifying testing and future enhancements to command handling. ## Notes -- The method relies on _conn.IsAuthenticated being accurate; if that state changes between the check and the API call, updates could occur inconsistently. -- It assumes _conn.Api is non-null after authentication (using the null-forgiving operator). If Api is unexpectedly null, a NullReferenceException may be thrown. -- There is no input validation on the color value; invalid colors may be rejected by the backend or cause API-side validation errors. -- Exceptions from UpdateProfileAsync propagate to the caller; callers should handle potential failures when updating the profile. +- No local validation of `color` is performed here; invalid values may cause the API call to fail. Validate or constrain `color` prior to invocation if necessary to avoid server rejection. + --- -### HandleCmdSetNick +### HandleCmdSetDownloadPath > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private async Task HandleCmdSetNick(string displayName) +private Task HandleCmdSetDownloadPath(string args) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `displayName` | `string` | — | +| `args` | `string` | — | **Returns:** `Task` -Handles nickname changes by persisting the new DisplayName to the server when the client is authenticated, and then updates the UI to reflect the new nickname and a connected status. If the client is not authenticated, it exits early without performing any network or UI changes. +HandleCmdSetDownloadPath updates the application's download folder. If called with an argument, it applies that path directly; otherwise it opens the OS-native folder picker (when available) and updates the path based on the user's selection, or informs the user if the picker is unavailable or the operation is cancelled. ## Remarks -Centralizes the nickname-change flow inside the AppOrchestrator, bridging the command, network call, and UI update. The UI updates are marshaled via InvokeUI to ensure thread affinity, and the Api reference is assumed non-null once IsAuthenticated holds, as evidenced by the null-forgiving operator. +This private command handler centralizes the logic for configuring the download directory, supporting both direct path input and interactive picking. It initializes the target directory from `_config.DownloadPath` or `GetDownloadDir()`, invokes the native picker via `NativeFolderPicker.PickFolderAsync(current)`, and applies the outcome on the UI thread using `InvokeUI`. It treats three outcomes: `PickerOutcome.Chosen` (set path to `result.Path`), `PickerOutcome.Cancelled` (show "Download folder unchanged."), and `PickerOutcome.Unavailable` (show a hint that no native picker exists and offer manual setup). ## Notes -- The method uses _conn.Api! to pass UpdateProfileRequest to the API, relying on IsAuthenticated to guarantee Api is non-null; if that guarantee ever fails, a NullReferenceException could occur. -- No input validation of displayName is performed here; validation and sanitization should occur at higher layers or via server-side checks. +- Calling with a non-empty `args` bypasses the picker and completes after `SetDownloadPath(args.Trim())`. +- If the native folder picker is unavailable (headless environments or missing tooling), the user is guided to set the path directly using `/downloadpath `. +- The method uses `RunAsync` to perform the picker operation asynchronously and marshals UI updates to the main thread via `InvokeUI`. --- -### HandleCmdSetStatus +### HandleCmdSetNick > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private async Task HandleCmdSetStatus(UserStatus? status, string? message) +private async Task HandleCmdSetNick(string displayName) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `status` | `UserStatus?` | — | -| `message` | `string?` | — | +| `displayName` | `string` | — | **Returns:** `Task` -Handles the command to set the current user's status and optional message. It only executes when the client is connected; it determines the final status and message from the provided arguments, preserving existing values when an argument is omitted and clearing the message when an empty string is supplied. The method updates the remote server via UpdateStatusAsync and then synchronizes the local session state. +Handles the command to set the user’s nickname by updating the authenticated user’s profile through the client API and then refreshing the UI to reflect the new display name and the connected status. If the user is not authenticated, the method exits early without performing any update. ## Remarks -This abstraction centralizes how a user-initiated status change is applied across both server and client state. It ensures consistent behavior for preserving or clearing values and guarantees the UI and session reflect the server after a successful update. The operation is a no-op when the connection is unavailable, avoiding unintended network activity. +This method acts as a small bridge between authentication, the API client, and the UI, centralizing the nickname update behavior to keep command handling consistent. It relies on the convention that an authenticated session provides a non-null `Api` and uses `InvokeUI` to marshal UI updates onto the main thread. ## Notes -- Early return when not connected means there is no network call or local state change in offline scenarios. -- Null status or null message preserves the current values; an empty message clears the status message. -- The server update is performed before mutating the local session; if UpdateStatusAsync throws, the session remains unchanged. +- Update operations may fail due to network or server errors; no internal retry logic is present here, so callers should handle exceptions as appropriate. +- The `DisplayName` is passed directly via [`UpdateProfileRequest`](../EchoHub.Core/DTOs/ProfileDtos.cs.md) without local validation; ensure inputs are validated by callers to avoid invalid nicknames. +- The code uses a null-forgiving `!` on `_conn.Api`, assuming it is non-null when `_conn.IsAuthenticated` is true; if this invariant is violated, a `NullReferenceException` could occur. + +--- -## Dependencies -- _conn -- _session +### HandleCmdSetStatus +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private async Task HandleCmdSetStatus(UserStatus? status, string? message) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `status` | `UserStatus?` | — | +| `message` | `string?` | — | + +**Returns:** `Task` -## Dependency APIs -- _conn.IsConnected: bool -- _conn.UpdateStatusAsync(UserStatus newStatus, string? newMessage): Task -- _session.Status: UserStatus -- _session.StatusMessage: string? -## Symbol To Document -- Name: HandleCmdSetStatus -- Kind: method -- File: src/EchoHub.Client/AppOrchestrator.cs -- Language: csharp -- ID: 2eb21054-d449-44d8-a265-4b55287688a4 +HandleCmdSetStatus applies a requested status and optional message for the current user. If the client is not connected, the method exits early and makes no changes. With the two parameters, a null `status` preserves the current session status, while a non-null `status` overrides it; for the `message` parameter, a null value keeps the existing message and an empty string clears it, enabling commands like `/status away` to change the status without erasing the message, or `/status msg brb` to set a new message while keeping the current status. It pushes the updated values to the server via `_conn.UpdateStatusAsync(newStatus, newMessage)` and then synchronizes the in-memory session by assigning `_session.Status` and `_session.StatusMessage`. --- @@ -1212,14 +1292,10 @@ private Task HandleCmdSetTheme(string name) **Returns:** `Task` -Marshals a theme-change request to the UI thread via InvokeUI by invoking HandleThemeSelected(name). It does not perform the theme switch itself; instead it queues the work on the UI thread and returns a completed Task. This is useful when a command handler or non-UI code needs to trigger a theme change while preserving proper UI thread affinity. +`HandleCmdSetTheme` acts as a command handler that, given a theme name, schedules the actual theme application on the UI thread by invoking `HandleThemeSelected(name)` through `InvokeUI`. It then returns `Task.CompletedTask` to integrate with asynchronous command pipelines without awaiting the UI work. ## Remarks -HandleCmdSetTheme acts as a small bridge between command reception and UI state mutation. By decoupling the theme-name parameter from the actual UI update, it keeps non-UI layers free of UI-thread requirements and centralizes the invocation in InvokeUI. The pattern helps prevent cross-thread access issues while keeping a concise, test-friendly signature. - -## Notes -- Awaiting the returned Task does not guarantee the theme has been applied; the UI action runs asynchronously on the UI thread after this call returns. -- Exceptions raised during HandleThemeSelected occur on the UI thread and are not surfaced through this Task; consider handling errors inside the UI action or via a global UI exception handler. +By using `InvokeUI`, the method ensures that UI-affecting work runs on the UI thread, avoiding cross-thread access violations when applying a new theme. The method is a lightweight bridge between the command surface and the UI logic, returning a completed `Task` to keep async-call sites simple and non-blocking while the actual work is dispatched to the UI context. --- @@ -1240,15 +1316,14 @@ private async Task HandleCmdSetTopic(string topic) **Returns:** `Task` -HandleCmdSetTopic is the internal handler for the user command that sets the topic of the current channel. It first guards against an unauthenticated user and a missing channel context; if either condition is not met, it exits without performing any update. When both checks pass, it updates the channel topic via the API, and on success it marshals back to the UI thread to refresh the display and emit a system message confirming the new topic. If an error occurs during the API call or UI update, it surfaces an error message to the user. +The `HandleCmdSetTopic` method processes the user command to set the topic of the currently selected channel. It first ensures the user is authenticated via `_conn.IsAuthenticated` and that a channel is selected (`_mainWindow.CurrentChannel` is not null or empty); if either check fails, it exits without making changes. When both checks pass, it calls the API via `_conn.Api!.UpdateChannelTopicAsync(channel, topic)` to apply the new topic, and, on success, updates the UI (`_mainWindow.SetChannelTopic`) and logs a system message through `_messageManager.AddSystemMessage`. If an exception occurs, it surfaces an error using `_mainWindow.ShowError`. ## Remarks -This method serves as the glue between the command processing layer, the network API, and the user interface. It encapsulates the end-to-end workflow for a topic update: validation of preconditions, server-side update, and immediate UI feedback, ensuring consistent state across the client and server while minimizing duplication of error handling across the command pipeline. +This method encapsulates a small, user-initiated operation that spans authentication, network, and UI concerns. It coordinates the backend update and the corresponding UI feedback, ensuring that the channel's topic appears consistent after a successful API call, or an error is surfaced otherwise. The use of `InvokeUI` ensures UI updates run on the main thread, preserving thread-safety while the operation executes asynchronously. ## Notes -- The API reference is accessed with a null-forgiving operator (_conn.Api!), so a null Api client can result in a NullReferenceException. Consider guarding Api’ availability or ensuring lifecycle management guarantees a non-null Api before invocation. -- If authentication is missing or there is no active channel, the method returns without user-facing feedback, which means callers should ensure appropriate UX messaging when these preconditions fail. -- Exceptions raised by the API call or by UI updates invoked in the success path are caught and surfaced as a user-visible error message; such exceptions are not propagated to the caller. +- Silent early returns on unauthenticated state or missing channel can surprise users; consider surfacing feedback earlier in the flow. +- The null-forgiving operator on `_conn.Api` assumes the API client is non-null after authentication; if it isn't, a `NullReferenceException` may occur. --- @@ -1263,14 +1338,10 @@ private async Task HandleCmdTestSound() **Returns:** `Task` -When invoked, this private async handler triggers a test playback of the notification sound by delegating to the injected _notificationSound and awaiting PlayTestAsync. It serves as a small command-handler within AppOrchestrator to validate audio feedback without blocking the command processing flow. +Handles the `HandleCmdTestSound` command by asynchronously invoking the underlying `_notificationSound.PlayTestAsync()` to emit a test notification sound. This method is a targeted bridge that lets the orchestrator trigger audible verification of the notification path without exposing playback specifics to higher-level command logic. Use it during development or diagnostics when you need to confirm that notification sounds play correctly. ## Remarks -Acts as a thin orchestration boundary, delegating to the notification sound service. This keeps the command-handling flow decoupled from the specifics of sound playback, enabling the sound provider to be swapped or mocked in tests. Being private ensures it's only called from within the orchestrator's command-processing logic, preserving encapsulation. - -## Notes -- No cancellation token is passed to PlayTestAsync from this wrapper; if cancellation is required, consider extending the API or wiring cancellation from the caller. -- Exceptions from PlayTestAsync will bubble up to the caller; consider adding error handling if resilience is needed in the command pipeline. +By encapsulating the playback call behind a private method, the code keeps command handling decoupled from the concrete sound implementation, easing testing and future substitutions of `_notificationSound`. This small abstraction centralizes the test-path behavior in one place, so changes to how test sounds are produced won't ripple through the command routing. --- @@ -1291,14 +1362,15 @@ private async Task HandleCmdUnbanUser(string username) **Returns:** `Task` -Unbans a user by delegating to the client API after confirming the current connection is authenticated. If the connection is not authenticated, the method returns immediately without performing any API call. When authenticated, it invokes Api.UnbanUserAsync with the provided username and awaits its completion, ensuring the operation completes asynchronously without blocking the caller. +Handles the unban-user command by first checking `_conn.IsAuthenticated`; if not authenticated, it exits without making a request. When authenticated, it forwards the request to the server by awaiting `_conn.Api!.UnbanUserAsync(username)`. ## Remarks -This method serves as a small command-handler primitive within the orchestration layer. It encapsulates the authentication check and the unban operation, so higher-level command wiring does not need to duplicate this guard. By awaiting the API call, it preserves responsiveness and enables proper exception propagation to the caller if the unban fails. The null-forgiving operator on Api expresses the expectation that the API client is prepared after a successful authentication handshake. +This method encapsulates the small, command-level action of unbanning a user behind an authentication gate. Keeping the authentication check and the API call in one place reduces duplication across command handlers and makes the flow easier to modify (for example, to add logging or auditing around unban operations). The implementation relies on the invariant that a valid, non-null `_conn.Api` is available when `_conn.IsAuthenticated` is true; the null-forgiving operator expresses this contract but could surface a `NullReferenceException` if the invariant is violated. ## Notes -- Exceptions from Api.UnbanUserAsync may propagate to the caller; callers should handle errors at a higher level. -- If invoked while not authenticated, the method performs no action and returns immediately. +- Silent no-op when unauthenticated; callers should provide appropriate user feedback at a higher level if needed. +- The call uses the null-forgiving operator on `_conn.Api`, which assumes a non-null API client when authenticated; ensure the invariant is maintained to avoid `NullReferenceException`. + --- @@ -1319,15 +1391,14 @@ private async Task HandleCmdUnmuteUser(string username) **Returns:** `Task` -Unmutes a user by delegating to the remote API after verifying that the current session is authenticated. If the client is not authenticated, the method returns immediately without issuing any API call. +Unmutes a user by username by forwarding the request to the backend API after validating the connection is authenticated. If the client is not authenticated, it returns early and does not call the API. ## Remarks -This method is a small piece of the command-handling layer in AppOrchestrator. It couples the authentication check to the concrete unmute operation, ensuring that unmute requests are only sent to the API when a user is authenticated. By encapsulating this flow in one place, it keeps higher-level command logic focused on parsing input while the API interaction remains centralized and straightforward. +This small wrapper isolates the moderation action from the underlying API call, enforcing the authentication precondition and delegating the actual unmute to the API client. It helps keep higher-level command handling uniform by providing a single path for unmute operations. It relies on the API client being initialized when authentication is established, tying its correctness to the lifecycle that creates `_conn.Api`. ## Notes -- No error handling within this method; any exception thrown by UnmuteUserAsync will propagate to the caller. -- If _conn.Api is null even after authentication, the null-forgiving operator is used, which will throw at runtime if the API reference is missing. -- The method is private, so its behavior is only observable within the containing class (and tests) and is not part of the public API. +- If `_conn.IsAuthenticated` is true but `_conn.Api` is null, the call will throw due to the null-forgiving operator used on `_conn.Api`. + --- @@ -1342,14 +1413,10 @@ private void HandleConnect() **Returns:** `void` -HandleConnect coordinates the user-initiated connection workflow. It first checks whether a connection is already active and, if so, prompts the user to disconnect before proceeding; if the user confirms, it initiates a disconnect. Otherwise it shows a ConnectDialog to select a server and credentials, logs the attempt, and runs the asynchronous connection. It calls _conn.ConnectAsync and updates the status bar as the connection progresses. If a saved refresh token exists but the attempt fails due to an expired or revoked session, it logs a warning, clears the saved token, informs the user, and aborts the connection attempt. On success, it records the resulting login, clears any pending unlocks, loads the last-read markers for the target server from the saved config, and updates the UI: sets the current user, loads channels, switches to the default channel, loads per-channel histories (honoring lastRead where available), focuses the input, and fetches the list of online users. Finally it persists the selected server configuration. +HandleConnect orchestrates the user-initiated connection flow: if already connected it prompts to disconnect via `MessageBox.Query`, then shows the connect dialog with `ConnectDialog.Show(_app, _config.SavedServers)` and starts an asynchronous connect via `_conn.ConnectAsync`, updating the UI status as it progresses. On success it updates the current user, populates channels, switches to the default channel (`HubConstants.DefaultChannel`), replays channel histories seeded from persisted last-read markers, focuses the input, fetches online users, and persists the server to configuration. ## Remarks -HandleConnect serves as the central flow controller that binds the UI to the authentication/connection protocol and the post-connection UI state. It isolates network logic from the UI by performing the connect work inside RunAsync and marshaling UI updates via InvokeUI; it also coordinates reading and applying per-server state (channels, histories, last-read markers) so the user sees an up-to-date workspace after connecting. - -## Notes -- It only handles a special exception path when a saved refresh token is present; other exceptions bubble to the RunAsync failure handler and surface a generic "Connection failed" message, so callers should be prepared for a generic error state. -- The last-read history seeding relies on the SavedServers list in the config; if the server URL isn't found, lastRead will be null and histories will start from the most recent history loaded from the server. +By centralizing the connect logic, this method coordinates cross-cutting concerns across `_conn`, `_mainWindow`, and [`ConfigManager`](Config/ConfigManager.cs.md) while preserving UI responsiveness through `RunAsync` and `InvokeUI`. It also handles a graceful recovery path when a saved refresh token is invalid: it logs a warning, clears the saved token, resets the UI status to "Disconnected", and prompts the user to reauthenticate. --- @@ -1364,10 +1431,28 @@ private void HandleCreateChannelRequested() **Returns:** `void` -Handles the user-initiated request to create a channel. It validates that the client is authenticated and connected, prompts the user for channel details via a dialog, and, if the user proceeds, performs the creation workflow asynchronously. If a password is provided for the channel, it derives a per-channel join credential and locally wraps a new room key so that the channel content remains end-to-end encrypted (the passphrase never leaves the client). The workflow includes salt generation, key derivation, room key generation, base64-encoding of the salt, and wrapping the room key with the derived key, before sending the resulting wirePassword, saltB64, and wrappedKey to the server. On success, any generated room key is stored for the channel, the client joins the channel, and its history is loaded if present. UI state is updated to show the new channel, set its topic, and switch focus to it, followed by a refresh of online users. If the user cancels the dialog or the channel creation fails, the method exits without side effects. +`HandleCreateChannelRequested` orchestrates the end-to-end flow when a user requests to create a new channel: it validates authentication and connection, collects channel details via `CreateChannelDialog.Show(_app)`, and, if the user confirms, asynchronously creates the channel and updates the UI. If a password is provided, it enforces the minimum length, derives join credentials with [`RoomCrypto`](../EchoHub.Core/Security/RoomCrypto.cs.md), generates a new room key, and wraps the key for secure transmission, sending the password-derived data as `wirePassword`, `saltB64`, and `wrappedKey` to `CreateChannelAsync`. On success, it stores the room key (when present), joins the channel to fetch history, and refreshes the channel list and topic in the UI, finally refreshing online user information. + +## Remarks +It acts as a focused orchestration layer that coordinates input collection ([`CreateChannelDialog`](UI/Dialogs/CreateChannelDialog.cs.md)), security setup ([`RoomCrypto`](../EchoHub.Core/Security/RoomCrypto.cs.md)), server interaction (`_conn.Api.CreateChannelAsync`), and UI state updates (`_mainWindow`, `_messageManager`). The encryption path is isolated to the password flow, ensuring that password-derived credentials and the wrapped room key are prepared locally before transmission. The method is a private responder to a user action and relies on `RunAsync` to surface failures with the message 'Failed to create channel'. + +## Notes +- The work is scheduled via `RunAsync`, so the caller remains responsive; any failure surfaces through the provided error caption (e.g., 'Failed to create channel'). + +--- + +### HandleDeleteChannelRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleDeleteChannelRequested() +``` + +**Returns:** `void` -The method orchestrates UI interaction, server communication, encryption preparation, and post-create UI updates in a single, cohesive flow, thereby providing a predictable and secure channel creation experience from a single entry point. +The `HandleDeleteChannelRequested` method coordinates the delete-channel flow, validating that the user is authenticated and connected and that a non-default channel is selected. If the user confirms, it deletes the channel via the API (`DeleteChannelAsync`), stops tracking the channel, switches the UI to the default channel (`HubConstants.DefaultChannel`), and posts a system message announcing the deletion. It also surfaces error messages if authentication/connection fails, no channel is selected, or the default channel is attempted to be deleted. --- @@ -1388,24 +1473,14 @@ private void HandleDeleteMessageRequested(Guid messageId) **Returns:** `void` -Deletes a message in response to a user-initiated delete request. - -The method first guards against unauthenticated calls by returning early if the connection is not authenticated. When authenticated, it delegates the actual deletion to the server via the API and relies on the server to enforce the hierarchy rule (own message or Mod+ over a strictly lower role) and to broadcast the deletion to all clients. The local message list is updated only in response to that broadcast, not by mutating internal state directly within this method. +HandleDeleteMessageRequested handles a user-initiated request to delete a message identified by `messageId`. It prevents unauthenticated deletions by returning early if `_conn.IsAuthenticated` is false, and, when authenticated, asynchronously calls `_conn.Api!.DeleteMessageAsync(messageId)` via `RunAsync`, relying on server-side enforcement of the hierarchy rule and on the deletion broadcast to refresh the local list. ## Remarks - -This function acts as a thin bridge between the UI action (delete request) and the network operation, deferring permission checks to the server to ensure consistent enforcement across clients. It also centralizes error handling through RunAsync, which surfaces a user-facing message ("Failed to delete message") if the server operation fails. By relying on the server broadcast to refresh local state, it avoids duplicating deletion logic on the client and keeps the UI in sync with server-side state changes. - -## Example - -```csharp -// Called when the user confirms deletion of a message with ID messageId -HandleDeleteMessageRequested(messageId); -``` +By design, this method is a thin orchestrator that encapsulates the authentication check and delegates the deletion to the server. It does not mutate the local message collection directly; instead, the client updates its UI when the server broadcasts the deletion. ## Notes -- The method is a no-op when the connection is not authenticated, avoiding any local or server interaction. -- The local message list is updated via server broadcasts rather than direct mutation inside this method, ensuring consistency with server-side state and other clients. +- No local persistence occurs inside this method; the local view updates in response to the server's deletion broadcast. +- `RunAsync` is supplied with the error message "Failed to delete message" to surface failures consistent with other API operations. --- @@ -1420,54 +1495,65 @@ private void HandleDisconnect() **Returns:** `void` -Handles the disconnection sequence for the EchoHub client orchestrator. It is invoked when the connection to the server is lost or a deliberate disconnect is initiated. The method centralizes teardown steps to ensure a consistent, user-friendly shutdown: it logs the disconnect, clears per-channel user state under a dedicated lock, resets the pending reply to avoid processing stale data, updates the UI to reflect that no reply is expected, persists the last-read state, and then kicks off an asynchronous cleanup of the underlying connection. The UI is updated on the main thread to display a disconnected state after cleanup, preserving responsiveness during teardown. +HandleDisconnect is a private method that gracefully tears down the client’s connection to the server and resets the UI and internal state. It logs the impending disconnect with `Log.Information("Disconnecting from server")`, clears `_channelUsers` under `_channelUsersLock`, sets `_pendingReply` to null, clears the reply target in the UI with `_mainWindow.SetReplyingTo(null)`, and persists the last reads by calling `PersistLastReads()`. It then starts an asynchronous cleanup by calling `_conn.CleanupAsync()` inside `RunAsync` with the labels `"Disconnect error"` and `"Disconnect"`, and on completion clears the UI (`_mainWindow.ClearAll()`) and updates the status bar to `Disconnected` via `_mainWindow.UpdateStatusBar("Disconnected")`. -## Remarks -This abstraction exists to provide a single, well-defined path for disconnect scenarios, reducing the risk of partial teardown and inconsistent UI state across call sites. By guarding channel-user state with a lock, it prevents race conditions during teardown, and by marshaling UI updates through InvokeUI, it maintains proper thread affinity between background work and the UI. Running the cleanup asynchronously ensures the user interface remains responsive while the connection is terminated and resources are released. +--- + +### HandleEditProfile +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method -## Example ```csharp -// Example usage: internally invoked when the underlying connection reports a disconnect -HandleDisconnect(); +private void HandleEditProfile(UserProfileDto? currentProfile) ``` -## Notes -- Clearing _channelUsers is performed under _channelUsersLock to prevent races with other threads that may access the collection during disconnect. -- _pendingReply is set to null to avoid handling a stale or invalid reply after disconnect. -- UI updates are dispatched through InvokeUI to guarantee execution on the UI thread, even when called from a background context. -- The actual connection teardown happens asynchronously via RunAsync, so any resulting UI changes (e.g., the status bar showing "Disconnected") occur after cleanup completes. +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `currentProfile` | `UserProfileDto?` | — | + +**Returns:** `void` +Orchestrates the end-to-end profile-edit operation: displays the [`ProfileEditDialog`](UI/Dialogs/ProfileEditDialog.cs.md) to collect changes, pushes updates to the server via [`UpdateProfileRequest`](../EchoHub.Core/DTOs/ProfileDtos.cs.md) when authenticated, updates the UI with the new display name, optionally uploads an avatar, updates notification preferences, and saves the updated profile as the default preset using `ConfigManager.Save`. + +## Remarks +HandleEditProfile acts as a coordinator that binds together the UI, API, avatar service, and configuration persistence. It ensures that updates are attempted only when `_conn.IsAuthenticated` is true, handles avatar upload failure gracefully by logging and showing an error, and keeps the user experience in sync by updating the main window and status bar when a display name is provided. When an avatar is uploaded, a system message is posted to the current channel to inform users, if one exists. + +## Notes +- If the user is not authenticated, the operation aborts early and makes no changes. +- The UI update of the display name is conditional on `editResult.DisplayName` not being null; otherwise the display name remains unchanged. +- The configuration is saved via `ConfigManager.Save` regardless of avatar upload success, so avatar failures do not automatically roll back server or UI updates. + --- -### HandleEditProfile +### HandleFileDownloadRequested > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void HandleEditProfile(UserProfileDto? currentProfile) +private void HandleFileDownloadRequested(string attachmentUrl, string fileName) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `currentProfile` | `UserProfileDto?` | — | +| `attachmentUrl` | `string` | — | +| `fileName` | `string` | — | **Returns:** `void` -Drives the user-initiated profile editing flow. It opens a ProfileEditDialog pre-populated with the current profile values, and if the user submits changes, it updates the server (when authenticated), refreshes the UI with the new display name, and optionally uploads a new avatar. It also applies any updated notification settings and persists the updated profile as the default preset in the local configuration. - -It encapsulates the end-to-end sequence as an asynchronous operation: show the dialog, validate the result, perform server updates, and reflect changes in both the UI and local config. The operation is guarded by an authentication check and is executed via RunAsync to avoid blocking the UI thread, with careful UI updates performed on the main thread where appropriate. +HandleFileDownloadRequested downloads an attachment from `attachmentUrl` with the given `fileName`. It only proceeds if `_conn.IsAuthenticated` is true; it downloads the file asynchronously, saves it to a deduplicated destination, notifies the UI about progress and the saved path, and, for extensions in `SafeOpenExtensions`, launches the file with the system default application, logging any failure to open. ## Remarks -Centralizes the profile-edit workflow in a single coordinator: it integrates the user dialog, the remote profile update, avatar handling, notification preference updates, and the persistence of a default profile preset. This orchestration ensures consistency across server state, UI presentation (display name and status), avatar state, and locally stored configuration. +This method encapsulates the end-to-end download-and-open pattern, shielding callers from the details of authentication checks, UI messaging, and filesystem operations behind a single, intention-revealing API. It collaborates with `_conn` for authentication, `_messageManager` and `_mainWindow` for user feedback, and `SafeOpenExtensions` to decide when to launch the file automatically, fitting into the app's orchestration layer that responds to file-download requests. ## Notes -- The code uses _conn.Api! after verifying IsAuthenticated; if Api can be null in edge cases, this could throw at runtime. Consider guarding Api against nulls or enforcing a stronger invariant that IsAuthenticated implies a non-null Api. -- Avatar upload failures are isolated: an exception will be logged and a user-visible error shown, but the rest of the profile updates (name, bio, colors, notifications) still apply. - +- If the user is not authenticated, the operation is short-circuited (returns early) with no downloads. +- Automatic opening is gated by `SafeOpenExtensions`; only safe extensions are opened; failures to launch are logged via `Log.Warning` and do not throw. --- @@ -1489,14 +1575,34 @@ private void HandleFilesStaged(string channel, IReadOnlyList files) **Returns:** `void` -Stages a batch of local files (multi-file paste or drag-and-drop) as attachments for the next message. It executes synchronously on the UI thread to ensure the staged attachment list is updated before the message is sent, preventing races with the routing path. If the client is not authenticated or connected, it returns immediately. It adds only up to the remaining attachment slots, reports an error if more files were provided than can be staged, and then refreshes the staging tray. +Stages a batch of local file paths as attachments for the next message. This runs on the UI thread to ensure the staging list cannot race with a subsequent send command, so a multi-file paste won’t slip past the limit. If the connection is not authenticated or not connected, the method exits early; otherwise it appends as many files as can fit within the per-message cap. + +It computes the remaining slots as `HubConstants.MaxAttachmentsPerMessage - _stagedAttachments.Count` and adds up to that many files from `files` via `files.Take(Math.Max(0, slotsLeft))`. If more files are provided than there are slots, it shows an error message to the user: "You can attach at most {HubConstants.MaxAttachmentsPerMessage} files per message." Finally it calls `RefreshStagingTray()` to update the UI. ## Remarks -This method centralizes the per-message attachment staging logic, tying the UI state (`_stagedAttachments`) to the maximum allowed attachments (`HubConstants.MaxAttachmentsPerMessage`). It guarantees a deterministic UI update by running on the UI thread and by refreshing the staging tray after updates, reducing the chance of inconsistent attachment state when users paste many files quickly. +Keeping this logic on the UI thread ensures the staging operation is atomic with respect to user interactions, avoiding interleaving with the sending path. The method relies on `_stagedAttachments` to track current attachments and enforces the per-message cap via `HubConstants.MaxAttachmentsPerMessage`. It also depends on `_conn` to determine whether staging should proceed and on `_mainWindow` to surface errors, with `RefreshStagingTray()` updating the visual staging area. -## Notes -- The channel parameter is currently unused by this method. -- If files.Count exceeds the available slots, only the portion that fits is staged; the user is shown an error message and the extras are ignored. +--- + +### HandleImageOpenRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleImageOpenRequested(string attachmentUrl, string fileName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `attachmentUrl` | `string` | — | +| `fileName` | `string` | — | + +**Returns:** `void` + + +Views an image attachment without saving it to the user's downloads. It requires authentication and branches on whether the current channel is encrypted: for unencrypted rooms it opens a full URL in the default browser (constructing the URL from `_conn.Api.BaseUrl` when needed) via `System.Diagnostics.Process.Start`; for encrypted rooms it only proceeds if the file extension is in `ImageOpenExtensions`, otherwise it delegates to the save flow, decrypts with `DownloadAttachmentAsync`, and opens the resulting temporary file with `Process.Start` after indicating progress via a system message. --- @@ -1518,26 +1624,38 @@ private void HandleImagePasted(string channel, byte[] png) **Returns:** `void` -Handles an image pasted from the clipboard by staging it as a temporary PNG in a per-paste folder so it flows through the same path-based staging and encryption pipeline as regular attachments. The temporary file is deleted once the message is sent. +Stashes a PNG image pasted from the clipboard into the message workflow by writing the raw PNG data to a per-paste temporary folder and registering the file with the current message’s staging pipeline so it flows through the same path-based handling as regular attachments. It only runs when `_conn.IsAuthenticated` and `_conn.IsConnected` are true, enforces the per-message attachment cap via `HubConstants.MaxAttachmentsPerMessage`, and ensures the temporary file is deleted once the message is sent; on failure, the error is logged and the user is notified via `_mainWindow.ShowError`. -If the client is not authenticated or connected, the method returns early without attempting to stage anything. +## Remarks +Centralizes pasted-image handling to keep the user experience consistent with other attachments and to reuse the existing staging/cleanup logic. It creates a unique per-paste folder under the system temp path and stores the image as `image.png` to preserve a familiar display name while allowing multiple pasted images to coexist in a single message. -If the attachment limit for the current message has been reached, it shows an error and returns. +## Notes +- Potential thread-safety trap: `_tempPastedFiles` and `_stagedAttachments` are mutated here without visible synchronization; ensure calls are serialized or synchronized when invoked from multiple threads. -It uses a unique, per-paste folder under the system temporary path (EchoHub/pasted/{8-char}) to preserve the familiar image.png name while letting multiple pasted images coexist in a single message. It writes the PNG as image.png, tracks the path in the internal _tempPastedFiles and _stagedAttachments collections, and refreshes the staging tray UI. +--- -If anything goes wrong, it logs the exception and informs the user via the main window. +### HandleImageSaveRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method -## Remarks +```csharp +private void HandleImageSaveRequested(string attachmentUrl, string fileName) +``` -This symbol acts as a glue between clipboard paste handling and the attachment pipeline, reducing ad-hoc clipboard writes and ensuring consistent processing downstream. By isolating each paste, it avoids file-name clashes and keeps UI behavior predictable (the image shows as image.png, even when multiple pastes exist). It relies on the existing authentication/connection state and the global attachment limit to maintain a robust UX. +**Parameters:** -## Notes +| Parameter | Type | Default | +|-----------|------|---------| +| `attachmentUrl` | `string` | — | +| `fileName` | `string` | — | + +**Returns:** `void` -- No operation occurs if the user is not authenticated or not connected; the method returns quietly in that case. -- Temporary files are not cleaned up by this method; cleanup occurs later in the message lifecycle when the message is sent. -- Pasting images enforces HubConstants.MaxAttachmentsPerMessage; once reached, a user-visible error is shown and gating prevents further staging. +HandleImageSaveRequested is a private method in `AppOrchestrator` that saves an image when a save is requested. It guards the operation with `_conn.IsAuthenticated`, then asynchronously downloads the attachment via `DownloadAttachmentAsync`, moves it to a deduplicated destination under the directory from `GetDownloadDir()` using `DedupPath` and `File.Move`, and reports progress to the UI with `InvokeUI` and `_messageManager.AddSystemMessage`. If authentication is not present, it exits early. + +## Remarks +Combines an authentication guard (`_conn.IsAuthenticated`), asynchronous work (`RunAsync`), and UI feedback via `InvokeUI` and `_messageManager.AddSystemMessage` to keep the user informed while the image is downloaded and moved. It relies on `DownloadAttachmentAsync`, `GetDownloadDir`, `DedupPath`, and `File.Move` to perform I/O in a deduplicated, user-visible way. This arrangement keeps authentication, I/O, and presentation concerns clearly separated in the orchestration flow. --- @@ -1552,16 +1670,7 @@ private void HandleLoadMoreRequested() **Returns:** `void` -Loads the next page of chat history for the currently selected channel when the user requests more messages. It executes asynchronously and prepends the retrieved history to the channel's message list, typically in response to a 'Load more' action in the chat UI. - -## Remarks - -Separates concerns by consolidating the load-more behavior in this method, acting as the bridge between the connection layer, the currently selected channel, and the in-memory history store. It uses RunAsync to perform the network call off the UI thread and InvokeUI to apply the updated history safely to the UI. The per-channel guard (_channelsLoadingMore) prevents overlapping loads for the same channel, preserving responsiveness when users tap repeatedly. - -## Notes - -- Exits early if the client is not connected or no channel is selected, avoiding unnecessary work. -- The fetch uses HubConstants.DefaultHistoryCount as the batch size and computes the offset from the current message count; a finally block ensures the per-channel loading flag is cleared even if the fetch fails, and RunAsync surfaces a 'Failed to load more messages' error to the user. +This private method handles the user-initiated request to load older messages for the currently selected channel. It returns early unless `_conn.IsConnected` and a non-empty `_mainWindow.CurrentChannel` are present, then uses `_channelsLoadingMore` to prevent concurrent loads for the same channel and computes the offset as `_messageManager.GetMessages(channel)?.Count ?? 0`. It runs a background task with `RunAsync` to fetch history via `_conn.GetHistoryAsync(channel, HubConstants.DefaultHistoryCount, offset)` and, on success, invokes `InvokeUI(() => _messageManager.PrependHistory(channel, history))`; finally, the channel is removed from `_channelsLoadingMore`, and any failure surfaces the message `Failed to load more messages`. --- @@ -1576,15 +1685,15 @@ private void HandleLogout() **Returns:** `void` -Orchestrates the end-to-end logout sequence for the client: it logs the logout, persists pending read state, and initiates an asynchronous server logout. It then conditionally clears the saved token based on the API base URL, performs cleanup, and updates the UI to show a disconnected state. +Coordinates the client-side logout sequence when the user signs out: it logs the event, persists the last-read state, and kicks off an asynchronous flow that signs out from the server, clears any saved token tied to the API's [`BaseUrl`](Services/ApiClient.cs.md), performs cleanup, and updates the UI to reflect a disconnected state. ## Remarks -This method centralizes the logout workflow in AppOrchestrator, coordinating server communication, local state cleanup, and UI transitions. It delegates UI changes to InvokeUI to guarantee thread-safety and relies on RunAsync to provide contextual error handling with the 'Logout error' title. +Acts as the centralized logout orchestrator within the app, encapsulating server communication, token management, and UI reset behind a single private method. By using `RunAsync`, it defers error handling to a consistent pathway and keeps UI threading concerns isolated to the `InvokeUI` call. It collaborates with `_conn` for logout/cleanup and with `_mainWindow` to reflect the disconnected state. ## Notes -- ClearSavedToken(baseUrl) is invoked only when baseUrl is non-null, preventing token removal in contexts without a configured API. -- UI updates are scheduled on the main thread via InvokeUI to avoid cross-thread interaction issues. -- If LogoutAsync fails, RunAsync provides the contextual error information ('Logout error'); subsequent steps inside the lambda are contingent on successful logout. +- If `baseUrl` is null, `ClearSavedToken(baseUrl)` is not invoked, so a saved token may remain. +- The logout sequence runs asynchronously via `RunAsync`; the method returns immediately while the operations execute in the background. +- UI updates occur on the UI thread through `InvokeUI` to avoid cross-thread issues. --- @@ -1606,17 +1715,15 @@ private void HandleMessageSubmitted(string channelName, string content) **Returns:** `void` -Handles the submission of a user-typed message for a specific channel. It validates the connection and routes the content through command handling, staged attachments, or a plain text send, while updating the UI with errors or system messages as appropriate. +HandleMessageSubmitted coordinates the user’s message submission in a channel by performing connectivity checks, routing command input through the `_commandHandler`, handling staged attachments via `SendStagedMessage`, and computing a potential `replyTo` for threaded replies before sending asynchronously through `_conn.SendMessageAsync` after ensuring the room is unlocked. It also surfaces command results as either errors or system messages and clears any pending reply once the message is sent. ## Remarks - -This method serves as the central orchestrator for message submission, coordinating the UI layer, command processor, and network transport. It enforces preconditions (an active connection and an unlocked room) and uses asynchronous execution to avoid blocking the UI thread, ensuring proper state transitions such as clearing a pending reply after a successful send. +This function is the single entry point for all message submissions, enforcing connectivity, handling commands, attachments, and reply threading in one place to maintain consistent user experience and state transitions across the chat UI and network layer. ## Notes +- The method has several early returns; a caller cannot assume a message was sent after invoking it. +- The reply-to resolution is channel-sensitive and uses a case-insensitive comparison; mismatched channels will skip applying the pending reply. -- Not connected: shows an error and aborts before performing any network activity. -- Command handling: commands are detected via IsCommand; when HandleAsync returns a result with a non-null Message, the UI is updated either with an error (if IsError) or a system message. -- Message sending paths: if there are staged attachments, a staged message is sent with the caption and attachments; otherwise, if there is a per-channel pending reply, the message is sent with a replyTo ID; in all sending paths, EnsureRoomUnlockedForSendAsync is awaited; after a pending-reply send, ClearPendingReply is invoked on the UI thread. --- @@ -1631,14 +1738,13 @@ private void HandleProfileRequested() **Returns:** `void` -Handles the internal action when a profile is requested by the user. It delegates directly to HandleViewProfile(null), which triggers the shared profile-view workflow for the current user without requiring a specific user identifier. +This private method handles the 'Profile Requested' action by delegating to the shared profile-viewing workflow. It calls `HandleViewProfile` with a `null` argument to trigger the default/current profile display. ## Remarks -By routing through this private method, the code preserves a clean separation between the act of requesting a profile and the details of how a profile is displayed. It keeps the event-name binding lightweight while centralizing the actual view logic in HandleViewProfile. If future requirements ever need to view a non-current profile, this wrapper can be adapted or extended to pass a concrete identifier without broadening the public API. +This forwarding method isolates the action-handling glue from the actual view logic. By funneling through `HandleViewProfile`, the orchestrator maintains a single path for presenting a profile, reducing duplication and centralizing validation or normalization of input when no specific target is provided. It remains a small wrapper around the real work, preserving a clean public surface while reusing the underlying view logic. ## Notes -- The wrapper relies on the null argument to signal 'current user' to the profile viewer; changing the contract of HandleViewProfile would require updating this method. -- Because the method is private, tests must exercise the public paths that lead to this method rather than invoking it directly. +- Relies on `HandleViewProfile` being able to handle a `null` input; if that contract changes, this method will need adjustment. --- @@ -1653,14 +1759,10 @@ private void HandleReplyCancelRequested() => ClearPendingReply() **Returns:** `void` -This private method handles a cancel request for a pending reply by clearing any in-progress reply state. It delegates to ClearPendingReply to perform the reset, providing a semantically meaningful hook within the orchestrator's reply lifecycle. +As a private handler, `HandleReplyCancelRequested` responds to a cancellation signal for the current reply by clearing any pending reply state. It simply delegates to `ClearPendingReply()` to perform the actual cleanup, ensuring the cancellation path does not duplicate the clearing logic elsewhere. ## Remarks -By naming this hook, the code communicates intent: cancellation of a pending reply is a distinct moment in the flow, and all necessary cleanup should be centralized here. This makes future changes to the cancellation behavior easier to implement without touching multiple call sites. - -## Notes -- This method is private; external callers should trigger cancellation through public channels that eventually surface this path. -- It is a synchronous wrapper around ClearPendingReply; no asynchronous operations are performed in this method. +By providing a dedicated private handler, cancellation triggers can be wired to a specific action (e.g., user-initiated cancel or protocol signal) without scattering the cleanup details across callers. The method keeps the cleanup behavior centralized in `ClearPendingReply()` while exposing a clear semantic hook for cancellation paths. --- @@ -1683,15 +1785,41 @@ private void HandleReplyRequested(Guid messageId, string sender, string snippet) **Returns:** `void` -Handles a user action to reply to a specific message. It first checks for an active channel from the main window; if none exists, it returns without altering any UI state. When a channel is present, it records the target message and channel as the pending reply, shortens the provided snippet to a concise preview (40 characters max, with an ellipsis added if truncation occurs), and updates the UI to show who is being replied to along with that preview. +HandleReplyRequested processes a user action to reply to a specific message by establishing the reply context for the current channel and updating the user interface to reflect that intent. If there is no active channel, the method exits early and makes no changes. When a channel exists, it records the target channel and message in `_pendingReply`, truncates the provided snippet to at most 40 characters (appending a Unicode ellipsis when truncated), and updates the main window with a header showing who is being replied to and the short snippet. ## Remarks -Handles the internal UX state for message replies by coupling the selected message (messageId) with its channel and exposing a concise preview to the user. This keeps the reply context consistent across subsequent actions without requiring the higher-level UI to recompute the target context on every interaction. It also prevents initiating a reply when there is no active channel. +By centralizing the reply-flow logic, this method ensures a consistent user experience for replying to messages across the application. It creates a lightweight, UI-facing preview and a stored reply target that subsequent compose/send logic can use to route the reply to the correct channel and message. The operation ties the reply context to the active channel, ensuring the reply target remains meaningful within the current conversation. + +## Example +```csharp +// Example: long snippet is truncated for the reply header +Guid messageId = Guid.NewGuid(); +string sender = "Alice"; +string longSnippet = "This is a long snippet that will be truncated to keep the reply header compact."; +HandleReplyRequested(messageId, sender, longSnippet); +``` ## Notes -- Early return when there is no current channel; the method exits without updating pending state or UI. -- Snippet truncated to 40 characters with a trailing ellipsis (the character '…') when longer, ensuring a compact in-UI preview. -- This method directly manipulates UI state via _mainWindow; callers should ensure execution on the appropriate UI thread to avoid cross-thread exceptions. +- If `CurrentChannel` is null or empty, the method returns immediately and does not modify state. +- Snippet truncation uses 40 characters and appends the Unicode ellipsis '…' when needed. +- The UI update calls `_mainWindow.SetReplyingTo` to reflect the reply context; ensure `_mainWindow` is initialized and that this runs on the UI thread to avoid cross-thread issues. + +--- + +### HandleRollbackRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleRollbackRequested() +``` + +**Returns:** `void` + + +Orchestrates the rollback flow for an in-progress update by restoring from a previously created backup. It first validates the existence of a backup via `UpdateBackupService.BackupExists()`. If none exists, it shows an error dialog through `MessageBox.ErrorQuery` and returns. If a backup is present, it retrieves backup details with `UpdateBackupService.GetBackupInfo()` to display the target version in a confirmation prompt via `MessageBox.Query(_app, "Rollback Update", `$"Restore to version {info?.Version ?? "unknown"}? + +The app will restart."`, "Restore", "Cancel")`. If the user confirms (the result is 0), it calls `UpdateBackupService.RestoreBackup()`, after which `Environment.Exit(0)` terminates the application. Any exception raised during restoration is caught, logged with `Log.Error`, and shown to the user via an error dialog containing the exception message. --- @@ -1706,15 +1834,26 @@ private void HandleSavedServersRequested() **Returns:** `void` -Shows the list of saved servers to the user by reading from the configuration and presenting it in a modal dialog. If there are no saved servers, it informs the user that none exist. +This private UI helper reads the configured list of saved servers from `_config.SavedServers` and presents it to the user. If the list is empty, it informs the user via a message box that no saved servers exist yet. If there are saved servers, it formats each entry as: server name, URL, username (defaulting to `?` when missing), and the last connected date formatted as `yyyy-MM-dd`; if a refresh token is present, it appends ` [session saved]`. The resulting lines are displayed in a `MessageBox.Query` under the title `Saved Servers`. ## Remarks -This method centralizes the presentation logic for saved servers, decoupling data retrieval from display concerns. It relies on the configuration's SavedServers collection and uses a straightforward LINQ projection to build human-readable lines, including a session indicator when a refresh token is present and the last connected date. The formatting rules (username fallback to ? and the optional [session saved] tag) are encapsulated here, making future changes localized to this UI path. +By isolating the saved servers rendering here, the UI layer has a single place to manage how server metadata is presented. It relies on `_config.SavedServers` for data and on `MessageBox.Query` for presentation, which keeps the behavior consistent with other similarly presented lists in the app. If later the display format changes (e.g., additional metadata is shown or localization), only this method needs adjustment. + +--- + +### HandleSearchRequested +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void HandleSearchRequested() +``` + +**Returns:** `void` + + +HandleSearchRequested is a private handler that processes the result of the search dialog by presenting channel names and routing the user to either channel navigation or an action handler. It calls `SearchDialog.Show(_app, _mainWindow.GetChannelNames())` and, if the result is `null`, returns; otherwise it branches on `result.Type` to either switch to a channel via `_mainWindow.SwitchToChannel(result.Key)` and call `HandleChannelSelected(result.Key)`, or dispatch to the appropriate action handler (e.g., `HandleConnect()`, `HandleDisconnect()`, `HandleLogout()`, `HandleProfileRequested()`, `HandleStatusRequested()`, `HandleCreateChannelRequested()`, `HandleDeleteChannelRequested()`, `HandleSavedServersRequested()`), or perform UI operations like toggling the users panel or quitting the app. -## Notes -- The function renders a modal dialog via MessageBox.Query, which blocks until the user dismisses it; callers should ensure this is invoked on the UI thread. -- The last connected date is formatted as yyyy-MM-dd, a culture-invariant representation; if LastConnected can be default or null, this may warrant data validation upstream. -- Username defaults to "?" when missing, and a server is labeled with "[session saved]" only if RefreshToken is non-empty, tying display state to authentication/session data. --- @@ -1729,15 +1868,13 @@ private void HandleStatusRequested() **Returns:** `void` -Handles a user-initiated request to change the current status. It presents StatusDialog to gather a new status and optional message, updates the local session with the chosen values, and, if a connection is active, persists the change asynchronously via the connection. +Handles a user-initiated request to change the current status by presenting a [`StatusDialog`](UI/Dialogs/StatusDialog.cs.md) to collect a new `Status` and optional `StatusMessage`. If the user cancels (the dialog returns `null`), the method returns early; otherwise it updates the in-memory `_session.Status` and `_session.StatusMessage` and, when a connection exists, asynchronously sends the update to the server via `_conn.UpdateStatusAsync(...)` inside `RunAsync` with a failure message. ## Remarks -This method is the bridge between the UI dialog and the rest of the application's state. It encapsulates the common pattern of mutating local session state plus a conditional remote update, so callers don't need to duplicate the sequence. If the user cancels the dialog (StatusDialog.Show returns null), no state is changed. The remote update runs asynchronously and uses a dedicated error message, allowing the UI to remain responsive and errors to be surfaced in a uniform way. +`HandleStatusRequested` acts as a small orchestration unit between the UI and the persistence layer. It delegates the prompting to `StatusDialog.Show`, then applies the resulting values to the local session and conditionally persists them to the server, ensuring the UI remains responsive through `RunAsync`. The local session is updated immediately, so the in-memory representation reflects the user's choice even if the remote update is deferred due to lack of connectivity. ## Notes -- The local session state is updated regardless of connectivity; the remote update only runs when _conn.IsConnected. -- If the user cancels the dialog, indicated by a null result, the method returns without modifying state. -- This method is private; callers should trigger it through the UI flow rather than invoking it directly. +- When offline (`_conn.IsConnected` is false), the remote update is skipped; the local session state changes still apply, which may require reconciliation once connectivity is restored. --- @@ -1758,14 +1895,10 @@ private void HandleThemeSelected(string themeName) **Returns:** `void` -Orchestrates the end-to-end theme switch when the user selects a theme. It loads the chosen theme via ThemeManager.GetTheme, applies it with ThemeManager.ApplyTheme, persists the selection by saving the updated config through ConfigManager.Save, and refreshes the UI to reflect the change. +Handles a user-initiated theme selection by resolving the requested theme with `ThemeManager.GetTheme(themeName)`, applying it through `ThemeManager.ApplyTheme(theme)`, persisting the chosen name to `_config.ActiveTheme` with `ConfigManager.Save(_config)`, and finally refreshing the UI via `InvokeUI` to re-apply color schemes and redraw the main window. This method serves as the central coordinator for the end-to-end theme-change workflow, ensuring the in-memory state, persisted configuration, and visual presentation stay in sync when a theme is selected. ## Remarks -Centralizes the end-to-end theme-switch workflow for the app: the same sequence is used whenever a theme changes, ensuring runtime state, persisted configuration, and the visible UI stay in sync. It coordinates ThemeManager, ConfigManager, and the main window (via InvokeUI) to apply color schemes and trigger a UI redraw in a thread-safe manner. Keeping this logic in one private method reduces duplication and makes future theme-related behavior easier to evolve. - -## Notes -- No exception handling is shown in this snippet; exceptions from GetTheme, ApplyTheme, or Save may propagate unless handled by the caller. -- Assumes _config and _mainWindow are initialized before invocation; otherwise a null reference may occur. +This symbol acts as the end-to-end theme-change workflow, coordinating `ThemeManager.GetTheme(themeName)` and `ThemeManager.ApplyTheme(theme)`, persisting the choice to `_config.ActiveTheme` with `ConfigManager.Save(_config)`, and refreshing the UI through `InvokeUI` (calling `_mainWindow.ApplyColorSchemes()` and `_mainWindow.SetNeedsDraw()`). It centralizes theme semantics so UI controls can request a theme by name without implementing the propagation logic themselves, and it ensures the selection survives across sessions by persisting it. --- @@ -1786,15 +1919,10 @@ private void HandleViewProfile(string? username) **Returns:** `void` -HandleViewProfile orchestrates the retrieval and presentation of a user’s profile. It determines whether the requested username represents the current user, fetches the profile asynchronously when authenticated, and routes the UI flow to either editing the own profile or viewing another user’s profile. +HandleViewProfile orchestrates the flow to display a profile by determining whether the requested username matches the current session's user (own profile) or not, and then, if authenticated, loading the target profile asynchronously before rendering the UI. For own profiles, it delegates to `ProfileViewDialog.ShowOwn` and handles actions such as `ProfileAction.EditProfile` and `ProfileAction.SetStatus`; for other users, it uses `ProfileViewDialog.Show` to present a read-only view. ## Remarks -This method centralizes the profile-view UX flow, coordinating session state, authentication status, data access, and the dialog-driven UI. By dispatching work to a background task and marshaling UI updates back to the main thread, it keeps the caller responsive while ensuring consistent dialog behavior for own vs. other profiles. The outcome of ShowOwn drives subsequent actions (EditProfile or SetStatus) via the ProfileAction return value, keeping subsequent logic cohesive within this symbol. - -## Notes -- The profile fetch only happens if authentication is present; target may be the current user or another user, and a non-empty target is required for the API call. -- All exceptions during data retrieval are caught and surfaced via ShowError on the UI thread; the flow gracefully aborts the view if loading fails. -- UI updates are performed on the UI thread using InvokeUI to avoid cross-thread issues. +This method centralizes the profile viewing UX, minimizing duplication of own-vs-other logic across the codebase. By performing the profile fetch in the background and only switching to the UI thread when needed, it preserves UI responsiveness and error handling through a single, consistent path. --- @@ -1815,16 +1943,16 @@ private void InvokeUI(Action action) => _app.Invoke(action) **Returns:** `void` -InvokeUI is a private helper that forwards an Action to the application's UI thread by calling _app.Invoke(action). It centralizes UI-thread marshaling within AppOrchestrator so UI updates scheduled from background or orchestrator code go through a single path, ensuring consistent threading semantics. +Invokes the supplied `Action` on the UI thread by delegating to the underlying `_app.Invoke(action)` call. This private wrapper centralizes UI-thread marshaling within the `AppOrchestrator` so that all UI work flows through a single, consistent path rather than sprinkling `_app` calls across the class. ## Remarks -Centers the UI-thread marshaling logic in a single place, reducing boilerplate and the risk of inconsistent marshalling across callers. Because the method is private, its usage is limited to internal orchestration and can be changed without impacting public APIs. The actual dispatch timing depends on the implementation of _app.Invoke; if non-blocking behavior is required, prefer a non-blocking path (such as a BeginInvoke variant) when available. +This private wrapper isolates UI-thread marshaling behind a small helper in `AppOrchestrator`. It makes the threading contract explicit and consistent: all code that must run on the UI thread goes through `_app.Invoke`, avoiding scattered direct `_app` usage. As a simple pass-through, it adds no additional synchronization or error handling beyond what `_app.Invoke` provides; callers should rely on that behavior. ## Notes -- The underlying _app.Invoke dictates whether the call blocks; this method does not introduce a new asynchrony model by itself. -- Avoid long-running work inside the Action passed to InvokeUI to prevent UI thread stalling; offload heavy work to background threads. +- If `_app` is null or uninitialized, this method will throw. +- Exceptions thrown by the provided `Action` propagate to the caller; this method does not swallow or transform them. --- @@ -1846,15 +1974,15 @@ private async Task?> JoinChannelWithPasswordPromptAsync(string **Returns:** `Task?>` -Joins a channel, automatically handling password prompts when the server requires authentication. If the channel is end-to-end encrypted, the user’s passphrase never leaves the client; a PBKDF2-derived authentication key is used to unwrap the room key locally, and the method returns the channel history. If the user cancels the password prompt, it returns null. +Joins a channel by name, prompting for a password when the server requires one and re-prompting on wrong password. For end-to-end encrypted channels the typed passphrase never leaves the client — a `PBKDF2`-derived auth key is sent to the server and the room content key is unwrapped locally. The method returns the channel history when the join succeeds, or null if the user cancels the prompt. ## Remarks -This method centralizes the join-with-password flow, including encryption metadata handling, local key management, and UI coordination, so callers do not need to re-implement retry logic. It coordinates with the connection's room-key store to cache and unwrap encryption envelopes; when a fresh envelope is obtained, it unwraps it locally (using the derived key, if available) and fetches history so decryption uses the latest key. The password prompt is dispatched to the UI via InvokeUI and the result is awaited, ensuring a responsive user experience even on the UI thread. +This method encapsulates the end-to-end join and key-management flow for encrypted channels. It first fetches optional crypto metadata and, if present, marks the channel encrypted. It then loops, deriving a key from the user's password when available and calling [`JoinChannelAsync`](../EchoHub.Server/Services/ChatService.cs.md) with the wire password. If a fresh `WrappedRoomKey` arrives, it attempts to persist it via `RoomKeys` and, on success, re-fetches history to decrypt content with the new key; if no local key is yet available, it triggers `UnlockRoomKeyAsync` to obtain one. The design keeps encryption material on the client and ensures history is decrypted with the current key state. ## Notes -- Cancellation returns null; callers should treat this as a failed join. -- Wrong passwords trigger a re-prompt loop via ChannelPasswordRequiredException handling. -- If crypto metadata cannot be retrieved, the join proceeds with best-effort encryption state and logs the incident for diagnostics. +- The join/prompt loop continues until a successful join yields a usable room key and history, or the user cancels by providing a null password. +- Key derivation is performed only when `crypto.IsEncrypted` is true and `crypto.EncryptionSalt` is non-null; otherwise `kek` remains null and envelope unwrap is skipped. +- UI interaction relies on `InvokeUI` and `ChannelPasswordDialog.Show`, so this path assumes a UI thread context. --- @@ -1875,15 +2003,14 @@ private bool NeedsUnlockPrompt(string channelName) **Returns:** `bool` -Determines whether the orchestrator should prompt the user to unlock the room key for a given end-to-end encrypted channel. It returns true only when the channel is encrypted, has no cached key, and the user has not declined the unlock prompt during this session. If the channel isn't encrypted or a key is already cached, no prompt is needed. +Determines whether the UI should prompt for unlocking the room key for a given channel. NeedsUnlockPrompt returns true only when the channel is end-to-end encrypted, its room key is not cached, and the user has not already declined the unlock prompt in this session. If the channel is not encrypted or a key is already cached, the method returns false; if encryption is present but the user has declined, it also returns false. The decision is guarded by a lock around the per-session declined-state to ensure thread-safe reads. ## Remarks - -By centralizing this decision, the code avoids spuriously prompting for unlocks. It leverages the RoomKeys service to inspect encryption status and key caching, and it uses a per-session declined-set to remember user choices, preventing repeated prompts for the same channel within a session. This method acts as a guard that the UI can consult before triggering any unlock UI. +This method centralizes the decision logic for showing an unlock prompt, coordinating between the encryption state available from `RoomKeys` and the per-session user preference tracked in `_declinedUnlocks`. By returning a simple boolean, it prevents repeated prompting for the same channel within a session and encapsulates the necessary synchronization around the decline-tracking collection. ## Notes - -- The check to read _declinedUnlocks is performed under a lock to ensure thread-safe access to the collection. +- This function is a pure decision point: it does not perform any UI action itself, it merely indicates whether a prompt should be shown. +- It relies on the per-session `_declinedUnlocks` collection to respect a user’s prior decline; the actual population of that set happens outside this snippet. --- @@ -1904,336 +2031,344 @@ private static string? NormalizeAsciiSize(string? size) => size?.Trim().ToLowerI **Returns:** `string?` -Normalizes common ASCII size descriptors into a canonical one-letter code used internally. It trims whitespace, lowercases the input, and maps 's'/'small' to 's', 'm'/'medium' to 'm', and 'l'/'large' to 'l'. Anything else (including null or unknown values) yields null, leaving it to the caller to decide how to proceed. +NormalizeAsciiSize is a private static helper method that standardizes a user-supplied size descriptor (parameter `size`). It trims whitespace with `Trim()`, converts to lowercase with `ToLowerInvariant()`, and maps common tokens to a canonical short form: `s` or `small` → `s`, `m` or `medium` → `m`, and `l` or `large` → `l`. If the input is `null` or does not match any known token, it returns `null` to indicate an unrecognized size. ## Remarks -Centralizes normalization logic for size inputs, ensuring consistent downstream handling wherever a compact size code is required. Since it returns null for unknown inputs, callers must guard against nulls or provide a fallback. The method is pure (no side effects) and deterministic given its input. +By centralizing these token mappings in a single helper, the codebase avoids duplicating normalization logic at call sites and ensures consistent downstream handling. Since it returns `null` for unrecognized input, callers must handle this possibility explicitly rather than relying on exceptions. ## Notes -- Returns null for any value that isn't a recognized descriptor, including null or whitespace. -- Whitespace is trimmed and case-insensitive matching is applied, so variations like ' Small ' or 'S' are treated the same. +- Returns `null` for unrecognized inputs; callers must handle this possibility. +- Trims whitespace and ignores case via `Trim()` and `ToLowerInvariant()` to make matching resilient to user input. --- -### RunAsync +### PersistLastReads > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void RunAsync(Func work, string errorPrefix, string? logContext = null) +private void PersistLastReads() +``` + +**Returns:** `void` + + +Persists the in-memory last-read message ids to the current server's config entry so unread and mention state can be reconstructed on the next connect. It reads the mapping from `_messageManager.LastReadIds`, returns early if it is empty, and uses `UpdateServerConfig` to write each `(channel, id)` pair into `server.LastReadMessages[channel]` as `id.ToString()`. + +## Remarks +This method encapsulates the persistence of per-channel read progress behind a single server-config mutation (`UpdateServerConfig`). It decouples in-memory tracking from durable storage, ensuring the last-read state survives restarts and reconnections. The mapping is stored as string values in `server.LastReadMessages`, derived from `id.ToString()`. + +--- + +### PromptPassword +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +} + + private string? PromptPassword(string prompt) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `work` | `Func` | — | -| `errorPrefix` | `string` | — | -| `logContext` | `string?` | `null` | +| `prompt` | `string` | — | + +**Returns:** `} + + private string?` + + +Displays a modal password-prompt dialog and returns the entered password when the user confirms; it returns `null` if the user cancels or leaves the field empty. The UI is constructed with a `Dialog` titled "Confirm Password" containing a `Label` for the prompt, a secret `TextField` for password input, and two `Button`s: "Confirm" (default) and "Cancel". When the user accepts, the handler assigns the field's `Text` to `result` and stops the dialog loop; when cancelling, it assigns `null` to `result` and stops. The dialog is shown by `_app.Run(dialog)`, and the final return is either the password or `null` if no input was provided. + +--- + +### RefreshStagingTray +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void RefreshStagingTray() +``` **Returns:** `void` -Consolidates the execution of an asynchronous operation within the application context by routing it through AsyncRunner.Run. It passes the current application context, the work to perform, and the UI-facing error handler, along with an error prefix and optional log context. Use RunAsync to ensure uniform error reporting and logging for asynchronous tasks without duplicating the wiring at every call site. +Refreshes the staging tray with the current staged files and ASCII size. It collects file names from `_stagedAttachments` via `Path.GetFileName` and updates the UI by calling `_mainWindow.SetStagedAttachments`, supplying the resulting `names` list and the ASCII size label produced by `AsciiSizeLabel(_config.DefaultAsciiSize)`. ## Remarks -RunAsync is a tiny abstraction that centralizes cross-cutting concerns around asynchronous work: error handling, user feedback through the main window, and optional diagnostic logging. It decouples callers from the AsyncRunner wiring, so changes to error handling or the logging strategy can be made in one place without touching every invocation. +Private helper that centralizes the UI refresh pattern for the staging area. It translates the raw attachments into the user-visible names and size indicator, ensuring a consistent presentation whenever the staged set changes. ## Notes -- Private accessibility means it can only be used within the containing class; callers should use higher-level methods that eventually invoke this helper. -- logContext is optional; omit it if there is no additional logging context, but providing a descriptive value improves traceability in logs and diagnostics. +- The method uses `OfType()` to ignore non-string entries in `_stagedAttachments` before extracting file names. +- The ASCII size displayed derives from `_config.DefaultAsciiSize` via `AsciiSizeLabel`, so changing the config affects the label globally. --- -### SendStagedMessage +### RunAsync > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void SendStagedMessage(string channel, string content) +private void RunAsync(Func work, string errorPrefix, string? logContext = null) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `channel` | `string` | — | -| `content` | `string` | — | +| `work` | `Func` | — | +| `errorPrefix` | `string` | — | +| `logContext` | `string?` | `null` | **Returns:** `void` -Sends one message with the given caption plus all staged files as attachments, then clears the staging tray. In encrypted channels each file is room-encrypted (blob + ASCII preview) client-side before upload; the caption is room-encrypted too. +A private helper that delegates the execution of an asynchronous unit of work to `AsyncRunner.Run`, binding it to the current application context and a centralized error presentation path. Callers supply a `Func` representing the work, an `errorPrefix` for user-facing errors, and an optional `logContext` for additional trace information; the method forwards these to `AsyncRunner.Run` along with `_app` and `_mainWindow.ShowError`. ## Remarks -This method serves as the orchestration point for sending staged content. It encapsulates the encryption decision (via RoomKeys and RoomCrypto), attachment construction (via OutgoingAttachment), and the lifecycle management of the staging tray and temporary pasted files. By coordinating several collaborators, it provides a single, reliable path to publish a caption and its attachments while preserving staging integrity and user experience even in edge cases (e.g., locked rooms or upload failures). - -## Notes -- If the channel is locked, the send is blocked and the files remain staged for after the unlock. The operation exits early in this case. -- The content and attachments are encrypted only if a room key is available; otherwise they are sent in plaintext. -- Temporary pasted files are cleaned up in a finally block, and the staging tray is refreshed regardless of success or failure to prevent orphaned UI artifacts. +Consolidates cross-cutting concerns: error handling and user feedback for async operations initiated by the orchestrator. By funneling all such work through this method, the codebase avoids duplicating boilerplate at every call site and ensures consistent error presentation via ` _mainWindow.ShowError` by passing it to `AsyncRunner.Run`. --- -### UnlockTrackedChannelAsync +### SaveServerToConfig > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private async Task UnlockTrackedChannelAsync(string channelName) +private void SaveServerToConfig(ConnectDialogResult result) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `channelName` | `string` | — | +| `result` | [`ConnectDialogResult`](UI/Dialogs/ConnectDialog.cs.md) | — | -**Returns:** `Task` - - -UnlockTrackedChannelAsync unlocks an end-to-end encrypted channel that is already joined as part of the hub. It re-joins the channel to obtain the WrappedRoomKey envelope, unwraps the key, and, if a key exists in the local store, loads decrypted history into the UI before signaling that the channel is unlocked. The method returns true on a successful unlock and false if the envelope is missing, the key cannot be unwrapped, or an error occurs during the flow. +**Returns:** `void` -## Remarks -This method centralizes the unlock sequence for channels the client is tracking, hiding the details of rejoining, envelope handling, and UI synchronization behind a single, reusable path. It relies on the connection's RoomKeys store to verify that a key has been unwrapped and on the UI dispatcher (InvokeUI) to surface decrypted history when available, ensuring the unlock outcome is reflected in the user interface in a thread-safe manner. -## Notes -- The function returns false if the channel does not present a WrappedRoomKey (i.e., not an E2E channel) or if the key cannot be established, providing a clear early-out behavior for non-E2E or cancelled unlocks. -- All exceptions are caught and logged with a warning, preventing an unhandled exception from propagating to callers. -- History is loaded into the UI only when a non-null history payload is produced; otherwise, unlocking still succeeds but there is nothing to display. +On a successful connection to a server, this method updates the per-server entry in the client config in place (never replacing it) so that cached room keys, left channels, and last-read markers survive across connections. It looks up the server by URL (case-insensitive) within the `config.SavedServers` collection and, if no entry exists, creates a new [`SavedServer`](Config/ClientConfig.cs.md) named after the URL host and adds it to the collection; it then updates `Username`, `RefreshToken` (taken from `_conn.Api!.RefreshToken` when `RememberMe` is true, otherwise `null`), `RememberMe`, and `LastConnected`, persists the updated [`ClientConfig`](Config/ClientConfig.cs.md) via `ConfigManager.Save(config)`, and updates the in-memory `_config` while logging the successful connection with the URL. --- -### WireCommandHandlerEvents +### SendStagedMessage > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void WireCommandHandlerEvents() +private void SendStagedMessage(string channel, string content) ``` +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `channel` | `string` | — | +| `content` | `string` | — | + **Returns:** `void` -WireCommandHandlerEvents subscribes the AppOrchestrator to a broad suite of events exposed by the command handler. This centralized wiring ensures that whenever the command handler raises events such as OnSetStatus, OnJoinChannel, or OnExportData, the corresponding local handlers (HandleCmdSetStatus, HandleCmdJoinChannel, HandleCmdExportData, etc.) are invoked. Use this method during initialization to establish the one-to-one event-to-handler mappings instead of scattering subscriptions throughout startup code. +Sends one message with the given caption plus all staged files as attachments, then clears the staging tray. In encrypted channels each file is room-encrypted (blob + ASCII preview) client-side before upload; the caption is encrypted too when a room key is available and the content is non-empty. -## Remarks +The operation runs asynchronously inside a `RunAsync` wrapper, taking a snapshot of the current staged attachments, clearing `_stagedAttachments`, and refreshing the staging tray UI via `RefreshStagingTray`. -This method encapsulates the event-binding choreography between the command layer and the orchestrator. It provides a single, discoverable place where command-related concerns are wired, which helps keep the AppOrchestrator focused on reacting to high-level user commands rather than wiring subsystems. Because the wiring is performed in one place, tests can swap a mock _commandHandler or verify that specific events are connected to their expected handlers. If the set of command events changes, updating this method is the one authoritative location. +The method builds a list of [`OutgoingAttachment`](Services/OutgoingAttachment.cs.md)s by calling `BuildOutgoingAttachmentAsync` for each staged path (using a room key if one is present). If a room key is available and `content` is not empty, the caption is encrypted with `RoomCrypto.EncryptText` before sending; otherwise the plain `content` is sent. The final send is performed through `_conn.Api!.SendMessageWithAttachmentsAsync(channel, wireContent, outgoing, size)` with `size` taken from `_config.DefaultAsciiSize`. -## Notes +A `finally` block ensures that temporary pasted files are cleaned up via `CleanupPastedTempFiles(tempFiles)` so there are no leftovers regardless of success or failure. -- If _commandHandler is not initialized before calling this method, a NullReferenceException will occur when subscribing to events. Ensure proper initialization order during construction/startup. -- Renaming or removing events requires updating this wiring method to maintain coverage. --- -### WireConnectionManagerEvents +### SetDownloadPath > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void WireConnectionManagerEvents() +private void SetDownloadPath(string path) ``` -**Returns:** `void` +**Parameters:** +| Parameter | Type | Default | +|-----------|------|---------| +| `path` | `string` | — | -## Source Code -Wires up the ConnectionManager's event handlers to drive the client UI for messages, joins/leaves, and presence updates. It subscribes to MessageReceived to display incoming messages and to notify the user when they are mentioned; it handles UserJoined and UserLeft to keep the per-channel online user list in sync and to refresh the UI when the current channel is active; and it reacts to UserStatusChanged to propagate presence updates across all channels and to update the cached channel user lists. The updates are marshaled onto the UI thread via InvokeUI, and thread-safety around the shared _channelUsers collection is maintained with a lock to avoid race conditions when users join, leave, or change status. +**Returns:** `void` -## Remarks -This method centralizes all event wiring for the connection manager, isolating networking concerns from UI/presentation logic. It coordinates message display, system messages for joins/leaves, and presence updates in a single place, which simplifies reasoning about real-time behavior and makes testing easier. The design favors immediate UI feedback for the active channel while ensuring the in-memory presence cache stays consistent across channels. -## Notes -- The code uses a lock (_channelUsersLock) to guard updates to the _channelUsers dictionary; avoid performing long-running work inside the locked region to prevent UI thread blocking. -- Notification sounds depend on a non-empty session username and on the message content mentioning that username; if either is missing, the sound is not played. -- Presence updates are propagated to all channels for status visibility, and the code path optimizes updates for the current channel by taking a snapshot when applicable. - -## Dependencies -- UserPresenceDto -- Content -- StringComparison -- Username -- Status -- UserStatus - -## Dependency APIs (verified signatures) -- record [`UserPresenceDto`](../EchoHub.Core/DTOs/ProfileDtos.cs.md) (`src/EchoHub.Core/DTOs/ProfileDtos.cs`) -- property `Content` (`src/EchoHub.Core/Models/Message.cs`) -- property `Username` (`src/EchoHub.Client/Config/ClientConfig.cs`) -- property `Status` (`src/EchoHub.Client/Services/UserSession.cs`) -- enum [`UserStatus`](../EchoHub.Core/Models/UserStatus.cs.md) (`src/EchoHub.Core/Models/UserStatus.cs`) - -## Symbol To Document -- Name: WireConnectionManagerEvents -- Kind: method -- File: src/EchoHub.Client/AppOrchestrator.cs -- Language: csharp -- ID: 8c768c24-e19f-4c25-b611-7ab4a805aa68 +Sets the application's download folder to the provided `path`. It first ensures the directory exists by calling `Directory.CreateDirectory(path)`; if that throws, it marshals a UI error through the main window and returns. On success, it updates `_config.DownloadPath`, saves the configuration with `ConfigManager.Save(_config)`, and posts a system message via `_messageManager.AddSystemMessage` to `_mainWindow.CurrentChannel` indicating the new download folder. All UI feedback is marshaled through `InvokeUI` to run on the UI thread. --- -### WireMainWindowEvents +### UnlockRoomKeyAsync > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void WireMainWindowEvents() +private async Task?> UnlockRoomKeyAsync(string channelName, JoinOutcome outcome) ``` -**Returns:** `void` - +**Parameters:** -Wires the MainWindow's UI events to the orchestrator's handlers by subscribing to a broad set of On...Requested events exposed by _mainWindow. This centralized bootstrap method maps user interactions—such as connecting, disconnecting, logging out, submitting messages, staging files, pasting images, selecting channels, requesting profiles or status, changing themes, managing servers and channels, audio playback, file download/save, image operations, deleting messages, checking for updates, rolling back, viewing user profiles, joining channels from messages, searching, loading more results, and replying to messages—to their corresponding handler methods. Call this during initialization to ensure UI actions are routed into the application's business logic. +| Parameter | Type | Default | +|-----------|------|---------| +| `channelName` | `string` | — | +| `outcome` | [`JoinOutcome`](Services/EchoHubConnection.cs.md) | — | -## Remarks -Centralizes event wiring and decouples the UI from concrete business logic by acting as the single source of truth for how UI actions map to handlers. It makes the orchestrator's responsibilities explicit and simplifies testing and future changes, since all UI-to-logic subscriptions are declared in one place. +**Returns:** `Task?>` -## Notes -- Invoking WireMainWindowEvents more than once will attach duplicate handlers, causing each event to invoke handlers multiple times; ensure this is called once or guard against re-subscription. -- If _mainWindow is not initialized before calling this method, a NullReferenceException can be thrown during subscription; ensure proper initialization order. +UnlockRoomKeyAsync is the encrypted-channel recovery workflow used when there is no cached room key for a channel (for example on a new device). It first checks the provided [`JoinOutcome`](Services/EchoHubConnection.cs.md) for `EncryptionSalt` and `WrappedRoomKey`; if either is missing, it returns the existing history. If both are present, it prompts the user with `ChannelPasswordDialog.Show` to enter a passphrase, derives a candidate key with `RoomCrypto.DeriveKeys(passphrase, salt)`, and attempts to store the derived key via `_conn.RoomKeys.TryStoreFromEnvelope(channelName, outcome.WrappedRoomKey, derived.KeyEncryptionKey)`. On success, it clears any decline flag for the channel and fetches the updated history using `_conn.GetHistoryAsync(channelName)`. If the passphrase is incorrect, it repeats the prompt; if the user cancels, it records the decline for that channel and returns the existing history. --- -## HandleCmdOpenProfile +### UnlockTrackedChannelAsync > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private Task HandleCmdOpenProfile(string? username) +private async Task UnlockTrackedChannelAsync(string channelName) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `username` | `string?` | — | +| `channelName` | `string` | — | -**Returns:** `Task` +**Returns:** `Task` -This private command handler serves as the command-path bridge to open a user's profile: it dispatches a UI action to display the profile for the provided username and returns a completed Task. It doesn't navigate directly; InvokeUI marshals the call to the UI thread and delegates to HandleViewProfile for the actual rendering. The username parameter is nullable and is passed through to the underlying handler. +UnlockTrackedChannelAsync orchestrates the unlock sequence for a channel that is already hub-joined. It reuses the join flow to fetch a wrapped room key envelope by calling `_conn.JoinChannelAsync(channelName, null)`. If the envelope is absent (the `WrappedRoomKey` is null), the channel is not an E2E channel and the method returns `false`. It then delegates to `UnlockRoomKeyAsync(channelName, outcome)` to unwrap the key (which prompts for the passphrase). If no key is retained in `_conn.RoomKeys` for the channel, the flow is considered cancelled or unwrapped, and the method returns `false`. If a `history` payload is produced, it is applied to the UI via `InvokeUI(() => _messageManager.LoadHistory(channelName, history))`. The method returns `true` when the unlock succeeds; any exception is caught, logged with a warning, and results in `false`. ## Remarks -This separation of concerns keeps command execution lightweight while centralizing UI-thread marshaling. By delegating to HandleViewProfile, the actual profile rendering logic remains centralized, promoting consistent navigation behavior across commands that open profiles. +UnlockTrackedChannelAsync centralizes the unlock sequence for an E2E channel into a single, testable flow that spans network joining, key envelope handling, and UI history restoration. It defers to the hub-provided envelope to determine applicability, uses `UnlockRoomKeyAsync` for unwrapping (and passphrase prompting), and only then surfaces the decrypted history via [`LoadHistory`](UI/Chat/ChatMessageManager.cs.md). This encapsulation keeps concerns separated: the rest of the app can request an unlock without wiring together [`JoinChannelAsync`](../EchoHub.Server/Services/ChatService.cs.md), envelope checks, and UI updates. ## Notes -- Returns immediately with Task.CompletedTask; the UI action runs asynchronously on the UI thread. -- The nullable username means callers should ensure compatibility with HandleViewProfile's expectations, or provide a fallback when null. +- Exceptions are caught and cause the method to return `false`; debugging may require examining logs produced by `Log.Warning`. +- The method returns `false` for multiple distinct failure modes (not an E2E envelope, cancellation/unwrapping failure, or an unexpected exception); callers should handle a generic failure outcome gracefully. --- -## HandleSearchRequested +### UpdateServerConfig > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void HandleSearchRequested() +private void UpdateServerConfig(Action mutate) ``` -**Returns:** `void` - +**Parameters:** -Handles the search-driven user flow by presenting a dialog of channel names and dispatching the selected result to navigation or action handlers. If a channel is chosen, it switches to that channel and triggers additional channel-selection logic; if an action is chosen, it delegates to the corresponding handler (connect, disconnect, logout, profile, status, create-channel, delete-channel, saved-servers, toggle-users, updates, or quit). When the dialog is canceled, the method exits without side effects. +| Parameter | Type | Default | +|-----------|------|---------| +| `mutate` | `Action` | — | -## Remarks +**Returns:** `void` -By acting as a centralized dispatcher, this method separates the UI interaction (SearchDialog.Show) from the concrete consequences of each choice. It delegates work to the AppOrchestrator's specialized handlers, ensuring consistent behavior for search-driven commands and simplifying future extension of supported actions. If new search result types or actions are introduced, this entry point will need parallel updates to the switch branches and handlers to maintain correctness. -## Notes +UpdateServerConfig mutates the configuration entry for the server associated with the current API base URL and persists the result. It loads the existing configuration with `ConfigManager.Load()`, locates the matching [`SavedServer`](Config/ClientConfig.cs.md) in `config.SavedServers` by comparing the server’s `Url` to the API base URL using `StringComparison.OrdinalIgnoreCase`, applies the mutation via the `mutate` action, saves the updated configuration with `ConfigManager.Save`, and updates `_config` to reflect the in-memory state. If the URL cannot be determined or no matching server exists, the method returns without changes. -- The method only handles Channel and Action result types; additional types will be ignored unless handled here. -- Action keys are plain strings; adding new actions requires updating both the inner switch and the corresponding handler methods. -- Null results are treated as cancellation and result in an early return. +## Remarks +UpdateServerConfig acts as a small centralization point for modifying the active server's settings, ensuring that mutations are consistently applied and immediately persisted. It encapsulates the guard logic (null URL and missing server) behind a simple contract and keeps the in-memory `_config` synchronized with the persisted config. --- -## PromptPassword +### WireCommandHandlerEvents > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -} - - private string? PromptPassword(string prompt) +private void WireCommandHandlerEvents() ``` -**Parameters:** +**Returns:** `void` -| Parameter | Type | Default | -|-----------|------|---------| -| `prompt` | `string` | — | -**Returns:** `} +Subscribes the command handler's events to their corresponding handlers within the orchestrator. It wires each `OnX` event exposed by `_commandHandler` to a concrete `HandleCmdX` method (for example `OnSetStatus` → `HandleCmdSetStatus`, `OnJoinChannel` → `HandleCmdJoinChannel`). This central wiring typically runs during initialization to ensure that incoming commands trigger the appropriate response logic. - private string?` +## Remarks +By centralizing all event subscriptions in `WireCommandHandlerEvents`, the class gains a single, discoverable place to manage the command-event surface, reducing drift where a handler might be forgotten. The pattern keeps wiring concerns separate from the handlers themselves, which simplifies testing and future extension (adding new commands simply introduces a new `OnX`-`HandleCmdX` pair). + +--- + +### WireConnectionManagerEvents +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** method + +```csharp +private void WireConnectionManagerEvents() +``` + +**Returns:** `void` -Prompts the user for a password via a modal dialog and returns the entered value, or null if the user cancels or submits an empty value. Use this helper when you need a blocking, masked password input in a Terminal.Gui-based UI, rather than duplicating dialog scaffolding in multiple places. +Wires the connection manager's events to drive UI updates and internal state in response to real-time chat activity. It subscribes to `MessageReceived`, `UserJoined`, `UserLeft`, and `UserStatusChanged`, updating the message feed, per-channel presence caches, and online user lists, while triggering mention notifications when relevant. All UI updates are marshalled via `InvokeUI`, and the shared cache is protected with `_channelUsersLock` to ensure thread-safety during joins, leaves, and status changes. ## Remarks -This method encapsulates a small, reusable UI flow using Terminal.Gui components: a dialog with a label that shows the provided prompt, a masked text field (Secret = true), and Confirm/Cancel buttons. It returns the raw password string entered by the user, or null when the user cancels or omits input. The prompt text is injected into the label, allowing reuse with different messages without changing layout. The interaction relies on _app.Run(dialog) to present the modal and _app.RequestStop() to close it once the user makes a choice. +By centralizing the wiring of `MessageReceived`, `UserJoined`, `UserLeft`, and `UserStatusChanged`, this method keeps the UI and the per-channel presence cache in sync with real-time activity, delegating display concerns to the UI while mutating a `List` under `_channelUsersLock`. This approach minimizes UI churn by refreshing the current channel's online list when needed and broadcasting presence changes across all channel views. ## Notes -- The password value is kept in memory as a string until the method returns; avoid logging or persisting it in plaintext. -- Whitespace-only input is not treated as empty by this implementation; if your validation requires trimming, perform it after receiving the result. -- Because this is a private helper, callers should provide an appropriate prompt message to convey the expected credential. - +- All presence mutations occur inside a `lock (_channelUsersLock)` block to guard against concurrent updates from multiple events. +- UI updates are dispatched via `InvokeUI` to ensure thread affinity for UI components like `_messageManager` and `_mainWindow`. +- `UserStatusChanged` propagates a textual status to all channels via `_messageManager.AddStatusMessage` and synchronizes per-channel lists, with special handling for `UserStatus.Invisible`. --- -## RefreshStagingTray +### WireMainWindowEvents > **File:** `src/EchoHub.Client/AppOrchestrator.cs` > **Kind:** method ```csharp -private void RefreshStagingTray() +private void WireMainWindowEvents() ``` **Returns:** `void` -RefreshStagingTray updates the UI to reflect the current set of staged attachments by extracting their file names and passing them to the main window alongside a size label derived from the configuration. It is a UI helper invoked after modifications to the staging area to keep the display in sync with the underlying data. +Subscribes the `_mainWindow` events to their corresponding handlers (for example, `_mainWindow.OnConnectRequested` to `HandleConnect`, `_mainWindow.OnMessageSubmitted` to `HandleMessageSubmitted`, `_mainWindow.OnChannelSelected` to `HandleChannelSelected`, and so on). This centralizes the UI-to-logic wiring that drives the application's event-driven behavior. Call this during initialization to bootstrap the UI event flow in a single place rather than scattering subscriptions across the codebase. ## Remarks -Serves as a UI adapter between the staging model and the presenter, encapsulating how the staged attachments are presented to the user. By using Path.GetFileName, it shows only the file names, avoiding full paths in the interface, and delegates formatting of the size indicator to AsciiSizeLabel(_config.DefaultAsciiSize). This separation simplifies updating presentation details without altering the staging logic. +By collecting all event subscriptions here, the method provides a single locus for the startup wiring and makes it easier to see which UI actions trigger which handlers. It also decouples the `_mainWindow` from concrete behavior; the handlers can be replaced or mocked for testing without changing event hookup sites. If this method runs multiple times, handlers would be added repeatedly; ensure it's invoked once or guard against re-subscription. ## Notes -- Null-reference risk if _mainWindow or _config is not initialized before this method runs. +- Calling this method more than once will attach duplicate event handlers to `_mainWindow`, causing handlers to fire multiple times for a single UI action. Consider guarding with a flag or detach before re-wiring in diagnostic scenarios. --- -## UnlockRoomKeyAsync +### ImageOpenExtensions > **File:** `src/EchoHub.Client/AppOrchestrator.cs` -> **Kind:** method +> **Kind:** field ```csharp -private async Task?> UnlockRoomKeyAsync(string channelName, JoinOutcome outcome) +private static readonly HashSet ImageOpenExtensions = new(StringComparer.OrdinalIgnoreCase) ``` -**Parameters:** -| Parameter | Type | Default | -|-----------|------|---------| -| `channelName` | `string` | — | -| `outcome` | [`JoinOutcome`](Services/EchoHubConnection.cs.md) | — | +ImageOpenExtensions is a private static readonly `HashSet` that lists the image file extensions the [open] action may hand to the OS image viewer in E2E rooms. It is constructed with `StringComparer.OrdinalIgnoreCase` to perform case-insensitive lookups, whitelisting extensions such as `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.bmp`. -**Returns:** `Task?>` +--- + +### SafeOpenExtensions +> **File:** `src/EchoHub.Client/AppOrchestrator.cs` +> **Kind:** field + +```csharp +private static readonly HashSet SafeOpenExtensions = new(StringComparer.OrdinalIgnoreCase) +``` -UnlockRoomKeyAsync drives the interactive unlock flow for an encrypted channel that lacks a cached room key (for example, on a new device). It prompts the user for a passphrase until the room key can be unwrapped and stored, at which point it loads and returns the channel history; if the user cancels, it records the decline and returns the existing history without unlocking. +SafeOpenExtensions is a private static readonly `HashSet` listing the file extensions the app is allowed to open with the system's default application via the shell. For anything else, the code downloads rather than auto-opening, enforcing a safe-open policy; the set uses `StringComparer.OrdinalIgnoreCase` to treat extensions case-insensitively (e.g., `.MP4` and `.mp4` are equivalent). ## Remarks - -This helper encapsulates the user interaction required to unlock messages for a specific channel, separating the UI-driven password prompt and key-derivation loop from the rest of the connection logic. It uses a per-channel decline cache to avoid nagging after a user declines to unlock, and it clears that cache on a successful unwrap to resume normal history loading. +Centralizes the open-via-shell policy for file handling by enumerating extensions that may be opened with the system default application; any file with an extension not present in `SafeOpenExtensions` is downloaded instead and not opened automatically. The policy uses `StringComparer.OrdinalIgnoreCase` to ensure case-insensitive matching, so `.MP4` and `.mp4` are treated equally. ## Notes - -- Early exit: if outcome.EncryptionSalt or outcome.WrappedRoomKey are null, the method returns outcome.History immediately. -- Asynchronous prompt: the passphrase prompt is dispatched to the UI and awaited without blocking the caller; the prompt is wired via a TaskCompletionSource and ChannelPasswordDialog.Show. -- Unlock success vs cancel: on success, the derived key is stored and the method returns the channel history; on cancel, the channel is added to _declinedUnlocks and history is returned unchanged. +- Underlying collection mutability: although the field is `static readonly`, the `HashSet` contents can be changed at runtime; treat this as a fixed policy only if you guarantee no mutation after initialization. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Commands/CommandHandler.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Commands/CommandHandler.cs.md index 15e7561..63322bd 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Commands/CommandHandler.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Commands/CommandHandler.cs.md @@ -5,7 +5,6 @@ ## Contents - [CommandHandler](#commandhandler) - - [HandleAsciiSize](#handleasciisize) - [HandleAsync](#handleasync) - [HandleAvatar](#handleavatar) - [HandleBan](#handleban) @@ -16,18 +15,19 @@ - [HandleExport](#handleexport) - [HandleHelp](#handlehelp) - [HandleInvite](#handleinvite) + - [HandleJoin](#handlejoin) - [HandleKick](#handlekick) - [HandleLeave](#handleleave) - [HandleMe](#handleme) - [HandleMeta](#handlemeta) - [HandleMute](#handlemute) - [HandleNick](#handlenick) + - [HandleNuke](#handlenuke) - [HandlePasswd](#handlepasswd) - [HandleProfile](#handleprofile) - [HandleQuit](#handlequit) - [HandleRole](#handlerole) - [HandleSend](#handlesend) - - [HandleServers](#handleservers) - [HandleStatus](#handlestatus) - [HandleTestSound](#handletestsound) - [HandleTheme](#handletheme) @@ -37,13 +37,13 @@ - [HandleUsers](#handleusers) - [IsCommand](#iscommand) - [IsValidHex](#isvalidhex) + - [ParsePathAndSizeFlag](#parsepathandsizeflag) - [StripQuotes](#stripquotes) - [StatusUsage](#statususage) - [CommandResult](#commandresult) +- [HandleAsciiSize](#handleasciisize) - [HandleDownloadPath](#handledownloadpath) -- [HandleJoin](#handlejoin) -- [HandleNuke](#handlenuke) -- [ParsePathAndSizeFlag](#parsepathandsizeflag) +- [HandleServers](#handleservers) --- @@ -56,65 +56,33 @@ public class CommandHandler ``` -Parses user-entered slash commands and turns them into asynchronous events the rest of the client can handle. Use this when you want a single place to interpret textual commands (for example from a chat input box) and decouple parsing from the actual side effects; call IsCommand to quickly test whether an input should be sent to the handler. +A central, asynchronous command dispatcher for parsing slash-style input and routing it to consumer-provided handlers. Use `CommandHandler` when you need a UI or orchestrator to interpret text commands (strings that begin with `/`) and invoke application logic via event hooks rather than hard-wiring command execution into the input component. The handler exposes a large set of `On...` events (for example `OnSetStatus`, `OnSendAction`, `OnCreateInvite`) that consumers subscribe to; calling `HandleAsync` parses the input and raises the appropriate event, returning a `CommandResult` that describes the outcome. ## Remarks -This class is an input-to-event bridge: it does not itself implement the side effects of commands but exposes one event per supported command (OnSetStatus, OnSendAction, OnCreateInvite, OnExportData, etc.). Handlers subscribe to these events to implement the application behavior. The source-level comments capture important parsing decisions made here — for example, status semantics (null status means keep the current status; null message means keep current message; empty message means clear it) and parsing details such as respecting quoted paths and an optional size flag when sending files. Those parsing responsibilities and the use of events keep command parsing isolated from platform-specific orchestration and UI code. +`CommandHandler` separates command parsing from command execution by exposing each command as an event of type `Func<..., Task>`. This makes it simple for the UI layer or an orchestrator to register asynchronous handlers for only the commands it cares about and keeps the parsing logic isolated in one place. The event signatures use nullable and optional parameters (for example `UserStatus?` and `int?`) to represent semantic distinctions described in the source comments — notably the `OnSetStatus` contract where a `null` status means "keep the current status", a `null` message means "keep the current message", and an empty message string means "clear it". ## Example ```csharp -// subscribe to a couple of command events and pass a slash-command string to the handler -var commands = new CommandHandler(); -commands.OnSetStatus += async (status, message) => +// Hook a handler and dispatch a command string +var handler = new CommandHandler(); +handler.OnSetStatus += async (status, message) => { - // apply status change (status may be null to mean "keep current") - await Task.CompletedTask; -}; -commands.OnSendAction += async action => -{ - // present an emote/action in the UI + // apply status and message to session state await Task.CompletedTask; }; -if (commands.IsCommand("/me waves")) +// This input will be recognized as a command because it starts with '/' +if (handler.IsCommand("/status away")) { - var result = await commands.HandleAsync("/me waves"); - // inspect result or propagate it to the UI + var result = await handler.HandleAsync("/status away"); + // inspect 'result' (a CommandResult record) to determine success or error } ``` ## Notes -- The IsCommand check is a simple StartsWith('/'); a bare "/" will be treated as a command by that predicate. -- Events are plain C# events and may be null if nobody has subscribed; callers should expect that raising a command could be a no-op unless subscribers are attached. -- Status handling has specific semantics encoded in comments: the orchestrator resolves null/empty values against session state (null = keep, empty string = clear). - ---- - -### HandleAsciiSize -> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` -> **Kind:** method - -```csharp -private async Task HandleAsciiSize(string args) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `args` | `string` | — | - -**Returns:** `Task` - - -HandleAsciiSize is a private asynchronous helper that processes the ASCII size command. If a consumer is attached via OnSetAsciiSize, it is invoked with the input string trimmed by the caller. A lack of argument follows the convention of opening the size picker, while a non-empty argument such as s, m, l, small, medium, or large is forwarded to the delegate to apply the chosen size. The method always returns a successful CommandResult to indicate the command was handled. - -## Remarks -By funneling ASCII-size logic through OnSetAsciiSize, this symbol decouples the command parsing from the actual size-management behavior (UI, persistence, or runtime configuration). The method remains flexible: if no delegate is supplied, the command is effectively a no-op but still reports success, allowing the caller to treat the input as handled. This pattern simplifies testing and enables swapping the size-setting behavior without changing the command-handler code. - -## Notes -- Potential NullReferenceException if OnSetAsciiSize is non-null and 'args' is null because args.Trim() is called without a null-check. -- Returning CommandResult(true) means callers should not rely on this method to signal whether a size was actually changed. +- `IsCommand` uses `input.StartsWith('/')`, so leading whitespace prevents recognition; callers should trim input if they expect tolerant detection. +- Many events may be `null` when no subscriber is attached; consumers should ensure they subscribe to the commands they intend to handle, and callers of `HandleAsync` should expect that some commands may have no effect if no handler is present. +- The `OnSetStatus` semantics are intentionally specific: `null` vs empty string for the message are distinct (keep vs clear). The handler's parsing enforces strictness for some forms (see `StatusUsage`), so inputs that look like natural language may be rejected as invalid commands rather than implicitly treated as a different command form. --- @@ -135,15 +103,14 @@ public async Task HandleAsync(string input) **Returns:** `Task` -HandleAsync is the central dispatcher for user-entered commands in the EchoHub client; it validates the input, parses the command and its arguments, and asynchronously delegates to the appropriate command-specific handler, returning a CommandResult that reflects success or failure. If the input doesn't look like a command it returns a failure immediately, and unknown commands produce a user-friendly error. +HandleAsync validates that the input starts with a slash command, normalizes the command name to be case-insensitive, and dispatches to the appropriate per-command handler (for example `HandleStatus`, `HandleMe`, `HandleExport`). It returns a `CommandResult` representing the outcome of the invoked handler, performing the operation asynchronously by awaiting the selected method; unknown commands yield a user-facing error. ## Remarks -This abstraction separates parsing from individual command implementations, normalizing input (lowercasing the command) and trimming whitespace so each handler can focus on its own logic. It uses the leading slash convention (input[1..]) and a switch expression to map to handlers, making it straightforward to add new commands by extending the switch. +HandleAsync is the central dispatcher for slash-based commands in the client. It maps each known command to a dedicated `HandleX` method, enabling the command implementations to stay focused on their behavior. It relies on `IsCommand` to guard inputs and on splitting logic to extract a command and its optional arguments, keeping routing consistent across commands. ## Notes -- The command is case-insensitive due to ToLowerInvariant. -- Unknown commands yield a friendly error that points to /help; non-command inputs yield a simple failure. -- Command behavior is delegated to a family of HandleX methods, keeping parsing concerns isolated from command logic. +- Adding new commands requires updating the command routing to point to a new `HandleX` method. +- Unknown commands are surfaced as a user-facing error rather than throwing exceptions. --- @@ -164,14 +131,15 @@ private async Task HandleAvatar(string args) **Returns:** `Task` -This private async method handles the /avatar command: it validates the provided argument, trims surrounding quotes, and, if a handler is attached via OnSetAvatar, invokes it with the cleaned target. It returns a CommandResult indicating either a usage error when the argument is missing or a success message once the update completes. +It handles the `/avatar` command by validating the input, stripping surrounding quotes from the trimmed argument, and then optionally invoking an avatar setter via `OnSetAvatar` before returning a success message. If no argument is provided (or it's whitespace), it immediately returns a `CommandResult` with an error and the usage hint `Usage: /avatar `. When a non-empty target is supplied, it calls `OnSetAvatar` if available and finally responds with `Uploading avatar...`. ## Remarks -This method isolates command parsing from the avatar update mechanism. By exposing OnSetAvatar as a delegate, the app can supply concrete update behavior (e.g., uploading to a server or updating local state) without coupling the command logic to the implementation. The user-facing feedback is produced after the asynchronous update completes, ensuring the message reflects the outcome of the operation. +This symbol encapsulates a small, testable command pattern: input validation, normalization, optional delegation, and user feedback. The optional `OnSetAvatar` delegate allows the avatar update logic to live outside the command handler, enabling different hosting contexts to supply their own upload behavior without changing this method. ## Notes -- If OnSetAvatar is null, the method completes without performing any update, but still returns a success message ('Uploading avatar...'). -- Exceptions thrown by OnSetAvatar are not caught within this method; callers should handle faulted tasks if the delegate fails. +- Awaiting `OnSetAvatar` means the command handling thread will asynchronously wait for the upload to complete; consider the caller's synchronization context and potential long-running operations. +- If `OnSetAvatar` is not provided, the method still returns a success message, so ensure a consumer wires up the handler to actually execute the avatar update. +- There is no additional validation on the URL or file path beyond non-emptiness and quotes stripping. --- @@ -192,16 +160,13 @@ private async Task HandleBan(string args) **Returns:** `Task` -Handles the /ban command by validating input, parsing a required username and an optional reason, and invoking the OnBanUser callback when provided. If no arguments are given, it returns a usage message; otherwise it awaits the ban handler and responds with a banning message for the target user. +HandleBan is a private asynchronous command handler for the `/ban` command. It validates input (returning a usage error when `args` is empty or whitespace), parses the first token as `username` and the remainder as an optional `reason`, and, if an `OnBanUser` handler is supplied, awaits it before returning a `CommandResult` indicating the ban is being processed. ## Remarks -HandleBan acts as a small bridge between the command-processing surface and the domain action of banning a user. It encapsulates input parsing and the optional ban callback behind a single method, enabling testability and decoupling from the actual ban implementation. Its behavior is explicitly contingent on the presence of OnBanUser: with it set, the ban logic runs; without it, the method still returns a result, but no action is performed. +This symbol acts as a thin wrapper that decouples the command parsing from the actual ban logic by delegating to `OnBanUser` when present. It uses `StringSplitOptions.TrimEntries` to robustly split the input into `username` and `reason` without stray whitespace. If no `OnBanUser` handler is attached, the method still returns a progress message, which can be misleading; the real ban happens only when a consumer wires up the `OnBanUser` callback. ## Notes -- The ban action only occurs if OnBanUser is assigned; otherwise, the command completes with a 'Banning ...' message but no side-effect. -- The reason parameter is optional; if omitted, reason is passed as null to OnBanUser, which should handle it accordingly. -- Input parsing uses a single split with a maximum of two parts and trims whitespace, so arguments like "user" and "user reason" are handled predictably. - +- If `OnBanUser` is null, no ban action is performed; ensure a handler is attached before invoking this symbol. --- @@ -222,24 +187,14 @@ private async Task HandleBanner(string args) **Returns:** `Task` -Processes the /banner command by validating the provided text, trimming it, and optionally dispatching it to a banner sender. If the input is null, empty, or whitespace, it returns an error CommandResult containing a usage message. If a banner handler is registered via OnSendBanner, it awaits the handler with the trimmed text; finally it returns a successful CommandResult. +Handles the `/banner` command by validating the provided text and broadcasting it to the client or other listeners via the `OnSendBanner` callback. If the input `args` is null, empty, or whitespace, it returns a `CommandResult` marked as an error with a usage hint: `Usage: /banner (letters, digits, basic punctuation)`. If valid text is supplied, it trims the input and, if the `OnSendBanner` handler is attached, awaits its invocation with the trimmed text, then returns a successful `CommandResult`. ## Remarks -Centralizes banner command handling and isolates input validation from the actual rendering logic. By exposing OnSendBanner as a delegate, the system can plug in different banner delivery strategies without changing the command code. Trimming before dispatch ensures consistent formatting and prevents trailing or leading whitespace from affecting the banner. - -## Example -```csharp -// Example: valid input -var result = await HandleBanner("Hello!"); -// If a subscriber is attached to OnSendBanner, it's invoked with "Hello!" and a success result is returned. - -// Example: invalid input (whitespace-only) -var error = await HandleBanner(" "); -``` +This method decouples the banner broadcast from the command-handling pipeline by emitting the banner text through a callback rather than performing the display itself. It centralizes validation for the `/banner` command and communicates outcomes via a `CommandResult`, which callers can inspect to present feedback to users. The `OnSendBanner` invocation is conditional, avoiding a null-reference when no subscriber is attached. ## Notes -- If OnSendBanner is null, the method completes successfully without attempting to send a banner. -- Leading/trailing whitespace is trimmed before dispatch; inner whitespace is preserved. +- The banner text is trimmed before broadcasting to avoid leading or trailing spaces. +- If no `OnSendBanner` subscriber is attached, the command still reports success; the side effect is simply skipped. --- @@ -254,14 +209,14 @@ private async Task HandleClear() **Returns:** `Task` -Describes the internal helper method HandleClear, which clears any attachments staged for a command by invoking the OnClearAttachments delegate if it's configured, awaits its completion, and then returns a CommandResult signaling success with the message "Cleared staged attachments." Because it is private, it is intended to be used internally by the command handling flow rather than as a public API. +This private async method clears attachments staged for the current command by invoking the optional `OnClearAttachments` callback when provided, awaiting its completion, and then returning a `CommandResult` indicating success with the message "Cleared staged attachments." It is used internally by the command-handling flow to reset the staging state after a clear action. ## Remarks -The method encapsulates the cleanup step of the command execution, ensuring a single point of behavior for clearing attachments and reporting results. It reduces duplication by handling the conditional delegate invocation and the standardized success result in one place. +By delegating the actual clearing action to the optional `OnClearAttachments` delegate, this method decouples the clear operation from the concrete storage or UI details, enabling swap-in of different clearing strategies in tests or configurations. It always returns a successful `CommandResult` when it completes, even if no delegate is provided (in that case nothing is cleared). ## Notes -- If OnClearAttachments is null, the method performs a no-op for the cleanup but still reports success. -- Exceptions raised by OnClearAttachments propagate to the caller; the method does not catch them. +- If `OnClearAttachments` is null, the method does not perform any action beyond returning the success result. +- If `OnClearAttachments` throws, the exception is propagated to the caller and not caught here. --- @@ -282,14 +237,14 @@ private async Task HandleColor(string args) **Returns:** `Task` -Handles the /color command by validating and normalizing the input hex color and, when valid, applying it through an optional callback. It accepts a string of arguments, ensures a color was provided, prepends a leading '#' if missing, enforces the #RRGGBB format, and calls OnSetColor(color) if a handler is attached before returning a CommandResult with a user-friendly message. +Parses and normalizes the color argument from a command, validating that it is a valid hex color in the format `#RRGGBB`. It accepts either a plain hex string or one prefixed with `#`, automatically prepends a leading `#` if missing, and rejects empty input with a usage hint. When the color is valid, it optionally notifies the consumer through the `OnSetColor` callback and returns a `CommandResult` that confirms the nickname color has been set to the normalized value. ## Remarks -By decoupling the color application from the parsing logic via OnSetColor, this method remains focused and testable. It normalizes input to a canonical #RRGGBB form and provides clear feedback for missing, malformed, or valid colors, integrating smoothly with the command-handling flow and the CommandResult feedback. +This method concentrates the color customization flow for the command surface. By deferring the final color application to the optional `OnSetColor` callback, it stays decoupled from the downstream state management and remains easy to unit-test. It relies on the `IsValidHex` helper to validate the color digits and provides explicit user feedback for both invalid input and success. ## Notes -- If OnSetColor throws, the exception will propagate to the caller since there is no internal try/catch. -- If OnSetColor is null, no color is actually applied, though a confirmation message is still returned. +- Because the method is `private`, tests typically exercise it through the public command handler path rather than calling it directly. +- If `OnSetColor` is not assigned, color changes are not propagated; ensure a handler is attached if you expect the color to take effect. --- @@ -304,14 +259,10 @@ private async Task HandleDeleteAccount() **Returns:** `Task` -HandleDeleteAccount is a private asynchronous helper that orchestrates the delete-account action by optionally invoking a deletion callback and then returning a CommandResult indicating success. If an OnDeleteAccount delegate is supplied, it is awaited; if not, the method completes immediately and signals success. +HandleDeleteAccount is a private async helper that, when invoked, awaits the optional `OnDeleteAccount` callback if it has been supplied, and then returns a `CommandResult` indicating success. It provides a simple, centralized way to trigger the deletion flow without exposing the internal delegation to external callers. ## Remarks -By relying on the OnDeleteAccount hook, this method decouples the command handling from the actual deletion logic, enabling different delete implementations to be plugged in without changing the caller. It participates in a command-pattern flow where a successful outcome is reported via CommandResult once the delegate (if any) completes. Note that exceptions thrown by the deletion delegate will propagate to the caller; this method does not translate errors into a failure result itself. - -## Notes -- Be mindful that if OnDeleteAccount is null, the method returns a successful CommandResult without performing any deletion. -- There is no internal error-handling; exceptions thrown by OnDeleteAccount bubble up to the caller. +Being private, it keeps the deletion orchestration encapsulated within the command handler. It invokes the `OnDeleteAccount` callback only when it is non-null and awaits its completion; if no handler is provided, it simply proceeds. It always returns a successful `CommandResult` after that, so the actual deletion outcome is determined by the `OnDeleteAccount` implementation, not this wrapper. --- @@ -326,14 +277,15 @@ private async Task HandleExport() **Returns:** `Task` -HandleExport is a private asynchronous helper in CommandHandler that conditionally triggers an export operation and then signals success to the caller. When OnExportData is assigned, the method awaits OnExportData, allowing export logic to be injected without forcing callers to perform the export themselves; if OnExportData is null, the method completes immediately. The resulting CommandResult(true) communicates that the command finished successfully regardless of whether an export was performed, effectively making the export step optional and pluggable within the command handling workflow. +`HandleExport` is a private asynchronous wrapper that triggers the export flow by invoking the optional `OnExportData` delegate (if assigned) and then returns a successful `CommandResult`. Callers reach for it to initiate export logic through the handler without coupling to a concrete export implementation; if no export handler is attached, it completes as a no-op while still signaling success. ## Remarks -Why this abstraction exists: It decouples the export behavior from the command flow, enabling tests and consumers to provide their own export logic via a delegate. It centralizes the export invocation behind a simple private method, which reduces repetition and makes the command-handling path easier to reason about. The null check ensures no-op behavior when no export is configured, while still preserving asynchronous semantics. +By deferring the actual work to `OnExportData`, this method isolates export concerns from the command handler and provides a clean extension point for tests and runtime customization. If `OnExportData` is null, the method simply returns success, making the export step optional. Any exceptions thrown by `OnExportData` will bubble up to the caller since there is no exception handling here. ## Notes -- If OnExportData throws, the exception propagates to the caller; HandleExport itself does not catch exceptions. -- Because the method is private, it can only be invoked within CommandHandler, ensuring export behavior is controlled and testable. +- This method is private, intended for internal orchestration within the class. +- The returned `CommandResult` is always created with `true` — it does not reflect the success or failure of the export delegate itself. +- If you rely on export results, consider handling exceptions at the call site or wiring `OnExportData` to signal failures. --- @@ -348,16 +300,13 @@ private async Task HandleHelp() **Returns:** `Task` -Handles internal help for the client command interface. When invoked, it optionally triggers the OnHelp hook if assigned, then returns a successful CommandResult containing a large multi-line help text that lists available commands, their usage, and usage tips. This method is used by the command-handling flow to present a consistent help experience to users. +This private async method handles the help command by optionally invoking the `OnHelp` callback and returning a successful `CommandResult` that contains the multi-line help text enumerating the available commands. It uses a pre-formatted string literal to deliver the help content so users see a readable, aligned list of commands across categories (user, moderation, and utility). ## Remarks -By centralizing the help text in this method, the UI can display a single source of truth for command guidance. The OnHelp hook provides a minimal extension point for runtime customization or side effects without duplicating the static content. Because the method is private, it's intended to be invoked by the command-handling flow rather than consumed as part of the public API. +By centralizing help content in `HandleHelp`, updates to the command list are made in one place, reducing drift between the dispatcher and the displayed help. The `OnHelp` delegate provides a hook for extending or customizing help behavior without altering the core command-dispatch logic. ## Notes -- The returned CommandResult is constructed with a success flag and the help message; the signature implies CommandResult(bool, string). -- The help content is embedded as a raw string literal; editing the literal updates every user-visible line of help. -- If OnHelp throws, the exception will propagate as part of the async flow since there is no internal try/catch. -- The content is not localized here; there is no localization mechanism evident in this snippet. +- Hard-coded help text means localization is not supported out of the box; consider external resources or injecting a localization service if multi-language support is required. --- @@ -378,13 +327,58 @@ private async Task HandleInvite(string args) **Returns:** `Task` -Parses and handles invite-related commands issued to the command handler. It recognizes three forms: /invite list to enumerate invites, /invite revoke to invalidate a specific invite, and /invite [maxUses] [expiresHours] to create a new invite with optional usage limits and expiration. It validates inputs, returns a CommandResult that signals success or error (with a usage message when inputs are invalid), and delegates the actual work to OnListInvites, OnRevokeInvite, or OnCreateInvite when those callbacks are provided. +HandleInvite is the private command handler that processes the '/invite' input by parsing the argument string and routing to list, revoke, or create flows. It uses space-delimited tokens and delegates to `OnListInvites`, `OnRevokeInvite`, or `OnCreateInvite` when wired, returning a `CommandResult` that signals success or provides usage guidance. + +## Remarks +HandleInvite centralizes the invite-management command surface in the client, decoupling the command parsing from the actual business logic behind listing, revoking, and creating invites. By validating inputs (e.g., ensuring revoke has a code; ensuring numeric values for maxUses and expiresHours) before invoking the callbacks, it minimizes error paths and provides consistent usage messages. Note that if the callbacks are not wired (null), the method completes with success but performs no action; wiring is required to affect state. + +## Example +```csharp +// Demonstrative inputs (not direct calls to this private method in production) +var r1 = await HandleInvite("list"); // lists invites +var r2 = await HandleInvite("revoke CODE"); // revokes CODE +var r3 = await HandleInvite("5 24"); // creates an invite with 5 uses and 24h expiry +``` + +## Notes +- If `OnListInvites`, `OnRevokeInvite`, or `OnCreateInvite` are null, the method completes with success without performing any action. +- The input is split using `StringSplitOptions.RemoveEmptyEntries` and `StringSplitOptions.TrimEntries`, so extra spaces do not produce empty tokens. + +--- + +### HandleJoin +> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` +> **Kind:** method + +```csharp +private async Task HandleJoin(string args) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `args` | `string` | — | + +**Returns:** `Task` + + +Parses the input for a '/join' command, validates that a channel (and optionally a password) is supplied, and forwards the join request to a registered handler. It normalizes the channel name by removing a leading '#', and returns a CommandResult that signals success or a usage error. ## Remarks -By centralizing the invite command parsing, this private method insulates the higher-level command flow from the details of argument interpretation and validation. It coordinates with the domain actions via the OnListInvites, OnRevokeInvite, and OnCreateInvite callbacks, enabling a clean separation between parsing logic and invite-management behavior. The returned CommandResult ensures callers observe a uniform success/error contract. +This method serves as a small command-dispatch helper that decouples the parsing/validation of join requests from the actual join operation. By invoking the `OnJoinChannel` callback when present, it allows the channel-join logic to be injected or mocked, which is useful for testing and for swapping out how joins are performed without touching the parsing layer. It also supports both public and password-protected channels by treating an additional argument as the optional password. + +## Example +```csharp +// Most common case: join a channel with a password +var result = await HandleJoin("#general mysecret"); +``` ## Notes -- It relies on StringSplitOptions and StringComparison for command parsing and case-insensitive subcommand matching, and it returns helpful usage messages when inputs are malformed. +- The password extraction line in the provided source appears garbled (the token `[REDACTED:CONNECTION_STRING_PASSWORD]` and a comparison against `1`). This will not compile as written. Replace with a proper password extraction, for example: + ```csharp + var password = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) ? parts[1] : null; + ``` --- @@ -405,15 +399,14 @@ private async Task HandleKick(string args) **Returns:** `Task` -Handles the /kick command by validating input, extracting the target username and an optional reason, and delegating the actual kick action to a callback if one is registered. If no arguments are supplied, it immediately returns a CommandResult indicating the proper usage. When arguments exist, it splits them into username and an optional reason (up to two parts) and, if OnKickUser is not null, awaits the callback with the username and reason. It then returns a success CommandResult with the message 'Kicking {username}...'. +HandleKick serves as the internal entry point for processing a `/kick` command. It validates that arguments are provided, splits them into a `username` and an optional `reason`, and, if present, awaits the `OnKickUser` callback before returning a `CommandResult` that signals the kick is in progress. It centralizes argument handling and user feedback for kicking a user. ## Remarks -Integrates input validation and a pluggable kick action via OnKickUser, acting as a thin mediator between the command parser and the actual moderation logic. It centralizes consistent user feedback and prevents empty or malformed kick commands from proceeding. +Decoupling the command surface from the actual kick action via the `OnKickUser` callback enables testability and modularity. If no listener is attached, the method still returns a `CommandResult` indicating the kick is being processed, which preserves a consistent user experience even when the actual kick logic is not wired up. The parsing uses a two-part split to treat the first token as `username` and the rest as an optional `reason`, preserving spaces in the reason. ## Notes -- If OnKickUser throws, this method does not catch the exception; the exception propagates to the caller. -- When args are valid but OnKickUser is null, the method still returns a success CommandResult reflecting the intent ("Kicking {username}..."). No action is performed in that case within this method. -- The argument parsing uses StringSplitOptions.TrimEntries and splits at most once, honoring the first space as the separator between username and optional reason. +- If `args` is null, empty, or whitespace, the method returns a usage error message. +- When `OnKickUser` is provided, it is awaited; otherwise the method completes with a proactive user feedback message without performing a kick action. --- @@ -428,17 +421,22 @@ private async Task HandleLeave() **Returns:** `Task` -HandleLeave is an asynchronous helper used by the command-handling flow to perform a channel leave action when provided. If a consumer assigns OnLeaveChannel, it will be awaited before the method completes; if not, the method completes immediately. In either case, it returns a CommandResult representing success. +Handles the leave action by optionally invoking the `OnLeaveChannel` callback and then returns a successful `CommandResult`. -## Remarks +Use this helper to centralize the leave flow so that the caller doesn't need to duplicate the optional callback invocation and always receives a confirmed success. -By delegating the actual leave operation to the OnLeaveChannel delegate, this symbol decouples the command-handling logic from the concrete leave behavior, enabling easier testing and composition. It acts as a thin wrapper around the optional leave action, ensuring a consistent CommandResult is produced for the rest of the pipeline. +## Remarks +This method serves as a tiny orchestration point within the command-handling flow. It delegates the optional notification of leaving to `OnLeaveChannel` and always yields a positive `CommandResult`, keeping the leave path concise and consistent. ## Notes +- If the delegate `OnLeaveChannel` throws, the exception propagates to the caller since there is no internal exception handling here. + +## Example +```csharp +// Inside the same class that defines HandleLeave +var result = await HandleLeave(); +``` -- If OnLeaveChannel is provided and it throws an exception, that exception propagates to the caller since there is no local error handling. -- The return value is always new CommandResult(true), so success is signaled regardless of whether a leave action existed or completed. -- This method is private; external components should not rely on it directly. --- @@ -459,13 +457,13 @@ private async Task HandleMe(string args) **Returns:** `Task` -HandleMe handles the /me command by validating the provided action and, when valid, dispatching it to an OnSendAction callback. If the argument is missing or whitespace, it returns an error-style CommandResult with a usage message; if OnSendAction is registered, it awaits the callback with the trimmed text and then returns a successful CommandResult. +HandleMe processes the user-directed emote command (/me). It requires an action string; if the input is missing or whitespace, it returns a `CommandResult` containing a usage message and flags it as an error. When an `OnSendAction` handler is present, it is awaited with the input trimmed of whitespace, and the method then returns a successful `CommandResult`. ## Remarks -By delegating the actual broadcast of the action to OnSendAction, this method remains focused on input validation and orchestration. The private scope indicates it is part of the internal command-processing pipeline, invoked by the higher-level handler when parsing a /me invocation. +This method decouples the parsing and validation of the `/me` command from the actual sending of the action by routing via the `OnSendAction` callback. This makes the behavior easily testable and allows the application to swap in different emission strategies without changing the command-handling code. ## Notes -- Exceptions raised by OnSendAction are not caught within this method; callers should rely on upstream exception handling. +- If `OnSendAction` is null, no action is emitted even when a non-empty argument is supplied; the method still returns a success result. --- @@ -480,17 +478,13 @@ private async Task HandleMeta() **Returns:** `Task` -HandleMeta is a tiny asynchronous helper in the command handling workflow. If an OnRoomInfo callback has been registered, it awaits that callback to allow any optional room-information logic to run, and then returns a CommandResult indicating success. - -Use it when you want an optional, side-effect-free hook for room-info refresh during meta command handling without forcing callers to implement the null-check and await themselves; if OnRoomInfo is not provided, the method completes immediately with a successful result. +Handles the internal 'Meta' operation by optionally invoking the `OnRoomInfo` delegate if it is assigned and awaiting its completion, then returning a successful `CommandResult` (`new CommandResult(true)`). This method is invoked as part of the command-handling flow when meta-information about the room may be produced via the `OnRoomInfo` hook. ## Remarks -HandleMeta acts as an abstraction that coordinates optional room-state enrichment with the command pipeline. By encapsulating the OnRoomInfo invocation, it decouples room-information concerns from the rest of the command logic, making it easy to inject or mock in tests. It guarantees a consistent success signal via CommandResult, even if OnRoomInfo performs no work. +This symbol acts as an internal bridge between command handling and an optional hook. By performing a null-check before calling `OnRoomInfo` and standardizing the success response as a `CommandResult`, it keeps the meta-processing path concise and testable. If `OnRoomInfo` throws, the exception propagates to the caller since there is no try-catch here. ## Notes -- If OnRoomInfo throws, the exception will bubble up to the caller; there is no internal error handling here. -- The returned CommandResult(true) does not reflect the outcome of OnRoomInfo; use OnRoomInfo itself to signal failures if needed. -- Because OnRoomInfo is awaited, any long-running operation inside it will increase the total latency of this method. +- The method is private and can only be invoked by its containing type; external callers should go through the public command-handling path. --- @@ -511,15 +505,17 @@ private async Task HandleMute(string args) **Returns:** `Task` -Parses and handles the /mute command by extracting a username and an optional duration from the input, then delegates the actual mute action to an optional OnMuteUser handler and returns a CommandResult indicating the operation is in progress. +HandleMute processes the '/mute' command by parsing the supplied arguments. It expects a username and an optional duration in minutes. If arguments are empty or whitespace, it returns a usage error through `CommandResult` with a usage string. If a username is provided (and an optional duration can be parsed), it invokes the `OnMuteUser` delegate (if assigned) to perform the mute, and then returns a `CommandResult` indicating that the user is being muted. ## Remarks -This method serves as a thin, command-UI-oriented wrapper around a mute action. It defers the concrete muting logic to OnMuteUser, enabling testability and alternate mute implementations without changing the command parsing. It also demonstrates defensive parsing: it requires a non-empty arg string, splits into at most two parts (username and an optional duration), and only proceeds to invoke the handler if one is provided. The user-facing feedback is consistently formatted as a "Muting {username}..." message to keep the command responsive while the mute operation completes asynchronously. + +Separation of concerns: this method is purely about parsing and delegating; the actual mute implementation is injected via `OnMuteUser`, enabling testability and replacement of mute logic. The input is split into at most two parts; the first is treated as the username and the second, if present and numeric, as the duration in minutes. This means usernames containing spaces aren't supported by this simple parser, and non-numeric durations are ignored (interpreted as null). ## Notes -- If OnMuteUser is null, no mute action is performed beyond returning the Muting message. -- If OnMuteUser throws an exception, HandleMute does not catch it; the exception will propagate to the caller. -- Only the first two space-delimited segments are considered: the first is the username, the second (if present) is parsed as an integer duration in minutes; if parsing fails, duration is treated as null (indefinite mute). + +- If `args` is empty or whitespace, a usage error is returned. +- If `OnMuteUser` is not assigned, the method will return a muting message without performing any action. +- The mute duration is optional; when omitted, duration stays null and the actual duration must be handled by the mute handler. --- @@ -541,15 +537,36 @@ private async Task HandleNick(string args) **Returns:** `Task` -HandleNick processes the /nick command by validating the supplied display name and applying it when possible. If the argument is missing or whitespace, it returns a CommandResult indicating the proper usage. If an OnSetNick callback is provided, it awaits that callback with the trimmed nickname, and finally returns a success CommandResult showing the updated display name. +HandleNick processes the `/nick` command by validating the supplied display name and updating the display name when a handler is attached. If the argument is missing or whitespace, it returns an error `CommandResult` with the usage hint; if a name is provided, it trims it, optionally invokes the asynchronous `OnSetNick` callback to apply the change, and then returns a success `CommandResult` confirming the new display name. + +## Remarks +By encapsulating nickname changes behind an optional `OnSetNick` delegate, this method decouples command parsing from nickname persistence and provides a uniform `CommandResult` contract to signal outcomes (whether guidance is needed or a nickname was updated). It relies on input sanitation (trimmed values) and adheres to the command-driven UX pattern used by other commands in the client. + +## Notes +- If `OnSetNick` is null, the nickname change is not persisted, though the method still returns a success `CommandResult` with the confirmation message. +- The input is trimmed before both the callback and the confirmation, ensuring consistent storage and user feedback regardless of user formatting. + + +--- + +### HandleNuke +> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` +> **Kind:** method + +```csharp +private async Task HandleNuke() +``` + +**Returns:** `Task` + + +HandleNuke asynchronously triggers the nuking workflow by invoking the optional `OnNukeChannel` callback and then returns a successful `CommandResult` with the message "Nuking channel history...". It is used by the command handling flow to initiate nuking through a configurable hook rather than containing the nuking logic itself. ## Remarks -HandleNick acts as a focused, testable wrapper around nickname changes, separating input validation from persistence. By accepting an optional OnSetNick delegate, it delegates the actual update to the surrounding system while keeping the command-handling logic simple and easy to reason about. +This symbol decouples the command-processing path from the actual nuking implementation by delegating to `OnNukeChannel` when provided. If the delegate is not assigned, the method still completes with a success result, ensuring the caller receives a consistent response even in the absence of a nuking hook. ## Notes -- If OnSetNick is null, the method reports success but does not persist the nickname; ensure a listener is wired if persistence is required. -- If OnSetNick throws, the exception bubbles to the caller since there is no internal error-handling around the delegate. -- The input is trimmed before both passing to OnSetNick and composing the final message, ensuring consistent display without leading or trailing spaces. +- If `OnNukeChannel` is null, the nuking action does not run; the method returns `new CommandResult(true, "Nuking channel history...")`. --- @@ -570,15 +587,15 @@ private async Task HandlePasswd(string args) **Returns:** `Task` -Parses and handles the /passwd command by validating its arguments and, when possible, delegating the actual password change to an optional callback. It expects two tokens: the old passphrase and the new passphrase, enforces a minimum length for the new passphrase, and returns a CommandResult indicating success or error. +HandlePasswd parses the argument string for the `/passwd` command, expecting exactly two tokens: the old passphrase and the new passphrase. It uses `StringSplitOptions.RemoveEmptyEntries` and `StringSplitOptions.TrimEntries` to split, validates the token count and the new passphrase length, and returns an error `CommandResult` with usage guidance or a length message if validation fails; if a handler is attached via `OnChangeRoomPassword`, it is invoked asynchronously with the old and new passphrases, and a success `CommandResult` is returned. ## Remarks -This method acts as a lightweight command-scaffold: it performs input validation and defers the real password change to OnChangeRoomPassword if provided. By isolating argument parsing from the change logic, it keeps command-handling concerns separate from the actual credential update, enabling test doubles or alternate implementations of the change process. The final outcome is indicated via CommandResult; errors produce usage information or length violations, while a successful path triggers the change callback and returns a non-error result. +Conceptually, this symbol acts as a thin, command-level gatekeeper that delegates the actual password change to a pluggable collaborator. It centralizes basic input validation for the `/passwd` command and ensures the operation is asynchronous by awaiting the handler. The real change occurs only if an `OnChangeRoomPassword` handler is wired; otherwise the method returns success without modifying any state. ## Notes -- Input is split on a single space; passphrases containing spaces won't be accepted. -- If OnChangeRoomPassword is null, the method completes without invoking a change, yet still returns a success result. -- New passphrase must be at least 3 characters; there is no verification of the old passphrase here. +- The method is `private`; callers outside the class cannot invoke it directly and tests must exercise the command pipeline. +- If no `OnChangeRoomPassword` handler is attached, the call will report success without applying any change. +- It delegates actual password-changing logic to the `OnChangeRoomPassword` handler; the method itself performs only light input validation. --- @@ -599,15 +616,13 @@ private async Task HandleProfile(string args) **Returns:** `Task` -HandleProfile is a private asynchronous command handler that triggers a profile view by delegating to an optional OnOpenProfile callback. It normalizes the incoming argument into a username: if the provided args are null, empty, or whitespace, it uses null; otherwise it trims surrounding whitespace. If a listener is attached, it awaits the callback with the computed username, allowing the consumer to decide how to present the profile. Finally, it returns a successful CommandResult, indicating the command was processed at the wrapper level. +HandleProfile processes the profile command by deriving a sanitized username from the input `args`, then, if an `OnOpenProfile` handler is registered, it awaits that callback with the `username`. It always returns a successful `CommandResult`, independent of whether a profile navigation was actually performed. ## Remarks -By design this method decouples the command-handling surface from the actual navigation logic. It passes null when no explicit username is supplied, enabling the consumer to interpret that as a request for the current user's profile or a default view. The method is asynchronous only because it awaits the OnOpenProfile callback; the surrounding code can rely on the Task-based pattern without assuming internal navigation details. Note that exceptions raised by OnOpenProfile propagate to the caller, since there is no internal error handling here. +HandleProfile serves as a small mediator between the command parser and the profile navigation logic. By normalizing the input and guarding the callback against absence, it decouples command handling from the actual navigation implementation represented by `OnOpenProfile`. The method returns a `CommandResult(true)` to indicate the command was processed, while the navigation is carried out asynchronously when available. ## Notes -- If OnOpenProfile is null, the method completes trivially after returning a success result. -- Exceptions from OnOpenProfile bubble up to the caller; this method does not swallow errors. -- Input normalization ensures that whitespace-only arguments are treated as no username. +- If `args` is null or whitespace, `username` becomes `null`, and the call passes `null` to `OnOpenProfile`—this is a contract decision left to the handler. --- @@ -622,17 +637,15 @@ private async Task HandleQuit() **Returns:** `Task` -HandleQuit is a private asynchronous method that executes the quit sequence by invoking OnQuit (if provided) and then returning a CommandResult indicating success. It provides a centralized quit path for the command-handling flow, enabling an optional host-defined cleanup step to run before signaling completion to the caller. +`HandleQuit` is an asynchronous method that coordinates the quit workflow for the command-handling path. It conditionally invokes the `OnQuit` callback if one is supplied, awaiting its completion, and then returns a successful `CommandResult` by constructing `new CommandResult(true)`. ## Remarks -By centralizing quit semantics in HandleQuit, the class separates the mechanics of terminating a session from the rest of command processing. It wires an optional asynchronous hook (OnQuit) that external code can supply to perform cleanup or notifications during quit, without forcing callers to know about the hook's existence. Note that exceptions raised by OnQuit will propagate to the caller; this method does not swallow errors, preserving fail-fast semantics for quit-related failures. The method always returns a successful CommandResult when OnQuit completes, or immediately if no OnQuit is provided. +This method serves as a lightweight quit coordinator: it delegates the actual quit work to an optional `OnQuit` handler and exposes a consistent success signal to its callers. By centralizing this logic, the command pipeline can trigger quit behavior without duplicating event invocation logic elsewhere, while still allowing consumers to subscribe to `OnQuit` to perform custom shutdown steps. ## Notes -- If OnQuit is null, the method returns a CommandResult that signals success immediately after the null check. -- Exceptions from OnQuit propagate to the caller and are not swallowed here. -- This method is private; external code cannot call it directly and must go through the public command-handling path. +- If `OnQuit` throws, the exception propagates out of `HandleQuit` because there is no try/catch inside this method. --- @@ -653,25 +666,18 @@ private async Task HandleRole(string args) **Returns:** `Task` -Parses and validates a /role command string, requiring a username and a target role (admin, mod, or member). If valid, it optionally invokes OnAssignRole to apply the change and returns a CommandResult that communicates the outcome or usage errors. - -## Dependencies -- CommandResult -- StringSplitOptions - -## Dependency APIs (verified signatures) - -The REAL, parser-verified API surface of this symbol's collaborators: +HandleRole is a private asynchronous command handler that processes the arguments of a "/role" command, validates the username and the requested role, and triggers the role-assignment workflow if a listener is provided. It validates that two arguments are present (a username and a role from the set admin, mod, or member) and returns clear, user-facing messages for both usage and invalid input. If a valid role is provided and an assignment callback is supplied via `OnAssignRole`, the method awaits that callback before returning a success message. -- record `CommandResult` (`src/EchoHub.Client/Commands/CommandHandler.cs`) +Because the method encapsulates argument parsing, validation, and user feedback, callers gain a consistent command-handling surface for role changes without duplicating error handling logic. It delegates the actual assignment to `OnAssignRole` to keep the command-layer concerns isolated from business logic, enabling test doubles or alternate implementations to be plugged in without altering the parsing behavior. ## Remarks -HandleRole centralizes command parsing for role assignment. It isolates input validation (presence, tokenization, allowed roles) from the actual mutation performed by OnAssignRole, enabling easier testing and decoupling of concerns. By normalizing the role to lowercase, it accepts case-insensitive input while enforcing a strict set of roles. The method always returns a CommandResult, conveying either an error message with usage guidance or a confirmation that the role is being set. +HandleRole centralizes the /role command’s input handling: it ensures you always receive a well-formed pair of `username` and `role`, normalizes the role to lower-case, and only proceeds when the role is one of the allowed values. The actual grant or revocation action is delegated to the `OnAssignRole` callback, preserving separation of concerns between command parsing and role-management logic. If `OnAssignRole` is not provided, the method still validates input and returns a final message indicating the target role would be set, which can be useful in dry-run scenarios or for UI testing. ## Notes -- The arguments are split with a maximum of two parts, so the first token is the username and the second is the role; any extra tokens are ignored. -- Role normalization uses ToLowerInvariant to enable case-insensitive input while restricting to the allowed values: 'admin', 'mod', or 'member'. -- OnAssignRole is optional; if provided, it's awaited to perform the actual assignment; if not, the method still returns a confirmation message. +- Uses `Split` with a maximum of two parts and `StringSplitOptions.TrimEntries`, so extra tokens beyond the username and role are rejected as an invalid role (e.g., "/role user admin extra" becomes an invalid role). +- Normalizes the role with `ToLowerInvariant()` and accepts only `admin`, `mod`, or `member`; any other value yields a user-facing usage note about valid roles. +- If `OnAssignRole` is provided, the method awaits it before reporting success; if not, it returns a success message without performing any assignment. + --- @@ -692,41 +698,7 @@ private async Task HandleSend(string args) **Returns:** `Task` -HandleSend implements the /send command handler. It validates the provided target (a file path or URL), extracts an optional size flag, and triggers the actual sending via the OnSendFile callback when available. It distinguishes between HTTP/HTTPS URLs and local files, returning a user-facing CommandResult that reflects either the ongoing action (Sending or Uploading) or any encountered error (usage, missing target, or missing file). - -## Remarks -HandleSend isolates user interaction from the sending mechanism by coordinating argument parsing and the send callback. It calls OnSendFile(target, size) when provided and then formats a helpful status message that includes the resolved file name (defaulting to 'image' when the URL lacks a file name). This method remains robust against missing files and invalid targets, giving clear guidance to the user while deferring the actual transmission to the registered callback. It also supports reading an optional size flag from the end or start of the argument string while honoring quoted paths. - -## Notes -- If OnSendFile is null, HandleSend still returns a status message (e.g., "Sending:..." or "Uploading:...") without performing a callback. -- For HTTP/HTTPS URLs, the file name is derived from the URL's path; if absent, a sensible default of "image" is used. -- It relies on an external ParsePathAndSizeFlag(args) helper; behavior depends on that implementation, so edge cases around quoting and flag placement should be considered. -- When targeting a local file, the method checks File.Exists and returns a "File not found: ..." error if absent. - ---- - -### HandleServers -> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` -> **Kind:** method - -```csharp -private async Task HandleServers() -``` - -**Returns:** `Task` - - -HandleServers centralizes the action of opening servers by invoking an optional host-provided callback and returning a success result. Developers reach for this helper when they want the command sequence to trigger server-opening behavior that may be supplied externally via OnOpenServers, rather than implementing the logic inline. - -## Remarks - -It decouples the behavior from the command flow by using a nullable delegate, making the logic easier to test and replace in different hosting environments. If OnOpenServers is null, the method still returns a successful CommandResult, providing a safe no-op path. Note that there is no try-catch around the await, so exceptions raised by OnOpenServers will propagate to the caller rather than being swallowed here. - -## Notes - -- Exceptions from OnOpenServers propagate to the caller (no internal suppression). -- The returned CommandResult(true) is independent of the success of OnOpenServers. -- This method is private and intended for internal command flow; external code should not rely on it directly. +HandleSend is the private asynchronous handler for the `/send` command. It validates input, supports both `http`/`https` URLs and local file paths, extracts an optional size flag, and triggers the `OnSendFile` callback with the resolved target and size, returning a `CommandResult` that informs the user of the action (either sending a URL or uploading a file). --- @@ -747,30 +719,15 @@ private async Task HandleStatus(string args) **Returns:** `Task` -Parses and handles the /status subcommands by interpreting either a status keyword or a status message, and then invokes an optional OnSetStatus callback to apply the change. It validates input, supports a message form (msg/message) to set or clear a status message, and returns a CommandResult that communicates success or a structured error (with usage guidance) when appropriate. +HandleStatus processes a user-issued status command. It splits the incoming `args` into at most two parts using `StringSplitOptions.TrimEntries` and normalizes the first token with `ToLowerInvariant`. It supports `msg`/`message` to set or clear the status message via the `OnSetStatus` callback; otherwise it recognizes `online`, `away`, `dnd`/`donotdisturb`, and `invisible` as [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md) values. Unknown tokens or malformed input yield a `CommandResult` error that includes the usage string. ## Remarks -This method centralizes the parsing logic for status-related subcommands and delegates the actual state mutation to a downstream callback via OnSetStatus, enabling clean separation between command parsing and state management. The OnSetStatus delegate is optional; if it is not provided, the method still returns a descriptive result but performs no external mutation. A strict policy is enforced: if a valid status keyword is supplied, no additional text is allowed; the special form msg/message may include a trailing message to set, otherwise the message is cleared. - -## Example -```csharp -// Example: set online status -var r1 = await HandleStatus("online"); - -// Example: set a status message -var r2 = await HandleStatus("msg System maintenance at 22:00"); - -// Example: clear the status message -var r3 = await HandleStatus("msg"); - -// Example: unknown status yields an error -var r4 = await HandleStatus("busy"); -``` +HandleStatus centralizes parsing and validation of status-related commands and delegates actual state updates to the `OnSetStatus` callback, keeping the parser as a pure command translator. This separation helps ensure consistent behavior across the UI and core models while allowing the rest of the system to react to status changes. It also encodes results via `CommandResult`, making success and error messages explicit to callers. ## Notes -- The status argument is treated case-insensitively due to ToLowerInvariant, so "Online" and "online" behave the same. -- If a known status is supplied with additional text (e.g., "online extra"), the method returns an error including StatusUsage. -- Even when OnSetStatus is null, the method returns a meaningful CommandResult message and avoids side effects; hook up OnSetStatus to enact real status changes when available. +- Unknown status tokens yield an error and show the usage hint. +- Passing `msg`/`message` with no trailing text clears the status message; providing text sets the message. +- If `OnSetStatus` is `null`, the method returns a success `CommandResult` but no external state is updated. --- @@ -786,14 +743,13 @@ private async Task HandleTestSound() **Returns:** `Task` -HandleTestSound is a private async helper that, when an OnTestSound callback is provided, awaits that delegate to play a test notification sound, and then returns a CommandResult signaling success with the message 'Playing notification sound...'. +Handles the test sound command by optionally invoking the `OnTestSound` callback if it is provided, then returns a `CommandResult` indicating success with the message `Playing notification sound...`. The method is `async` to accommodate the potential asynchronous callback invocation. ## Remarks -Serves as a thin abstraction that decouples the act of playing a test sound from the command result flow. By wrapping an optional callback in a single, awaitable operation, it keeps the surrounding command-handling code concise and testable, while allowing the actual sound playback logic to be supplied (or mocked) at runtime. +This method serves as a small adapter around an optional delegate: it defers the actual sound-playing work to `OnTestSound` when supplied, and it guarantees a consistent command result is returned to the caller. By centralizing the return value to a `CommandResult` with a success flag, it keeps the command-handling flow uniform across potential test sound implementations. It also isolates the scheduling of the sound-playing action from the rest of the command handling, making the behavior easy to mock in tests. ## Notes -- If OnTestSound throws, the exception propagates to the caller because HandleTestSound doesn't catch it. -- The returned CommandResult is always constructed after OnTestSound completes (when present), with a hard-coded success value; the result does not reflect any potential failure inside the OnTestSound callback. +- If `OnTestSound` throws, this method will propagate the exception since there is no try/catch around it; callers should handle failures at a higher level. --- @@ -814,15 +770,30 @@ private async Task HandleTheme(string args) **Returns:** `Task` -HandleTheme processes the /theme command. If the user supplies no name, it returns a usage message guiding the caller to provide a theme name from the User menu's theme list (e.g. Default, Transparent, TransparentLight, Hacker). If a name is provided, it forwards the trimmed name to the OnSetTheme callback (if it is set) and returns a confirmation indicating the theme was switched. +Handles the `/theme` command by validating the provided theme name, optionally dispatching the change via the `OnSetTheme` callback, and returning a structured `CommandResult` that reflects the outcome. When a non-empty argument is supplied, it trims the value, awaits `OnSetTheme` if a handler is registered, and returns a success message: `Theme switched to: `. If the argument is missing or whitespace, it returns an error `CommandResult` with usage guidance containing the supported usage: `Usage: /theme — pick one from the User menu's theme list (e.g. Default, Transparent, TransparentLight, Hacker)`. ## Remarks -By delegating to OnSetTheme, this method keeps command parsing separate from the actual theme application. It also ensures a consistent user feedback surface through CommandResult, regardless of how themes are implemented elsewhere in the codebase. +The `HandleTheme` method decouples command handling from the actual theme application by exposing an optional `OnSetTheme` callback. This allows hosting environments to plug in their own theme behavior without the command logic needing to know how themes are stored or applied. By funneling user feedback through the `CommandResult`, callers receive a consistent success/failure signal and message formatting. ## Notes -- Validation of the theme name itself is not performed here; it is expected to be validated by OnSetTheme or the surrounding UI layer. -- If OnSetTheme is null, the method still returns a success message, which means the theme change may not be applied. -- Whitespace-only input is rejected with the usage message due to the IsNullOrWhiteSpace check. +- Be aware that exceptions thrown by `OnSetTheme` propagate to the caller since there is no internal try/catch around the await. If you want robust error handling, consider wrapping the callback invocation or validating themes within the delegate itself. +- The method trims input before processing, so leading/trailing whitespace is ignored when applying the theme. + +## Dependencies +- CommandResult + +## Dependency APIs (verified signatures) +The REAL, parser-verified API surface of this symbol's collaborators: + +- record `CommandResult` (`src/EchoHub.Client/Commands/CommandHandler.cs`) + +## Symbol To Document +- Name: `HandleTheme` +- Kind: method +- File: `src/EchoHub.Client/Commands/CommandHandler.cs` +- Language: `csharp` +- ID: 92d79819-5755-4464-a323-9101267cd9ab + --- @@ -843,16 +814,15 @@ private async Task HandleTopic(string args) **Returns:** `Task` -Processes a topic-setting command by validating that text is provided, optionally forwarding the trimmed topic to a consumer via OnSetTopic, and returning a CommandResult that indicates success or instructs the user on proper usage. If no text is supplied, it immediately returns an error result with a usage hint; otherwise it applies the topic (when a listener is present) and responds with a confirmation message reflecting the new topic. +Handles the `/topic` command by validating the input and optionally delegating to the `OnSetTopic` callback. If the `args` parameter is null or whitespace, it returns a `CommandResult` containing a usage message and marks the result as an error. Otherwise, it trims the input, invokes `OnSetTopic` if provided, and returns a successful `CommandResult` with the message `Topic set to: {trimmed}`. ## Remarks - -- By accepting an optional OnSetTopic callback, this method decouples the command parsing from the actual topic application. It trims input to standardize the topic value and uses a single, consistent success message. The async nature allows the topic application to perform I/O-bound work without blocking the caller. +This method serves as a small, focused command-dispatch wrapper: it performs input validation, keeps the topic persistence/update logic decoupled via the `OnSetTopic` hook, and consistently reports the outcome back to the caller. By making the topic update an optional hook, the surrounding system can decide how to apply or broadcast the new topic without changing the command-handling flow. The trimming of the topic text ensures user input is normalized before persistence and confirmation. ## Notes - -- If OnSetTopic throws, the resulting Task will fault; callers may wish to handle exceptions when the topic application performs I/O or other operations. -- The method is private and intended to be used by the surrounding command-handling workflow; external callers should interact with higher-level command APIs rather than this helper directly. +- If `OnSetTopic` throws, the exception will propagate to the caller since there is no local exception handling here. +- The method short-circuits on empty or whitespace-only `args`, returning a usage-style error message instead of attempting to set a topic. +- It is a private member, indicating it's intended for internal command processing rather than public API consumption. --- @@ -873,16 +843,14 @@ private async Task HandleUnban(string args) **Returns:** `Task` -Handles the /unban command by validating the provided username and triggering the unban workflow if a handler is wired up. If the caller passes an empty or whitespace-only string, it returns an error CommandResult with a usage hint. When a username is supplied, it trims whitespace, calls the OnUnbanUser delegate if it exists, and then returns a success result with a live message indicating the unban operation is underway. +The `HandleUnban` method processes the `/unban` command by validating the input and delegating the unban action to an optional callback. If the input is missing or whitespace, it returns a `CommandResult` with an error message and usage instructions via `string.IsNullOrWhiteSpace`. If a username is provided, it trims it with `args.Trim()` and, if an `OnUnbanUser` handler is assigned, awaits it. It then returns a success `CommandResult` indicating the unbanning of the specified user. ## Remarks - -By delegating the actual unban action to OnUnbanUser, this method remains agnostic of how bans are enforced (in-memory, persisted, or communicated to another service). This separation concerns input validation, user feedback, and orchestration, while leaving the business logic to the registered handler. The approach also makes it straightforward to unit-test the command flow by supplying a mock OnUnbanUser and asserting that it was invoked with the trimmed username. +This method centralizes the unban command's validation and delegation, keeping the command handling logic decoupled from the actual unban implementation. It relies on a potential consumer exposed via `OnUnbanUser` to perform the unban, allowing the hosting context to supply the concrete behavior. ## Notes - -- If OnUnbanUser throws, the exception will propagate to the caller since there is no try/catch here. -- The error path uses IsError: true to signal invalid usage; a non-empty username will yield a non-error CommandResult with the final message. +- If `OnUnbanUser` is assigned and throws, the exception propagates to the caller because there is no internal catch block. +- The final user-facing message uses the trimmed username via `args.Trim()` to avoid leading/trailing spaces in the display. --- @@ -903,15 +871,36 @@ private async Task HandleUnmute(string args) **Returns:** `Task` -Handles the /unmute command by validating its argument, optionally triggering a delegated action, and returning a user-facing result. If no username is provided, it returns an error CommandResult with usage instructions. When a username is supplied, it trims whitespace, invokes the OnUnmuteUser callback if present, and finally returns a success CommandResult indicating that the unmute process has started for that user. +````markdown +"""Description""" + +`HandleUnmute` is a private async method that processes the `/unmute` command for a given username. It first validates the input; if the argument is null or whitespace it returns a `CommandResult` marked as an error with the usage hint. If a username is provided, it trims whitespace and, if the `OnUnmuteUser` callback is wired, awaits its execution with the username. It then returns a success `CommandResult` with a human-friendly message indicating that the unmute action is underway. This method encapsulates small command-handling logic and centralizes user feedback, while delegating the actual unmute operation to the consumer via `OnUnmuteUser`. + +""" ## Remarks -Encapsulates the unmute flow behind a private helper, decoupling the UI command parsing from the actual unmute operation. By deferring to an optional OnUnmuteUser delegate, it enables testability and flexible wiring of the unmute logic. The method also normalizes input by trimming the username before use. + +By funneling the unmute action through a delegate (`OnUnmuteUser`), the symbol decouples command parsing from domain logic, enabling tests and replacements of the unmute behavior without changing the command handler. The method always produces a `CommandResult`, ensuring the caller can render consistent feedback regardless of whether the unmute handler is attached. The input normalization with `args.Trim()` prevents issues from extra whitespace. If no handler is attached, the call is effectively a no-op besides returning the success message, which is a deliberate design choice to keep the user experience consistent. + +## Example + +```csharp +// Typical usage within the command handler class +OnUnmuteUser += async username => { + // Actual unmute logic would run here + await Task.CompletedTask; +}; + +var result = await HandleUnmute("Alice"); +// result.IsError == false and result.Message contains "Unmuting Alice..." (subject to implementation) +``` ## Notes -- The input username is trimmed before passing to the callback and before including in the final status message, preventing issues caused by surrounding whitespace. -- If OnUnmuteUser is null, the method returns a success result without performing any side effects, allowing the UI to display a consistent message even when no backend action is wired. -- The method is private; it is intended to be invoked through the command-handling pipeline rather than called directly from external code to preserve encapsulation. +- The error path returns a `CommandResult` with `IsError` set (e.g., to indicate invalid usage). +- The actual unmute action is delegated to `OnUnmuteUser`; if this delegate is null, the call does not perform unmuting but still returns a success message. +- Whitespace in the input is trimmed before any processing to normalize the username. +```` + --- @@ -926,14 +915,10 @@ private async Task HandleUsers() **Returns:** `Task` -Handles the 'list users' command by optionally invoking an OnListUsers callback and then returning a successful CommandResult. If a consumer provides OnListUsers, that callback is awaited; otherwise the method completes by returning a CommandResult indicating success. This wrapping method keeps the command dispatch path consistent while delegating the actual listing logic to the injected delegate. +Invokes the optional `OnListUsers` callback to perform the user listing and always yields a successful `CommandResult` (`new CommandResult(true)`). If a consumer supplies `OnListUsers`, the method awaits `OnListUsers()`; otherwise it completes without performing any listing. ## Remarks -A thin wrapper in the command handling pipeline. It delegates the actual listing to OnListUsers via a delegate and then yields a consistent CommandResult(true). This separation allows tests to inject different listing behaviors without altering the caller, and keeps the path for listing users uniform. - -## Notes -- OnListUsers exceptions propagate to the caller; this method does not catch errors. -- No cancellation token is observed or supported here. +By delegating the actual listing work to the `OnListUsers` delegate, this method acts as a thin adapter that preserves a consistent command result while allowing the listing behavior to be swapped or mocked in tests. It decouples the command flow from the UI or data access concerns and ensures a predictable outcome regardless of whether a listing handler is provided. --- @@ -954,14 +939,13 @@ public bool IsCommand(string input) => input.StartsWith('/') **Returns:** `bool` -IsCommand is a tiny predicate that determines whether an input string should be handled as a command by checking if the first character is '/'. Use it at the entry point of the command-processing path to decide whether to route the input to the command parser rather than treating it as ordinary text, thereby keeping command-detection logic in one place. +IsCommand determines whether the provided input should be treated as a command by checking if it starts with the '/' prefix. This predicate is used to route input to the command-handling path rather than treating it as plain text. ## Remarks -IsCommand encapsulates the slash-prefix convention used to invoke commands, providing a single, testable contract for command detection. This keeps command routing decoupled from unrelated input handling and makes future changes—such as supporting a different prefix or multiple prefixes—easier to implement. It also clarifies intent at call sites by replacing ad-hoc prefix checks with a well-named predicate. +This tiny predicate encapsulates the command-prefix rule, allowing future changes (for example, supporting additional prefixes or configurable behavior) to be implemented in one place. It clarifies the separation of concerns: any code that needs to decide between command parsing and regular message processing should rely on this method's boolean result rather than duplicating the prefix check. `IsCommand` acts as a single source of truth for command-detection logic. ## Notes -- Null input may cause a NullReferenceException since input.StartsWith is called directly on input. -- Strings with leading whitespace won't be treated as commands unless trimmed; consider input = input.TrimStart() or adjust logic. +- Passing a null `input` will throw a `NullReferenceException` when evaluating `StartsWith`; ensure callers perform null checks or the parameter is made non-nullable in line with your project's nullability rules. --- @@ -983,14 +967,53 @@ private static bool IsValidHex(string s) => **Returns:** `bool` -This private helper returns true if every character in the input string is an ASCII hexadecimal digit (0–9, A–F, a–f); otherwise it returns false. It is intended for internal use within the command handler to validate hex-like input before parsing or converting it to binary data. +IsValidHex is a private static predicate that returns true when every character in the input string `s` is an ASCII hexadecimal digit, i.e., one of `0`–`9`, `A`–`F`, or `a`–`f`. Implemented as a single expression using `s.All(c => char.IsAsciiHexDigit(c))`, it serves as a compact guard to validate hex-like inputs before parsing or further processing. Developers reach for this helper when a quick, inline check is needed to enforce that an input contains only ASCII hex digits, rather than performing a manual loop or pattern match. + +## Remarks +Because it's private and static, it encapsulates the rule inside the command handler's implementation, preventing duplication and keeping the public API cleaner. It relies on `char.IsAsciiHexDigit`, which ensures strict ASCII-only digits rather than any Unicode hex digit, and therefore may reject strings that conceptually represent hex values but include non-ASCII characters. The function is pure and side-effect free; its result depends solely on the input string. + +## Notes +- Null input will throw a `NullReferenceException` when the expression `s.All(...)` is evaluated; ensure `s` is non-null before calling `IsValidHex`, or guard the call site accordingly. + +--- + +### ParsePathAndSizeFlag +> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` +> **Kind:** method + +```csharp +private static (string Path, string? Size) ParsePathAndSizeFlag(string args) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `Path` | `string` | — | +| `Size` | `string?` | — | + + +Parses a single argument string to extract a file path (which may be quoted) and an optional size specifier. The size flag can be written as -s, -m, or -l either before or after the path. It returns a tuple (Path, Size) where Path is the path with surrounding quotes removed, and Size is null if no flag was found or 's', 'm', or 'l' if a flag is present. This encapsulates a tiny, CLI-like parsing rule so the rest of the command handling can rely on a normalized pair rather than slicing and quoting logic itself. ## Remarks -IsValidHex centralizes hex-character validation, promoting consistent input checks across the command handling code. By leveraging the built-in IsAsciiHexDigit predicate, it stays robust against locale issues and clearly expresses the intent of the check. As a private static member, it remains an internal utility rather than part of the public API, simplifying maintenance and testing within the containing class. +This helper centralizes input normalization for a small command-line style syntax used by the command handler. It trims whitespace, supports quoted paths with spaces, and detects the flag in either position (start or end). When both forms could apply, the trailing-flag form takes precedence; after recognizing a flag, the path is trimmed accordingly and quotes are stripped via `StripQuotes` so callers always receive a clean path string. The returned `Size` is a single-letter value ("s", "m", or "l") or `null` when no flag is present, enabling callers to branch on the presence of a size specifier without re-parsing the string. + +## Example +```csharp +// Trailing flag +var (path1, size1) = ParsePathAndSizeFlag("C:\\Files\\report.txt -m"); +// path1 == "C:\\Files\\report.txt", size1 == "m" + +// Leading flag with quoted path +var (path2, size2) = ParsePathAndSizeFlag("-s \"My Documents\\data.csv\""); +// path2 == "My Documents\\data.csv", size2 == "s" +``` ## Notes -- Null input will throw a NullReferenceException when evaluated, since the method dereferences the input string. If you need null-tolerant behavior, guard the parameter before calling this helper. -- An empty string is considered valid by this implementation because All over an empty sequence returns true. If empty input should be rejected, add an explicit check before invoking this method. +- The size value is a single-letter string: `"s"`, `"m"`, or `"l"`, or `null` if no flag is found. Never assume a full word; the implementation maps only these short forms. +- The path is always passed through `StripQuotes`, so callers work with an unquoted path even if the input used quotes. +- The parser only recognizes flags at the defined positions (start or end) and requires a space separating the flag from the path segment in its respective form. Inputs that don’t match these patterns will yield a `Size` of `null` and a trimmed path without quotes. + --- @@ -1011,22 +1034,30 @@ private static string StripQuotes(string s) **Returns:** `string` -Removes surrounding quotes from a string when the value is wrapped in matching single or double quotes; otherwise it returns the input unchanged. Use this helper to normalize command arguments that may be quoted, instead of duplicating substring logic or handling quotes at every parse path. +StripQuotes removes matching surrounding quotes from a string. If the input begins and ends with the same quote character, either a double quote or a single quote, it returns the inner content (`s[1..^1]`). Otherwise, it returns the original string unchanged (`s`). This private helper is typically used in command or argument parsing to normalize values by removing wrapping quotes before further processing. Note that the method assumes a non-null input; passing a null input will throw a `NullReferenceException`. ## Remarks -Private static helper inside the command handling path, StripQuotes encapsulates a small, focused normalization concern. It prevents scattering the same quote-stripping logic across multiple call sites and makes the intended behavior (remove only matching outer quotes) explicit. The method uses C# range and index syntax (s[1..^1], s[^1]) for a compact implementation. +This tiny utility encapsulates a common formatting concern: stripping a wrapping quote from a value. It centralizes the logic so command and argument parsing can rely on a single, consistent normalization step instead of duplicating code at multiple sites. Because it is private and static, it's intended as a local helper within the class that owns it rather than a general-purpose API. ## Example ```csharp -string input = "\"hello\""; -string output = StripQuotes(input); // output == "hello" +string a = "\"hello\""; +string r1 = StripQuotes(a); // hello + +string b = "'world'"; +string r2 = StripQuotes(b); // world + +string c = "\"mismatch'"; +string r3 = StripQuotes(c); // "mismatch' + +string d = "plain"; +string r4 = StripQuotes(d); // plain ``` ## Notes -- It only strips when both ends are the same quote character. -- It does not interpret escape sequences or nested quotes; quotes inside remain. -- Because it is private, external consumers cannot call it; to reuse externally, expose a public wrapper or move to a shared utility. -- Requires C# 8+ for index and range syntax (s[^1], s[1..^1]). +- Null inputs are not handled; a null argument will throw `NullReferenceException`. +- Surrounding quotes are stripped only when both ends are the same quote type (both double quotes or both single quotes); otherwise the string is returned unchanged. +- It is private to the class and not intended for external use; if reuse is required, consider extracting a public helper. --- @@ -1040,14 +1071,10 @@ private const string StatusUsage = ``` -StatusUsage is a private constant string that holds the canonical usage text for the /status command used by the command handler. It defines two forms of input: either selecting a predefined status (online, away, dnd, invisible) or providing a custom message with /status msg . An empty text clears the current status. Centralizing this string avoids duplicating literals and helps keep the command’s user-facing guidance consistent across the codebase. +StatusUsage is the canonical usage text for the `/status` command. It defines the accepted inputs: `/status online`, `/status away`, `/status dnd`, and `/status invisible`, as well as the variant `/status msg ` for setting a textual status, where an empty `` clears the status. This constant is used by the command handler to present consistent usage information and to validate user input without duplicating strings elsewhere. ## Remarks -By keeping the usage text in a private field, the implementation encapsulates help and validation concerns within CommandHandler.cs. This makes it easy to adjust the wording or supported syntax in one place. If localization or broader reuse is needed later, this constant should be replaced with a resource or exposed via a helper so external components can reference it consistently. - -## Notes -- If you extend supported presets, update both the hard-coded usage and the parsing logic; otherwise the command may reject valid inputs or mislead users. -- Because it's private, external code/tests can't reference it directly; consider exposing a read-only accessor or moving to a resource to improve testability. +`StatusUsage` encapsulates the exact syntax users must follow for the `/status` command, acting as a single source of truth that the command handler relies on when parsing input and presenting help. Keeping it as a private constant prevents duplication and mismatches between parsing logic and user-facing messages, and it simplifies future localization or extension of the command. --- @@ -1068,32 +1095,16 @@ public record CommandResult(bool Handled, string? Message = null, bool IsError = | `IsError` | `bool` | `false` | -Represents the outcome of handling a command as a compact, immutable value. It indicates whether the command was processed (Handled), carries an optional diagnostic or user-facing message (Message), and signals whether the outcome is an error (IsError). Use this as the return type from a command handler to convey success or failure without relying on exceptions, and to expose contextual details to the caller. - -## Remarks -CommandResult acts as a lightweight contract between command invokers and handlers. By being a C# 9 record, it benefits from value-based equality and immutability, enabling safe sharing and straightforward comparisons in tests or across layers. The IsError flag clarifies how callers should react, while a non-null Message can provide actionable context in logs or UI. - -## Example -```csharp -// Successful handling with no extra message -var success = new CommandResult(true); - -// Failed handling with diagnostic -var failure = new CommandResult(false, "Unknown command", true); -``` - -## Notes -- Message may be null; always guard before displaying to users. -- Records support with-expressions, so you can derive a near-identical result with a different Message or IsError without reconstructing all fields. +Represents the outcome of processing a command in the command handling flow. Use `CommandResult` to convey whether the command was handled, optional detail text in [`Message`](../../EchoHub.Core/Models/Message.cs.md), and whether the result represents an error via `IsError`. As a `record`, it benefits from value-based equality and immutability, allowing straightforward comparisons and safe sharing of command outcomes. --- -## HandleDownloadPath +## HandleAsciiSize > **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` > **Kind:** method ```csharp -private async Task HandleDownloadPath(string args) +private async Task HandleAsciiSize(string args) ``` **Parameters:** @@ -1105,26 +1116,23 @@ private async Task HandleDownloadPath(string args) **Returns:** `Task` -Handles the download path configuration by consuming an optional argument. If the trimmed argument is empty, it signals the OnSetDownloadPath callback to open the native folder picker; if a value is provided, that value is used as the path to set directly. If OnSetDownloadPath is not wired, the call is a no-op and the method still returns a successful CommandResult. +Handles an ASCII size command by delegating to the `OnSetAsciiSize` callback when present and always returning a successful `CommandResult`. It trims the incoming `args` and forwards it to the registered handler; the inline comment indicates that no-argument invocations should open the size picker, while providing an argument (such as `s`, `m`, `l` or `small`, `medium`, `large`) sets the size. ## Remarks - -Serves as a thin adapter between the command processing layer and the UI/persistence logic that applies the download path. It centralizes the branching logic: either prompt the user for a path via the native picker or apply a provided path, without forcing the caller to know which path was chosen. This keeps command handling simple while delegating the actual path application to a separate, testable component. +This method acts as a thin command adapter that decouples input parsing from the actual size-changing logic. By invoking the optional `OnSetAsciiSize` callback, it allows UI or domain logic to implement the size selection behavior while keeping the command handling surface minimal and testable. If no handler is registered (`OnSetAsciiSize` is null), the call is effectively a no-op aside from returning a success result. ## Notes +- Potential null-argument risk: if `args` is null, `args.Trim()` will throw a `NullReferenceException`. Ensure callers provide a non-null string or guard against null before trimming. -- If OnSetDownloadPath is null, the method completes without changing the path; callers should ensure the callback is assigned before invocation. -- Whitespace-only arguments are treated as empty after trimming, which triggers the native folder picker behavior. -- The method does not perform path validation; downstream logic or the callback is responsible for validating the path. --- -## HandleJoin +## HandleDownloadPath > **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` > **Kind:** method ```csharp -private async Task HandleJoin(string args) +private async Task HandleDownloadPath(string args) ``` **Parameters:** @@ -1136,98 +1144,36 @@ private async Task HandleJoin(string args) **Returns:** `Task` -HandleJoin is a private asynchronous command handler that processes the /join command by validating input, extracting a channel and an optional password, and delegating the actual join operation to a callback if one is registered. It normalizes the channel name by removing a leading '#', and returns a usage error when no arguments are supplied; otherwise it invokes the join hook and returns a success result. +HandleDownloadPath asynchronously forwards a download path instruction to the registered host via the `OnSetDownloadPath` callback when available, passing the trimmed `args`. This design allows either supplying a path directly (via `args`) or prompting the host to present its native folder picker, while always returning a successful `CommandResult`. ## Remarks -HandleJoin serves as the bridge between user input and the join logic by decoupling command parsing from the actual join implementation. It relies on the OnJoinChannel callback to perform the real join work, enabling testability and flexibility by swapping in different join strategies. The normalization step (stripping a leading '#') supports common user conventions for channel identifiers. +This method serves as a thin adapter between the command system and the host's path-picking experience. It centralizes the decision to either apply a provided path or delegate to a UI prompt, keeping the command handler decoupled from platform specifics. ## Notes -- The password extraction line in the snippet appears garbled (a redaction artifact). Ensure a properly declared password variable is assigned from parts[1] when present. -- Accessing parts[1] without checking parts.Length could lead to an IndexOutOfRangeException if the user supplies only a channel name. -- If OnJoinChannel is null, the method will return a successful CommandResult even though no join occurred; consider whether a different outcome is desired when no handler is attached. - -## Dependencies -- CommandResult -- StringSplitOptions - -## Dependency APIs -- CommandResult (record) — src/EchoHub.Client/Commands/CommandHandler.cs -- StringSplitOptions (enum) — System/StringSplitOptions (used in the Split call) - -## Symbol To Document -- Name: HandleJoin -- Kind: method -- File: src/EchoHub.Client/Commands/CommandHandler.cs -- Language: csharp -- ID: 7f13a65c-5457-40c2-b652-518b98329639 +- If `OnSetDownloadPath` is `null`, the method returns `true` without any path being set; callers should not assume a path was applied in this case. --- -## HandleNuke +## HandleServers > **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` > **Kind:** method ```csharp -private async Task HandleNuke() +private async Task HandleServers() ``` **Returns:** `Task` -HandleNuke is a concise asynchronous command handler that, when invoked, optionally invokes the OnNukeChannel callback to perform the nuking operation and then returns a CommandResult indicating the channel history is being nuked. - -## Remarks -This method acts as a thin wrapper around nuking logic, decoupling the command invocation from the actual nuking work by delegating to OnNukeChannel. It always returns a CommandResult, enabling a uniform response path whether or not a callback is attached. - -## Source Code -```csharp -private async Task HandleNuke() -{ - if (OnNukeChannel is not null) - await OnNukeChannel(); - return new CommandResult(true, "Nuking channel history..."); -} -``` - -## Dependencies -- CommandResult - -## Dependency APIs (verified signatures) -- record `CommandResult` (`src/EchoHub.Client/Commands/CommandHandler.cs`) - -## Symbol To Document -- Name: `HandleNuke` -- Kind: `method` -- File: `src/EchoHub.Client/Commands/CommandHandler.cs` -- Language: `csharp` -- ID: ecfce33b-9722-42e3-ab86-a70abb710ed8 - ---- - -## ParsePathAndSizeFlag -> **File:** `src/EchoHub.Client/Commands/CommandHandler.cs` -> **Kind:** method - -```csharp -private static (string Path, string? Size) ParsePathAndSizeFlag(string args) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `Path` | `string` | — | -| `Size` | `string?` | — | - - -Parses a single argument string to extract a file path (which may be quoted) and an optional size flag (-s, -m, or -l). The flag can be placed either before or after the path, provided there is a separating space. The method returns a tuple (Path, Size) where Path is the cleaned file path and Size is null or the single-letter flag ('s', 'm', or 'l'). The Path is produced by stripping surrounding quotes via StripQuotes. +HandleServers is a small asynchronous helper that conditionally delegates to the `OnOpenServers` hook and then returns a successful `CommandResult`. If a consumer has attached an `OnOpenServers` handler, the method awaits it; otherwise it completes immediately. This pattern keeps the command flow decoupled from the concrete implementation of opening servers while centralizing the control flow in this private handler. ## Remarks -Centralizes argument parsing for command-line handling. It supports both -flag path and path -flag formats, selecting the flag based on the first matching pattern and ensuring unambiguous separation by a space. The path is normalized by removing surrounding quotes, reducing downstream quoting concerns and making the return value straightforward to consume. +Because `HandleServers` forwards to an optional delegate, its value is to provide a single, testable point for the open-servers workflow. It decouples the command invocation from the actual opening logic, enabling substitution of the behavior via `OnOpenServers` without changing call sites. ## Notes -- If both start and end forms are present, the end form wins because the end-check runs first and the start form is only considered if no end flag was found. -- A space is required to delimit the flag from the path (either before or after); without the space, the flag is not recognized. -- This is an internal helper (private) intended for use within the command handling logic; external callers cannot rely on it directly. +- The method will return `new CommandResult(true)` after the delegate completes, so success is reported if the delegate completes without throwing. +- If `OnOpenServers` throws, the exception propagates to the caller; there is no internal exception handling in this method. +- If `OnOpenServers` is null, the method completes immediately with a successful `CommandResult`. + --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Config/ClientConfig.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Config/ClientConfig.cs.md index 23b724f..56d7733 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Config/ClientConfig.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Config/ClientConfig.cs.md @@ -20,15 +20,24 @@ public class AccountPreset ``` -AccountPreset is a lightweight data container that groups three optional account identity properties—DisplayName, Bio, and NicknameColor—so 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 client’s 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 - { - 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 (0–255). If your UI operates in a 0–100 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 user’s 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 + { + { "general", "base64encryptedKeyHere" } + }, + LeftChannels = new List { "old-channel" }, + LastReadMessages = new Dictionary + { + { "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 client’s 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Config/ConfigManager.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Config/ConfigManager.cs.md index 48b2d37..ca52b9b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Config/ConfigManager.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Config/ConfigManager.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Program.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Program.cs.md index 3813afe..27dac9e 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Program.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Program.cs.md @@ -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). \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/ApiClient.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/ApiClient.cs.md index a6b809d..3fa6e15 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/ApiClient.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/ApiClient.cs.md @@ -43,13 +43,13 @@ - [RekeyChannelAsync](#rekeychannelasync) - [RevokeInviteAsync](#revokeinviteasync) - [SendMessageWithAttachmentsAsync](#sendmessagewithattachmentsasync) + - [SendUrlAsync](#sendurlasync) - [SetTokens](#settokens) - [UnbanUserAsync](#unbanuserasync) - [UnmuteUserAsync](#unmuteuserasync) - [UpdateChannelTopicAsync](#updatechanneltopicasync) + - [UpdateProfileAsync](#updateprofileasync) - [UploadAvatarAsync](#uploadavatarasync) -- [SendUrlAsync](#sendurlasync) -- [UpdateProfileAsync](#updateprofileasync) --- @@ -62,34 +62,16 @@ public sealed class ApiClient : IDisposable ``` -A high-level HTTP client for EchoHub that centralizes authentication (login, refresh, logout) and common server operations (channels, messages, uploads, invites, profile). Use ApiClient when you want a single, in-memory service to manage access/refresh tokens, provide a token for long-lived connections (SignalR), and call the server's REST endpoints through convenient async methods. +A high-level HTTP client that encapsulates the application's server API surface and manages authentication state (access token, refresh token and expiration). Reach for `ApiClient` when you need a single reusable component to perform user authentication (`RegisterAsync`, `LoginAsync`, `LoginWithRefreshTokenAsync`, `LogoutAsync`), query chat data (`GetChannelsAsync`, `GetChannelMetaAsync`, `GetChannelCryptoAsync`), upload/download assets (`UploadAvatarAsync`, `DownloadFileToTempAsync`) and send messages (`SendMessageWithAttachmentsAsync`, `SendUrlAsync`) while keeping token refresh logic co-located with the HTTP interactions. The client exposes `BaseUrl`, the current `Token`/`RefreshToken`, and an `OnTokensRefreshed` event consumers can subscribe to. ## Remarks -This class wraps an HttpClient and keeps the current access and refresh tokens in memory, exposing them via the Token and RefreshToken properties and notifying consumers through the OnTokensRefreshed event. GetValidTokenAsync is intended as the token provider for long-lived connections and will proactively refresh an access token that is near expiry (the implementation refreshes if the token expires within about 60 seconds). Methods that return DTOs commonly use nullable results to indicate "not found" or absence. - -## Example -```csharp -// Create the client and log in -using var api = new ApiClient("https://api.example.com"); -var login = await api.LoginAsync("alice", "s3cret"); - -// Persist tokens or react when they've been refreshed -api.OnTokensRefreshed += () => -{ - var latestAccess = api.Token; - var latestRefresh = api.RefreshToken; - // Save to secure storage if needed -}; - -// Use the token provider for a SignalR connection or call APIs that need auth -var tokenForSignalR = await api.GetValidTokenAsync(); -var channels = await api.GetChannelsAsync(); -``` +`ApiClient` centralizes network calls and authentication for the client-side application: it owns a single `HttpClient` instance (`_http`), stores the current tokens and expiration, and provides convenience methods that map to common server endpoints (login/registration, channels, invites, user profile, file operations and message sending). It also provides `GetValidTokenAsync` specifically for token providers (for example, a [`EchoHubConnection`](EchoHubConnection.cs.md) SignalR token provider) so callers can obtain a token that will be refreshed if it is about to expire. Because these responsibilities cross-cut the UI and real-time layers, the class exists to avoid scattering token refresh and HTTP wiring throughout the codebase. ## Notes -- GetValidTokenAsync may refresh the token when it will expire within ~60 seconds; callers should still handle server-side authorization failures. -- LogoutAsync is implemented as "best-effort" — it may not always revoke server-side state; clear persisted tokens yourself if you saved them. -- Tokens are stored only in memory by this client. To persist them across restarts, subscribe to OnTokensRefreshed and save Token/RefreshToken externally. +- Token refresh timing and concurrency: the implementation intends to "Refresh if token expires within 60 seconds" (see `GetValidTokenAsync`), and one comment notes that callers should "Return current token and let the caller handle auth failure." The source does not show explicit synchronization around token refresh, so concurrent callers could trigger multiple simultaneous refresh attempts. +- Attachments and encrypted channels: `SendMessageWithAttachmentsAsync` requires that for end-to-end encrypted channels each file has a declared kind and a room-encrypted preview; these must be provided in the same order as the files so the server's attachment index aligns. Non-image attachments use an empty preview string; the caption is also room-encrypted. +- Logout and disposal: `LogoutAsync` is described as a "best-effort logout" (errors are non-fatal). The type implements `IDisposable`, so callers should dispose the `ApiClient` instance when finished to allow it to release its resources. + --- @@ -108,19 +90,14 @@ public ApiClient(string baseUrl) | `baseUrl` | `string` | — | -Initializes an ApiClient with a base URL, normalizes the URL by removing any trailing slash, and configures an HttpClient whose BaseAddress is that normalized URL. This ensures that subsequent requests can be issued with relative URIs against the configured API host. +The `ApiClient` constructor initializes the client to target a REST API at the provided `baseUrl`. It trims any trailing slash from the input, assigns the result to `BaseUrl`, and creates a new `HttpClient` stored in `_http` with its `BaseAddress` set to a `Uri` built from `BaseUrl`. This setup enables subsequent requests to use relative endpoints against the configured base address. ## Remarks -This constructor centralizes HTTP client initialization for the API client, enforcing a consistent base address across calls. By normalizing the base URL, it prevents subtle URI mistakes when composing requests. It encapsulates the HttpClient setup so callers don’t have to manage base addresses or client lifetimes directly. - -## Example -```csharp -var client = new ApiClient("https://api.example.com/"); -``` +By centralizing base URL handling and HTTP client initialization, this constructor ensures a consistent, reusable `HttpClient` instance per `ApiClient` and avoids URL-assembly errors caused by trailing slashes. ## Notes -- baseUrl must be non-null; passing null will throw a NullReferenceException at TrimEnd('/'). -- If baseUrl is not a well-formed absolute URL, creating new Uri(BaseUrl) will throw UriFormatException. +- Passing `null` for `baseUrl` will throw a `NullReferenceException` when `TrimEnd('/')` is invoked. +- Passing an empty or otherwise invalid URL will throw a `UriFormatException` when constructing `new Uri(BaseUrl)`. --- @@ -134,14 +111,7 @@ public string BaseUrl ``` -BaseUrl is a read-only string property that exposes the root URL the ApiClient uses to construct request URIs. It provides a single source of truth for the API endpoint so callers can reason about where requests are sent without mutating the value at runtime. - -## Remarks -BaseUrl serves as the canonical root for all REST calls performed by the ApiClient. Centralizing the endpoint simplifies testing across environments (dev, staging, prod) because the rest of the client can build URIs without caring about the actual host. Since it is read-only, the value is established during construction; to change environments you create a new client instance with a different BaseUrl. It integrates with the request pipeline by being the starting point for path composition. - -## Notes -- Value is set in constructor; to change it, instantiate new ApiClient with a different BaseUrl. -- When combining with path segments, avoid manual string concatenation; prefer proper Uri or relative path builders to prevent double slashes. +This read-only property exposes the base endpoint URL the `ApiClient` uses to build full request URIs. Reading `BaseUrl` helps diagnostics and test scenarios where you need to know or confirm the target server without inspecting internal configuration. --- @@ -154,15 +124,7 @@ public string? RefreshToken => _refreshToken ``` -The RefreshToken property is a read-only accessor that returns the current value of the private _refreshToken field. Because it is nullable, callers should be prepared for a null value when no token is stored or when the token has been cleared. - -## Remarks -The property provides a lightweight way to observe or forward the stored refresh token without permitting mutation. It reinforces centralized token management by keeping updates to _refreshToken out of the consumer's hands, while still enabling advanced scenarios such as diagnostics or token-forwarding workflows that require the current token value. - -## Notes -- The value may be null; always account for a possible null return in calling code. -- Do not log or transmit the token in telemetry or UI streams; treat it as sensitive information and guard its exposure. - +The `RefreshToken` property exposes the current refresh token stored by the client as a read-only accessor. It returns the private backing field `_refreshToken` as a nullable `string`, meaning callers can read the value but cannot assign a new one through this property. --- @@ -175,10 +137,13 @@ public string? Token => _accessToken ``` -Exposes the current access token as a read-only, nullable string. This property simply forwards the value stored in the private backing field _accessToken, providing a stable API surface for callers that need to read the token without mutating the field. Since the value can be null when no token is present, callers should handle null gracefully, for example by guarding token-dependent logic or by omitting the token from headers when it is absent. +Public property `Token` is a read-only accessor that forwards to the private field `_accessToken`, returning its current value as a nullable string. It does not transform or mutate state; its purpose is to expose the managed token to callers who need to inspect or reuse the token value. Since the underlying field can be `null` before a token is obtained, callers should handle a possible null result when using `Token`. ## Remarks -This property acts as a stable abstraction over the token storage, decoupling callers from how and where the token is stored. It allows the rest of the API client to evolve its token management (for example, refreshing tokens or lazy loading) without affecting callers that just need to read the current value. It also signals that token retrieval is a side-effect-free operation, reinforcing a read-only contract for consumers. +This straightforward forwarder exists to surface the internal token while preserving encapsulation of the backing field. It supports diagnostics, testing, and scenarios where downstream components require the raw token without duplicating token management logic, without re-exposing the field directly. + +## Notes +- The value can be `null` until a token is obtained; always null-check before use. --- @@ -200,7 +165,15 @@ public async Task AssignRoleAsync(string username, ServerRole role) **Returns:** `Task` -AssignRoleAsync assigns a server-side role to a user by issuing an authenticated POST request to the moderation API. It builds an AssignRoleRequest from the provided username and role and posts it to /api/moderation/role. The call is preceded by EnsureAuthenticated() and followed by EnsureSuccessAsync(response), so authentication is enforced and failures are surfaced as exceptions. The method is asynchronous and returns a Task that completes when the server confirms success. +Assigns a server role to a user by performing an authenticated HTTP POST to `"/api/moderation/role"` with an [`AssignRoleRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md) that contains the target `username` and `role`. It enforces authentication via `EnsureAuthenticated()`, sends the request asynchronously through `AuthenticatedRequestAsync`, and validates the response with `EnsureSuccessAsync`. The method returns a `Task` and completes when the server confirms the assignment, or raises an exception if authentication fails or the server returns an error. + +## Remarks +By centralizing moderation actions in `ApiClient`, this method provides a single, authentication-aware path for role management. It hides HTTP details and enforces consistent error handling by combining `EnsureAuthenticated()`, `AuthenticatedRequestAsync`, and `EnsureSuccessAsync`, so callers can rely on a uniform success/exception model across moderation operations. + +## Notes +- This method returns a `Task` and does not produce a value; success is signaled by completion. +- Failures can arise from lack of authentication, server-side permission checks, or other HTTP errors surfaced by `EnsureSuccessAsync`. +- The call payload relies on [`AssignRoleRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md); changes to that DTO will affect this method. --- @@ -221,14 +194,14 @@ private async Task AuthenticatedGetAsync(string url) **Returns:** `Task` -Performs a GET request against the provided URL and, if the response is 401 Unauthorized and a non-empty refresh token is available, automatically refreshes the token and retries the request. The original response is disposed before replacing it with the retried response. The caller is responsible for disposing the final HttpResponseMessage returned by this method. If the token refresh fails, the original response is returned unchanged. +This private helper performs an HTTP GET to the specified `url` and, if the response is `HttpStatusCode.Unauthorized` and a non-empty `_refreshToken` is available, refreshes the token via `RefreshTokenAsync()` and retries the request once. The caller must dispose of the returned `HttpResponseMessage`. ## Remarks -By centralizing this refresh-and-retry logic, the API client avoids duplicating authentication-handling boilerplate across multiple calls and ensures a consistent behavior when a token has expired. The method keeps retry logic deliberately simple: at most one retry is attempted, and only when a refresh token exists, to prevent potential retry storms and loops. +By centralizing this token-refresh pattern for GETs, it reduces duplicated boilerplate at call sites while ensuring a consistent retry policy. If the refresh cannot be completed, the original 401 is returned, preserving standard authorization semantics. The method also disposes the intermediate response during a retry to avoid resource leaks. ## Notes -- Exceptions thrown by RefreshTokenAsync are swallowed; if the refresh process fails, the original 401 (or other status) is returned without propagating an error. -- The initial response is disposed when a retry occurs to avoid leaking resources; the caller must dispose the final response they receive. +- The final `HttpResponseMessage` returned by this method must be disposed by the caller after processing. +- A token refresh is attempted only when `_refreshToken` is non-empty and the initial response is `HttpStatusCode.Unauthorized`; otherwise, no refresh is performed. --- @@ -249,20 +222,14 @@ private async Task AuthenticatedRequestAsync(Func` -Performs an HTTP request with automatic token refresh on a 401 Unauthorized response. You supply a factory that creates the request; if the call yields 401 and a refresh token is present, it refreshes the token and retries once. The caller is responsible for disposing the returned HttpResponseMessage. +Executes a request produced by the provided `requestFactory` and, if the initial response is `HttpStatusCode.Unauthorized` and a refresh token is available, refreshes the token via `RefreshTokenAsync()` and retries the request once. The original response is disposed before the retry response is returned, and the caller is responsible for disposing the final `HttpResponseMessage`. ## Remarks -Encapsulates the common pattern of refreshing an access token to keep HTTP call sites concise and consistent. It enforces a single refresh-and-retry cycle to avoid repeated token refresh attempts and unintended multiple requests. If the refresh fails or there is no refresh token, the original response is returned, allowing callers to handle authentication failures uniformly. The returned HttpResponseMessage should always be disposed by the caller. - -## Example -```csharp -// Inside the class that defines AuthenticatedRequestAsync, assuming a configured httpClient exists -HttpResponseMessage response = await AuthenticatedRequestAsync(() => httpClient.GetAsync("https://api.example.com/protected")); -``` +Centralizes the token-refresh retry pattern for authenticated API calls. It hides token management behind a single helper so callers can issue requests without duplicating refresh logic, while ensuring a single, well-scoped retry path. Disposing the intermediate response avoids leaks, and the final response is returned for the caller to manage lifetime. ## Notes -- If a 401 is observed without a valid refresh token, the call returns the original 401 without attempting a refresh. -- The final HttpResponseMessage must be disposed by the caller; the method disposes the original response only when a retry occurs and a new response is obtained. +- If `RefreshTokenAsync()` fails (throws) or the refresh path throws, the exception is swallowed and the method returns the original 401 response, signaling to the caller that re-authentication is needed. +- Only one automatic retry is performed; a second 401 will be returned as-is. --- @@ -284,15 +251,10 @@ public async Task BanUserAsync(string username, string? reason = null) **Returns:** `Task` -BanUserAsync bans a user by their username via the server's moderation API. It ensures the caller is authenticated, then issues a POST to /api/moderation/ban/{Uri.EscapeDataString(username)} with a BanRequest payload that carries the optional reason. The operation completes when EnsureSuccessAsync confirms a successful HTTP response; otherwise, it throws. Use this helper when you want to ban a user through the API client instead of assembling HTTP calls yourself. +Bans a user identified by the provided `username` by performing an authenticated HTTP POST to the moderation endpoint, optionally including a `reason` via a [`BanRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md). The username is escaped with `Uri.EscapeDataString(username)` when constructing the URL, and the operation is validated by `EnsureSuccessAsync` after the HTTP call. ## Remarks -This method encapsulates the authentication check and the HTTP POST, providing a single, reusable surface for moderation actions. It hides the exact endpoint path and JSON payload, reducing duplication and centralizing error handling via EnsureSuccessAsync. - -## Notes -- Authentication is enforced by EnsureAuthenticated before performing the request; if the client is not authenticated, this will fail early. -- The username is URL-escaped with Uri.EscapeDataString to prevent route interpretation issues. -- BanRequest is constructed with the optional reason; passing null is allowed. +This method provides a focused client-side wrapper around the server's moderation API to ban a user. It ensures the caller is authenticated, encodes the target username for safe URL usage, and sends a [`BanRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md) containing the optional `reason`. By funneling the action through `AuthenticatedRequestAsync` and `EnsureSuccessAsync`, it enforces consistent authentication and error handling across moderation operations, keeping concerns separated from higher-level business logic. --- @@ -302,7 +264,7 @@ This method encapsulates the authentication check and the HTTP POST, providing a ```csharp public async Task CreateChannelAsync(string name, string? topic = null, bool isPublic = true, - string? [REDACTED:CONNECTION_STRING_PASSWORD] string? encryptionSalt = null, string? wrappedRoomKey = null) + string? password = null, string? encryptionSalt = null, string? wrappedRoomKey = null) ``` **Parameters:** @@ -312,21 +274,22 @@ public async Task CreateChannelAsync(string name, string? topic = n | `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` | **Returns:** `Task` -Creates a new channel on the server by sending a CreateChannelRequest after ensuring the caller is authenticated. Use this method when you need to programmatically create a channel by name with optional topic and visibility settings, optionally including encryption-related data; it handles request construction, HTTP communication, and JSON deserialization into a ChannelDto. +CreateChannelAsync creates a new chat channel on the server by sending an authenticated POST to `"/api/channels"` with a [`CreateChannelRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) constructed from the provided parameters, and returns a `ChannelDto?` from the response content when successful. Use this helper when you need to create a channel with a name and optional topic, visibility, and security settings without writing the HTTP boilerplate yourself. ## Remarks -This abstraction centralizes the channel-creation workflow in the client. It automatically enforces authentication, builds the payload (including optional topic, visibility, and encryption-related fields), issues the POST to the /api/channels endpoint, and deserializes the response into a ChannelDto for the caller. By encapsulating these concerns, callers avoid manual HTTP handling and keep channel creation consistent across the codebase. The method returns a ChannelDto representing the newly created channel, or null if the response body cannot be parsed into that shape. +This method centralizes the channel creation workflow in the client: authenticate, construct a [`CreateChannelRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the provided parameters, POST to `"/api/channels"`, ensure the response indicates success, and deserialize a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the response content. It keeps higher-level code free from repetitive HTTP boilerplate and ensures consistent error handling via `EnsureAuthenticated` and `EnsureSuccessAsync`. ## Notes -- Requires the client to be authenticated; an unauthenticated call will be rejected by EnsureAuthenticated/EnsureSuccessAsync. -- The JSON deserialization may yield null if the response body is empty or not compatible with ChannelDto. -- If encryptionSalt or wrappedRoomKey are provided, they influence the server-side setup for encrypted channel access; incorrect values may cause the server to reject the request. +- The method returns `ChannelDto?`; if the server returns no content or invalid JSON, the result may be `null`. Callers should check for `null` before use. +- Requires authentication; `EnsureAuthenticated()` enforces this, so unauthenticated callers will fail. +- The parameters `password`, `encryptionSalt`, and `wrappedRoomKey` relate to channel security; supply them only when creating secure channels and be mindful of security implications. --- @@ -348,15 +311,20 @@ public async Task CreateInviteAsync(int? maxUses = null, int? expire **Returns:** `Task` -Creates a new invite by sending a POST request to the server with an optional maximum usage limit and expiration window. The operation requires the user to be authenticated and returns the created invite data when the server responds successfully. If maxUses or expiresInHours are not supplied, the server applies its defaults. Use this method when you need to programmatically generate a shareable invite with controlled usage or expiry. +Creates a new invite by posting a [`CreateInviteRequest`](../../EchoHub.Core/DTOs/InviteDtos.cs.md) to `/api/invites` after ensuring authentication, and returns the deserialized `InviteDto?` from the response. Provide optional `maxUses` and `expiresInHours` to control usage limits and expiry. ## Remarks -This method serves as a focused, typed abstraction over the invites API. It hides HTTP details (endpoint path, request payload, and response parsing) and centralizes authentication and basic success handling, so callers don’t need to manage HTTP lifecycles or error translation. It relies on InviteDto and CreateInviteRequest to shape the contract and on the underlying HTTP client to perform the request. +This method hides the transport details of invite creation, centralizing authentication, request serialization, error handling, and JSON deserialization into a single, strongly-typed call. By returning an `InviteDto?`, it cleanly signals the possibility of no content while enforcing a consistent contract via [`InviteDto`](../../EchoHub.Core/DTOs/InviteDtos.cs.md) and [`CreateInviteRequest`](../../EchoHub.Core/DTOs/InviteDtos.cs.md). + +## Example +```csharp +var invite = await apiClient.CreateInviteAsync(maxUses: 5, expiresInHours: 24); +``` ## Notes -- Requires authentication; calling without a valid authenticated context will cause an exception via EnsureAuthenticated. -- maxUses and expiresInHours are nullable; omitting them defers to the server’s default behavior. -- Returns InviteDto?; callers should handle potential null if the response body is empty or deserialization yields no content. +- The return value may be `null` if the response body is empty or cannot be deserialized as an [`InviteDto`](../../EchoHub.Core/DTOs/InviteDtos.cs.md). +- The method calls `EnsureAuthenticated()`, so unauthenticated callers will observe an exception if authentication has not been established. +- Providing `null` for both parameters relies on server defaults; if you need explicit control, supply non-null values for `maxUses` and/or `expiresInHours`. --- @@ -377,20 +345,10 @@ public async Task DeleteChannelAsync(string channelName) **Returns:** `Task` -Deletes a channel on the server by its name. It first ensures the client is authenticated, then issues an authenticated HTTP DELETE to /api/channels/{encodedName}, where the channel name is URL-encoded to safely survive special characters. The response is validated with EnsureSuccessAsync, and the operation completes when the server confirms success. Use this method when you need to remove a channel from the backend by name, rather than performing a raw HTTP request. +Deletes the channel named `channelName` by issuing an authenticated HTTP DELETE to `/api/channels/{Uri.EscapeDataString(channelName)}`. It first calls `EnsureAuthenticated` to guarantee the caller is authorized, then performs the request via `AuthenticatedRequestAsync` using `_http.DeleteAsync(...)`, and finally awaits `EnsureSuccessAsync` to validate the response. ## Remarks -This method encapsulates the REST pattern for removing a resource and centralizes authentication and error handling. By encoding the channel name and validating the response, it provides a reliable, reusable operation for channel management across the client. It relies on EnsureAuthenticated, AuthenticatedRequestAsync, and EnsureSuccessAsync, aligning with the client's approach to API calls. - -## Example -```csharp -// Delete a channel named "general" -await apiClient.DeleteChannelAsync("general"); -``` - -## Notes -- The channel name is URL-encoded in the request path using Uri.EscapeDataString to handle spaces and special characters. -- Non-success HTTP responses will throw exceptions via EnsureSuccessAsync; consider wrapping in try/catch if you need to surface user-friendly errors. +Using `EnsureAuthenticated` and `AuthenticatedRequestAsync` centralizes authentication flow and uniform error handling for API calls in the client. Escaping the channel name with `Uri.EscapeDataString` prevents malformed URLs and potential issues when channel names contain special characters. --- @@ -411,14 +369,10 @@ public async Task DeleteMessageAsync(Guid messageId) **Returns:** `Task` -Deletes a moderation message identified by messageId by issuing an authenticated HTTP DELETE request, and then ensures the response indicates success. Use this method when you need to remove a specific moderation message through the client without dealing with authentication or direct HTTP handling. +Deletes a moderation message identified by `messageId` by issuing an HTTP DELETE to `/api/moderation/messages/{messageId}` after ensuring the client is authenticated. It uses `AuthenticatedRequestAsync` to execute the request against the shared `_http` client and then validates the server response with `EnsureSuccessAsync` before returning. ## Remarks -This method centralizes the common pattern of authenticated server mutations: vetting authentication, performing the HTTP call through a shared AuthenticatedRequestAsync wrapper, and validating success with EnsureSuccessAsync. It hides the details of the endpoint path and response handling from callers, offering a clean, exception-driven contract where failures surface as exceptions. The operation does not return a payload; callers rely on the absence of an exception to determine success. - -## Notes -- Non-success HTTP responses (such as 404, 403, or 5xx) are surfaced as exceptions via EnsureSuccessAsync. -- The call depends on a valid authenticated context; if authentication is not established, EnsureAuthenticated will throw before the HTTP request is issued. +Like other mutating API calls, `DeleteMessageAsync` enforces authentication via `EnsureAuthenticated` and delegates the HTTP invocation to `AuthenticatedRequestAsync`. This pattern centralizes authentication, request execution, and response validation around a shared `_http` client, ensuring consistent behavior for moderation mutations. --- @@ -439,21 +393,19 @@ public async Task DeleteMyAccountAsync(string password) **Returns:** `Task` -Deletes the currently authenticated user's account by issuing an HTTP DELETE to /api/users/me with a JSON payload containing the provided password to confirm intent. The method first verifies the caller is authenticated, performs the request, and then asserts a successful server response. Use this when a user explicitly wants to permanently remove their own account; the password requirement helps prevent accidental deletions. +Deletes the currently authenticated user's account after re-confirming intent with the provided password. The method ensures the caller is authenticated (`EnsureAuthenticated`), issues an HTTP `DELETE` to `"/api/users/me"` with a [`DeleteAccountRequest`](../../EchoHub.Core/DTOs/AccountDtos.cs.md) payload containing the `password` (constructed via `JsonContent.Create`), and validates the response with `EnsureSuccessAsync`. ## Remarks -Encapsulates the account-deletion flow within the client to centralize authentication validation, request construction, and server response handling. The operation relies on a DeleteAccountRequest DTO to carry the password payload, aligning client and server contracts and keeping sensitive data isolated in the request body. The surrounding EnsureAuthenticated and EnsureSuccessAsync calls provide clear preconditions and postconditions for callers and tests, reducing boilerplate at call sites. +Encapsulates the account-deletion flow behind a client API, centralizing authentication checks, request construction, and error handling for a destructive user action. Provides a single, consistent path to delete the user's account, preventing boilerplate duplication across the UI layer and aligning with other API client methods. ## Example ```csharp -// Example usage: -await client.DeleteMyAccountAsync("P@ssw0rd!"); +await apiClient.DeleteMyAccountAsync("Pa$$w0rd"); ``` ## Notes -- This is a destructive, irreversible operation; confirm user intent and consider UX safeguards before invoking. -- The method does not return a value; failures surface as exceptions via EnsureSuccessAsync. Wrap calls in appropriate error handling if needed. -- The password is transmitted as part of the request payload; avoid logging the password and ensure transport security is in place. +- Destructive action: irreversible; ensure the user truly intends to delete their account before invoking this. +- Password is transmitted in the request body as part of [`DeleteAccountRequest`](../../EchoHub.Core/DTOs/AccountDtos.cs.md); ensure transport security and proper credential hygiene. --- @@ -468,25 +420,15 @@ public void Dispose() **Returns:** `void` -Disposes the internal HTTP resource held by this ApiClient by delegating to _http.Dispose(). Call Dispose when you’re finished using the ApiClient to ensure network resources are released promptly, rather than waiting for finalization. +Disposes the ApiClient's internal HTTP resource by delegating to `_http.Dispose()`. This ensures that the underlying HTTP client and its resources are released when the ApiClient is disposed. ## Remarks -This is a straightforward implementation of the IDisposable pattern: the outer wrapper delegates cleanup to its disposable member. By disposing the inner _http resource, the ApiClient ensures that associated network resources (such as open connections) are released in a deterministic manner as soon as the caller is done with the instance. - -## Example - -```csharp -using (var client = new ApiClient()) -{ - // use client -} -``` +This is a straightforward delegation in the IDisposable pattern. It relies on `_http` implementing `IDisposable` and being non-null; ensure `_http` is initialized before disposing to avoid potential `NullReferenceException`. ## Notes -- If the internal _http resource is shared with other components, disposing the ApiClient may affect those components; ensure ownership semantics are clear. -- After Dispose is called, subsequent use of the ApiClient (or its _http) may throw ObjectDisposedException unless the class guards against use after disposal. +- Potential `NullReferenceException` if `_http` is null; consider guarding or ensuring initialization guarantees. --- @@ -508,16 +450,19 @@ public async Task DownloadFileToTempAsync(string relativeUrl, string fil **Returns:** `Task` -Ensures authentication, retrieves content from the given relative URL via an authenticated GET, and validates the HTTP response. It then creates a temporary EchoHub directory under the system temp path, streams the response body to a uniquely named file there, and returns the file path. +Downloads a resource from `relativeUrl` using an authenticated GET, streams the response to a uniquely named temporary file under the EchoHub temp directory, and returns the created file path as a `string`. It authenticates via `EnsureAuthenticated()`, fetches the payload with `AuthenticatedGetAsync(relativeUrl)`, and writes the response body to disk without buffering the entire content. ## Remarks -Centralizes the common pattern of authenticated download and temporary-file storage, reducing duplication across callers. The method uses a GUID-based filename to avoid collisions and writes the stream directly to disk, minimizing memory usage. It returns the path to the temporary file but does not perform cleanup; callers should delete the file when it is no longer needed to avoid littering the temp directory. +By centralizing authentication, streaming, and temporary-file management, this abstraction reduces boilerplate at call sites and helps callers avoid loading large payloads into memory. The implementation uses streaming (`ReadAsStreamAsync` followed by `CopyToAsync`) to minimize memory usage and guarantees a unique temporary file name with a `Guid`. + +## Example +```csharp +// Example usage assumes an instance named `client` of `ApiClient` +string tempPath = await client.DownloadFileToTempAsync("reports/annual.pdf", "annual.pdf"); +``` ## Notes -- Creates the EchoHub subdirectory in the system temporary path if it does not exist. -- Uses a GUID (with no dashes) as part of the filename to guarantee uniqueness. -- The return value is a path to the downloaded temporary file; callers are responsible for cleanup. -- No cancellation token or progress reporting is exposed by this API. +- The method returns a path to a temporary file located under the EchoHub temp directory; there is no automatic cleanup, so callers should delete the file when it is no longer needed. --- @@ -532,13 +477,19 @@ private void EnsureAuthenticated() **Returns:** `void` -This private guard validates that the ApiClient is in a state suitable for making authenticated requests by ensuring the internal _accessToken is present. When the token is missing or empty, it signals misuse of the client by throwing InvalidOperationException with guidance to call LoginAsync or RegisterAsync before attempting any API calls. +Ensures that the client is authenticated by validating that the private field `_accessToken` is not null or empty; if it is missing, it throws an `InvalidOperationException` with guidance to call `LoginAsync` or `RegisterAsync` first. This guard is typically invoked at the start of API calls to fail fast when authentication has not yet been established. ## Remarks -This centralizes the authentication precondition in ApiClient, so all protected operations rely on a single, consistent guard. It communicates a clear contract: you must authenticate prior to using the client. By encapsulating the check, code duplication is reduced and the error message remains uniform across methods that require authentication. +By centralizing the authentication precondition, this guard prevents accidental unauthorized requests and provides a consistent error when the client isn't authenticated. It keeps authentication logic in one place, making future enhancements (such as token validation or refresh handling) easier to implement without duplicating checks across multiple call sites. The private scope communicates that this is an internal invariant of the API client, not part of its public surface. ## Notes -- The method only checks for a non-empty _accessToken; it does not validate token expiry or current validity. Expired or invalid tokens may still cause failures at the API boundary, which should be handled by higher-level logic if present. +- The check only verifies presence of `_accessToken`; it does not validate expiry, issuer, or revocation. A non-empty token can still be invalid at runtime, causing a request to fail after this guard passes. +``` +// Token could be expired or revoked even though `_accessToken` is non-empty. +// EnsureAuthenticated() won't catch this; a later API call will fail. +``` +- Because the method is private, external code cannot invoke it directly, so callers rely on the class's internal usage pattern to uphold the authentication precondition. If a future path bypasses this guard, authentication requirements might be violated. + --- @@ -559,15 +510,15 @@ private static async Task EnsureSuccessAsync(HttpResponseMessage response) **Returns:** `Task` -Converts non-success HTTP responses into a single HttpRequestException with a best-effort error message. If the response is unsuccessful, it builds a message from the status code and reason phrase, then optionally enriches it by extracting a top-level 'error' or 'Error' property from a JSON body; if those properties are absent or parsing fails, it falls back to the raw body or the status message, and finally throws HttpRequestException with that message. +Ensures that a non-success `HttpResponseMessage` is surfaced as a descriptive `HttpRequestException` by inspecting the response and extracting a meaningful error message. For successful responses it returns immediately; for failures it builds a message from the status code and reason phrase, then tries to read and parse a JSON body to use an `error`/`Error` property when available, otherwise falling back to the raw body. ## Remarks -Centralizes HTTP error handling in the client. It encapsulates the logic for translating HTTP failure responses into exceptions, so callers can catch HttpRequestException and rely on a consistent message shape. It uses JsonDocument to inspect the body for 'error'/'Error' fields and gracefully degrades when the body is not JSON or lacks those fields. +Centralizes HTTP error reporting in the client. It uses the `Content` payload and attempts to parse JSON with `JsonDocument` to surface server-provided error details, improving diagnosability across the API surface. If the body cannot be parsed or no error field is present, the message remains based on the status code and reason phrase. ## Notes -- Parsing failures during body extraction are swallowed; the catch block is intentionally empty, so a non-JSON body or parsing error won't crash the flow but may limit message richness. -- The fallback to using the raw body in the error message can reveal server details; avoid logging or exposing this in user-facing errors. -- The method is private and intended for internal use; external callers cannot invoke it directly. +- The body reading is wrapped in a try/catch that suppresses parsing errors; if parsing fails, the message is based on the initial status code and reason phrase. +- It only recognizes top-level `error` or `Error` properties; nested fields are ignored. +- The method is `private` and `static`, serving as an internal helper within the containing class and not part of the public API. --- @@ -582,14 +533,14 @@ public async Task ExportMyDataAsync() **Returns:** `Task` -Downloads the authenticated user's complete data export as raw JSON text. The method authenticates the caller, performs an authenticated GET to /api/users/me/export, ensures the response indicates success, and returns the response body as a string for downstream processing or persistence. It is suited for data portability or backup scenarios where the consumer will parse or store the JSON themselves. +Downloads the authenticated caller's full data export as raw JSON text by first ensuring the caller is authenticated, performing an authenticated GET against `/api/users/me/export`, validating the response with `EnsureSuccessAsync`, and returning the response body as a `string`. This `ExportMyDataAsync` method serves as a concise, high-level entry point when you need a portable copy of the current user's data without writing repetitive HTTP boilerplate. ## Remarks -This symbol provides a focused convenience around the ApiClient by hiding endpoint details and error handling behind EnsureAuthenticated and EnsureSuccessAsync. By returning raw JSON instead of a deserialized object, it offers maximum flexibility for downstream processing, partial deserialization, or deferred parsing in response to evolving export schemas. +By encapsulating authentication, request dispatch, and success verification, `ExportMyDataAsync` provides a single, consistent path for obtaining a user's data export. It hides HTTP plumbing from callers and pairs with downstream deserialization to produce typed representations if needed. It relies on the client authentication flow (`EnsureAuthenticated`) and the standard success check (`EnsureSuccessAsync`). ## Notes -- The payload is returned as raw JSON text; there is no deserialization here, so callers should parse it if they need structured data. -- The entire payload is read into memory via ReadAsStringAsync; for very large exports this can incur noticeable memory usage. Consider streaming approaches or server-side handling if the export size is a concern. +- ``ExportMyDataAsync`` returns a raw JSON `string`; callers should deserialize it into typed objects as needed. +- The payload can be large; callers should be mindful of memory usage when exporting very large user datasets. --- @@ -610,17 +561,15 @@ public async Task GetChannelCryptoAsync(string channelName) **Returns:** `Task` -Retrieves the public cryptographic metadata for a channel, indicating whether the channel is end-to-end encrypted and the key-derivation salt. If the channel does not exist, the method returns null. It requires an authenticated context, issues an HTTP GET to /api/channels/{channelName}/crypto with channelName URI-escaped, and deserializes the JSON body into a ChannelCryptoDto. +Gets the crypto metadata for a specific channel, including whether end-to-end encryption is enabled and the key-derivation salt, returning a [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) when the channel exists. If the channel doesn't exist, it returns null; the call requires authentication and performs an authenticated GET to `/api/channels/{channelName}/crypto` with `channelName` escaped via `Uri.EscapeDataString`. ## Remarks - -Encapsulates the remote API contract for channel crypto settings, providing a single, typed entry point for clients. It centralizes authentication enforcement and error handling so callers do not need to manage low-level HTTP concerns. The NotFound (404) path is represented by a null return value, while other HTTP errors are surfaced as exceptions by EnsureSuccessAsync. +By wrapping this HTTP call in a single client method, the library provides a consistent, strongly-typed view of a channel's crypto settings and hides HTTP details from callers. It also centralizes the NotFound -> null convention, so callers can distinguish a missing channel from other failures without adding boilerplate. ## Notes - -- 404 Not Found is mapped to null; non-success statuses throw. -- Channel name is escaped with Uri.EscapeDataString to ensure a safe, well-formed URL. -- JSON deserialization relies on the ChannelCryptoDto type; changes to the API shape may require updating the DTO. +- When the channel is missing, the method yields null; other HTTP errors throw via `EnsureSuccessAsync`, so callers should be prepared to handle exceptions for non-NotFound failures. +- The channel name is escaped with `Uri.EscapeDataString`, preventing path issues with special characters. +- JSON deserialization relies on the shape of [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md); mismatches or deserialization errors can surface as exceptions during `ReadFromJsonAsync`. --- @@ -641,7 +590,7 @@ public async Task GetChannelMetaAsync(string channelName) **Returns:** `Task` -Fetches a channel's human-facing metadata (message count, unique posters, estimated size, created date, room id) for the /meta command. Returns null if it doesn't exist. The method authenticates the client, issues an HTTP GET to /api/channels/{channelName}/meta (with the channel name URI-escaped), returns null on a 404 Not Found, validates the response, and deserializes the JSON body into a ChannelMetaDto. The operation is asynchronous and relies on the client being authenticated prior to the call. +Fetches a channel's public, human-facing metadata used by the `/meta` command. It returns a [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) containing the message count, number of unique posters, an estimated size, the channel's creation date, and the room id when the metadata exists; if no metadata exists for the channel, it returns `null`. The call first authenticates via `EnsureAuthenticated()`, URL-encodes the channel name using `Uri.EscapeDataString`, performs an HTTP GET to `/api/channels/{channelName}/meta` with `AuthenticatedGetAsync`, returns `null` on a `NotFound` response, enforces success via `EnsureSuccessAsync`, and deserializes the response body to [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) with `ReadFromJsonAsync()`. --- @@ -656,23 +605,19 @@ public async Task> GetChannelsAsync() **Returns:** `Task>` -Fetches the channels accessible to the currently authenticated user by calling the API endpoint `/api/channels`. It handles authentication, dispatches the HTTP GET, validates the response, and deserializes the JSON payload into a paginated wrapper, finally returning the list of [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) items (or an empty list if no data is available). This method provides a simple, strongly-typed surface for consumers who need to enumerate or process channels without dealing with HTTP details or pagination concerns. +Gets the channels for the currently authenticated user by issuing an authenticated GET to `/api/channels` (via `AuthenticatedGetAsync("/api/channels")`), validating the response with `EnsureSuccessAsync`, and deserializing the payload into a [`PaginatedResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) before returning the collection `paginated?.Items ?? []`. ## Remarks -This symbol serves as a focused data fetch for the authenticated user's channels, encapsulating transport, auth, and error handling behind a clean API. It relies on `EnsureAuthenticated()` to confirm the user identity, `EnsureSuccessAsync(...)` to surface HTTP errors, and `ReadFromJsonAsync>()` to transform the payload. By returning `paginated?.Items ?? []`, callers always receive a non-null collection, even when the server returns no data. +By encapsulating the HTTP call, authentication, and pagination unwrap, this method provides a single, testable surface for retrieving channels and ensures callers always receive a non-null list. It relies on the server to provide a [`PaginatedResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) payload; if the server returns no items the method yields an empty list rather than null, smoothing downstream collection handling. ## Example ```csharp -var channels = await apiClient.GetChannelsAsync(); -foreach (var channel in channels) -{ - // Process each channel as needed -} +List channels = await client.GetChannelsAsync(); ``` ## Notes -- Returns an empty list when the API returns null or no items, never null. -- The method relies on authentication and HTTP status checks; callers should be prepared to handle exceptions from authentication failures or HTTP errors. +- Returns an empty list when there are no items in the response instead of null. +- Exceptions from authentication or HTTP failure propagate (e.g., via `EnsureAuthenticated` / `EnsureSuccessAsync`). --- @@ -693,16 +638,13 @@ private static string GetContentType(string fileName) **Returns:** `string` -GetContentType derives a MIME type from a filename by inspecting its extension. It centralizes a small, static mapping of common extensions to standard MIME types and falls back to application/octet-stream when the extension is unrecognized. +Determines the MIME content type for a file name by inspecting its extension. The private static method `GetContentType` extracts the extension with `Path.GetExtension(fileName)` and normalizes it to lower case using `ToLowerInvariant()`, then maps known extensions to their standard MIME types. It covers common image formats (`.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.gif` → `image/gif`, `.webp` → `image/webp`), text and document types (`.txt` → `text/plain`, `.pdf` → `application/pdf`), and falls back to `application/octet-stream` for unknown extensions. ## Remarks - -By using Path.GetExtension(fileName) and ToLowerInvariant(), the method performs case-insensitive matching and keeps the logic in a single place within ApiClient.cs, ensuring consistent Content-Type values across the client. Because it is private static, this helper is an internal concern of the API client and is not exposed as part of the public API; callers should rely on higher-level abstractions for content-type handling. +This function centralizes the mapping from file extensions to MIME types so all call sites share the same logic. The switch expression keeps the mapping compact and extensible, with a safe default of `application/octet-stream` for unknown extensions. It is intended for internal use by the HTTP client to populate Content-Type headers from filenames rather than duplicating mime-type logic across callers. ## Notes - -- This method considers only the file extension and does not inspect the file contents; for security-sensitive scenarios, use content-based detection as needed. -- Unrecognized or missing extensions result in application/octet-stream, which is a safe default but may not reflect the actual content type. +- Null input is not guarded; a null `fileName` will cause a `NullReferenceException` when calling `ToLowerInvariant()` on the extracted extension. Ensure the argument is non-null before invoking this method, or wrap the call with a null-check. --- @@ -717,14 +659,7 @@ public async Task GetEncryptionKeyAsync() **Returns:** `Task` -Gets the server-provided encryption key by performing an authenticated HTTP GET to /api/server/encryption-key. It first ensures an authenticated context, executes the request, verifies a successful response, deserializes the JSON payload into EncryptionKeyResponse, and returns the Key value. If the server returns no content, it throws an InvalidOperationException with the message "Server returned empty encryption key response." The caller receives the encryption key as a plain string for use in client-side encryption/decryption workflows. - -## Remarks -This method centralizes the retrieval of the server-supplied encryption key, encapsulating the endpoint path, authentication, and JSON deserialization behind a simple string return. It provides a stable surface for higher layers, hiding the details of the HTTP contract and error handling while ensuring a consistent failure path when the server cannot provide a key. Because keys may rotate or change over time, callers should decide on caching strategies appropriate to their security and consistency requirements. - -## Notes -- The call may throw if authentication fails, the HTTP response indicates failure, or the response body is empty (as explicitly guarded by the InvalidOperationException). -- The returned value is sensitive; avoid logging or persisting the key beyond its immediate use. +GetEncryptionKeyAsync fetches the server's encryption key in an authenticated context by issuing a GET to `/api/server/encryption-key`, validating the response with `EnsureSuccessAsync`, deserializing the payload to [`EncryptionKeyResponse`](../../EchoHub.Core/DTOs/ServerDtos.cs.md) via `ReadFromJsonAsync()`, and returning `result.Key`. If the server returns an empty encryption key payload, it throws `InvalidOperationException("Server returned empty encryption key response.")`. --- @@ -739,23 +674,14 @@ public async Task> GetInvitesAsync() **Returns:** `Task>` -GetInvitesAsync retrieves the invites for the currently authenticated user by issuing an authenticated HTTP GET to /api/invites. It ensures the caller is authenticated, verifies the HTTP response indicates success, and deserializes the JSON payload into a `List`. If the response body is null, an empty collection is returned, allowing callers to handle zero invites without additional null checks. +GetInvitesAsync retrieves the current user's invites by performing an authenticated GET request to `/api/invites` and deserializing the JSON payload into a `List`. It enforces authentication up-front and ensures a successful HTTP response before returning the deserialized collection (or an empty list if the payload is null). ## Remarks - -By encapsulating authentication, request dispatch, and JSON deserialization, this method hides boilerplate and enforces a consistent error-handling and data-contract approach for invite retrieval. It relies on the InviteDto data contract and the HTTP response content model to produce a strongly-typed result, reducing coupling between higher-level client code and the underlying HTTP details. - -## Example - -```csharp -var invites = await client.GetInvitesAsync(); -var total = invites.Count; -``` +GetInvitesAsync encapsulates the standard flow for retrieving a protected resource: ensure authentication, issue a GET to `/api/invites`, and deserialize the response into a `List`. It yields a non-null list to callers (empty when the server returns no content) and keeps HTTP/JSON concerns hidden behind a stable API. ## Notes - -- Requires a valid authentication context; calling without authentication will cause EnsureAuthenticated to throw. -- Deserialization depends on the InviteDto contract; changes to server payload or InviteDto shape may require corresponding updates to this method and its callers. +- A null payload is coerced into an empty list to avoid nulls for callers. +- No paging is handled; this reads a single page of results from `/api/invites` and will not automatically fetch subsequent pages. --- @@ -770,15 +696,14 @@ public async Task GetServerInfoAsync() **Returns:** `Task` -Retrieves the server's current status by making an asynchronous HTTP GET request to /api/server/info and deserializing the JSON response into a ServerStatusDto. It acts as a focused wrapper around a single API endpoint, allowing consumers to obtain server health and status information without performing HTTP requests or JSON parsing themselves. +Fetches the current server status by issuing an asynchronous HTTP GET to `"/api/server/info"` and deserializing the response into a [`ServerStatusDto`](../../EchoHub.Core/DTOs/ServerDtos.cs.md) via the underlying HTTP client. It returns the resulting `ServerStatusDto?`, or `null` if the payload is absent, providing a typed, convenient entry point for clients that need server health information. ## Remarks -By delegating to the HttpClient-based call, this method encapsulates transport concerns and JSON deserialization, keeping UI components focused on rendering. It relies on an HttpClient instance provided to ApiClient (typically via dependency injection), which facilitates testing through mocks or stubs. If the API contract changes (e.g., a different endpoint or DTO), updating this single method reduces the spread of changes throughout the client. +This method acts as a typed façade over the raw HTTP call, binding the `/api/server/info` endpoint to the [`ServerStatusDto`](../../EchoHub.Core/DTOs/ServerDtos.cs.md) contract. By encapsulating this endpoint behind `GetServerInfoAsync`, callers gain a stable API surface that is easier to mock in tests and evolve without changing consuming code. ## Notes -- This method has no CancellationToken parameter; callers that need cancellation would need to adapt the signature or apply cancellation at a higher level. -- Non-success HTTP responses or JSON deserialization errors will surface as exceptions; callers should handle HttpRequestException or JsonException as appropriate. -- The return type is ServerStatusDto?, indicating callers should check for null before use, though the underlying HTTP call can still throw before a value is produced in some error cases. +- Non-success HTTP status codes will throw (e.g., `HttpRequestException`), so callers should handle exceptions as part of their error handling strategy. +- The JSON payload must conform to the shape of [`ServerStatusDto`](../../EchoHub.Core/DTOs/ServerDtos.cs.md); any schema drift can lead to deserialization failures. --- @@ -799,28 +724,19 @@ public async Task GetUserProfileAsync(string username) **Returns:** `Task` -Gets the profile for a given username from the API. It requires the client to be authenticated, builds a GET request to /api/users/{username}/profile with the username safely escaped for the URL, validates that the response indicates success, and then deserializes the response JSON into a UserProfileDto. The return value is a UserProfileDto? representing the retrieved profile, or null if the payload is absent. +GetUserProfileAsync fetches the profile for a given `username` by asserting authentication via `EnsureAuthenticated()`, issuing an authenticated GET with `AuthenticatedGetAsync($"/api/users/{Uri.EscapeDataString(username)}/profile")`, and deserializing the response into a `UserProfileDto?` via `ReadFromJsonAsync()`. It should be used whenever you need a typed representation of a user's profile rather than crafting HTTP calls yourself; it centralizes URL construction, auth, and JSON deserialization in one place. ## Remarks -By encapsulating the HTTP call and JSON deserialization, this method provides a typed, reusable access point for user-profile data. It aligns with the ApiClient's pattern of performing authenticated requests and interpreting JSON responses, reducing boilerplate for callers. +This symbol encapsulates the profile-fetching pattern within the `ApiClient`, ensuring consistent error handling and response processing. It relies on `Uri` for safe username escaping, `AuthenticatedGetAsync` for the authenticated request, and `ReadFromJsonAsync` to produce a strongly-typed [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) payload, keeping UI code focused on presentation rather than transport concerns. ## Example ```csharp -var profile = await client.GetUserProfileAsync("alice"); -if (profile is null) -{ - // handle missing profile -} -else -{ - // use profile -} +var profile = await apiClient.GetUserProfileAsync("alice"); ``` ## Notes -- This method requires authentication; EnsureAuthenticated is called at the start and will raise if the client is not authenticated. -- The username is URL-escaped using Uri.EscapeDataString to ensure a safe path segment. -- The return type is nullable (UserProfileDto?), so callers should handle the null case appropriately. +- The return type is `UserProfileDto?`, so callers must handle the possibility of a `null` result if the response body is empty or JSON null. +- The method escapes the provided `username` and targets the `/api/users/{escapedUsername}/profile` endpoint, so callers should not attempt to bypass the abstraction for manual URL manipulation. --- @@ -835,15 +751,17 @@ public async Task GetValidTokenAsync() **Returns:** `Task` -Gets a valid access token asynchronously. If there is no current token, it returns null. If the token is within 60 seconds of expiry and a refresh token is available, it attempts to refresh via RefreshTokenAsync; any exception from RefreshTokenAsync is swallowed to allow the caller to handle authentication failure, and the current token is returned anyway. This method is used by EchoHubConnection as the token provider for SignalR, centralizing token management so callers obtain a usable token without duplicating refresh logic themselves. +Returns a valid access token, refreshing it when it is near expiry. This helper is used by the [`EchoHubConnection`](EchoHubConnection.cs.md)-provided SignalR token provider to obtain a token without requiring callers to manage refresh logic themselves. ## Remarks -This method centralizes the token lifecycle for EchoHubConnection's SignalR authentication. It reduces duplication by providing a single, reusable provider that handles refresh eligibility and fail-safe fallbacks. The refresh is best-effort; if RefreshTokenAsync fails or no refresh token is present, the method returns the current token, leaving the caller to respond to an authentication failure. + +To minimize unnecessary refresh calls, the method returns the current token if it is still valid; it only triggers a refresh when the token expires within 60 seconds and a `_refreshToken` is available. If `RefreshTokenAsync` fails, the exception is swallowed and the current `_accessToken` is returned, leaving authentication failure handling to the caller. The check uses `DateTimeOffset.UtcNow` for a timezone-agnostic calculation of freshness. Note that there is no synchronization around refresh operations, so concurrent calls may trigger multiple refresh attempts. ## Notes -- Returns null when there is no access token, signaling the caller to re-authenticate. -- Refresh attempts occur only when the token is near expiry (within 60 seconds) and a refresh token is available. -- There is no explicit synchronization; concurrent calls may trigger multiple refresh attempts if used from multiple threads. + +- The method returns `null` when `_accessToken` is null or empty, signaling that no token is currently available. +- If `_expiresAt` indicates imminent expiry but `_refreshToken` is missing, a potentially expired or invalid token may be returned. +- Refresh failures are swallowed; downstream logic should verify the resulting token and act on authentication failures accordingly. --- @@ -865,15 +783,16 @@ public async Task KickUserAsync(string username, string? reason = null) **Returns:** `Task` -KickUserAsync kicks a user by username through the moderation API. It ensures the client is authenticated, posts a KickRequest with the optional reason to the server, and validates the response, throwing on failure. +`KickUserAsync` asynchronously kicks a user by `username` through the moderation API, optionally including a `reason`. It ensures the client is authenticated with `EnsureAuthenticated()`, posts a [`KickRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md) to `/api/moderation/kick/{Uri.EscapeDataString(username)}` via `AuthenticatedRequestAsync`, and validates the result with `EnsureSuccessAsync`. ## Remarks -This abstraction encapsulates the moderation HTTP call behind a typed DTO, keeping authentication and success handling centralized and out of the caller's business logic. It uses Uri.EscapeDataString to safely embed the username in the request path and relies on KickRequest to carry the optional reason payload, promoting a clean separation between transport concerns and domain logic. + +Centralizes moderation actions on the client side by wrapping the HTTP call, ensuring authentication, and standardizing error handling for moderation endpoints. It composes with the shared `_http` client and uses `Uri.EscapeDataString` to safely embed the `username` in the request path. ## Notes -- Requires an authenticated session; the method enforces this by calling EnsureAuthenticated() before issuing the request. -- Reason is optional; passing null results in a request with no explicit reason (as defined by KickRequest). -- Username in the URL is URL-encoded via Uri.EscapeDataString to form a safe request path. + +- The `username` is URL-escaped via `Uri.EscapeDataString` to handle special characters. +- If the API returns an error, `EnsureSuccessAsync` will throw an exception; callers should handle it accordingly. --- @@ -895,17 +814,15 @@ public async Task LoginAsync(string username, string password) **Returns:** `Task` -LoginAsync authenticates a user by posting the supplied credentials to the API login endpoint, deserializes the LoginResponse, and stores the resulting tokens for subsequent requests. It hides HTTP transport and error handling behind a single asynchronous surface; use it when a user signs in to obtain and apply authentication tokens. +`LoginAsync` encapsulates the client login flow: it constructs a [`LoginRequest`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) from the supplied `username` and `password`, posts it to the authentication endpoint via `_http.PostAsJsonAsync`, calls `EnsureSuccessAsync` to enforce a successful status, reads a [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) from `response.Content` with `ReadFromJsonAsync()`, invokes `SetTokens` to persist tokens, and returns the [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md). This abstracts away HTTP wiring, error handling, and token management so callers only need to provide credentials to obtain tokens. ## Remarks - -LoginAsync centralizes the authentication workflow within ApiClient, encapsulating the HTTP POST, response validation, and token application behind a single method. It guarantees token updates via SetTokens after a successful login so subsequent requests are authenticated, and any HTTP errors or a missing response body surface as exceptions. The response is disposed promptly through the using var pattern, ensuring resources are freed even in error cases. +LoginAsync centralizes the login protocol for the client: it wires HTTP request/response handling, error checking, and token persistence in a single method. It relies on `EnsureSuccessAsync` to throw for non-success status codes, and it guards against an empty response by throwing `InvalidOperationException` if `Content.ReadFromJsonAsync()` returns null; callers can rely on the returned [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) to carry tokens after `SetTokens` has run. ## Notes +- It throws `InvalidOperationException` if the login response body is empty. +- On success, it calls `SetTokens` to persist authentication tokens for subsequent requests. -- Throws InvalidOperationException when the login response body is empty ("Login returned empty response."). -- Non-success HTTP statuses trigger exceptions via EnsureSuccessAsync. -- Relies on the server returning a non-null LoginResponse to feed SetTokens; if the payload does not provide tokens, subsequent authenticated calls may be ineffective. --- @@ -926,15 +843,14 @@ public async Task LoginWithRefreshTokenAsync(string refreshToken) **Returns:** `Task` -Exchanges a refresh token for a new LoginResponse by posting a RefreshRequest to /api/auth/refresh, validates the HTTP response, and updates the client's stored tokens. Call this method when you need to refresh the access token without prompting the user to sign in again. +LoginWithRefreshTokenAsync encapsulates the client-side token-refresh flow: it posts a [`RefreshRequest`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) containing the provided `refreshToken` to `/api/auth/refresh`, ensures a successful HTTP response via `EnsureSuccessAsync`, deserializes a [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) from the response content, updates the stored tokens with `SetTokens`, and returns the resulting [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md). Use this method when you have a valid refresh token and want to obtain new tokens in a single, consistent operation instead of duplicating the HTTP call and token persistence logic. ## Remarks -Centralizes the token-refresh behavior within the API client: it encapsulates the HTTP call, JSON deserialization, and token state update. It relies on EnsureSuccessAsync to surface HTTP errors and on SetTokens to persist the new tokens, guaranteeing that subsequent requests use the refreshed credentials. +By encapsulating the refresh flow behind the client boundary, this method ensures a single, consistent approach to exchanging a `refreshToken` for new tokens, with uniform error handling and token persistence via `SetTokens`. It coordinates the HTTP request, success validation, payload deserialization, and token state management, so callers don't have to duplicate boilerplate or risk divergent token-update semantics. ## Notes -- Throws InvalidOperationException if the refresh response payload is empty (`ReadFromJsonAsync`() returns null). -- Non-success HTTP responses are surfaced via EnsureSuccessAsync; callers should handle exceptions that indicate refresh failure. -- This method mutates the client's token state via SetTokens(result); a successful return means the tokens were refreshed and are ready for use in subsequent requests. +- Deserialization errors will bubble up if the response payload cannot be parsed as [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md). +- The method throws `InvalidOperationException` when the response body is empty to signal that a valid [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) was not received. --- @@ -949,14 +865,14 @@ public async Task LogoutAsync() **Returns:** `Task` -LogoutAsync ends the current session by (optionally) notifying the server to invalidate the refresh token and then clearing local authentication state. Call this when the user signs out to ensure both server-side revocation (when possible) and client-side cleanup. +LogoutAsync asynchronously logs out the current user. If a `_refreshToken` exists, it attempts to invalidate the server-side session by posting a `RefreshRequest(_refreshToken)` to `/api/auth/logout` via `_http.PostAsJsonAsync`, swallowing any exceptions to provide a best-effort logout. It then clears the client state by setting `_accessToken` and `_refreshToken` to `null` and removing the `Authorization` header from `_http.DefaultRequestHeaders`. ## Remarks -This method centralizes the sign-out flow in the client, coordinating server-side invalidation with a robust client-side cleanup. It uses a best-effort approach: if the server logout cannot be performed (e.g., network issues), the local sign-out still completes to prevent stale credentials from being used. Clearing the Authorization header in the HTTP client helps guarantee that subsequent requests are unauthenticated, even if other parts of the application held onto tokens in memory. +LogoutAsync centralizes sign-out behavior: it tries to invalidate the server-side session when a `_refreshToken` is present, then always clears local credentials to prevent further authenticated calls. The best-effort approach (catch-swallow) favors responsiveness over guaranteed server termination, which is acceptable for most clients but not a guarantee in all environments. It coordinates with the HTTP client state by clearing the `Authorization` header so no stale tokens accompany future requests. ## Notes -- The server logout is best-effort; any exception during the logout request is swallowed, so callers may not be notified of server-side success. -- The method clears both tokens and the Authorization header unconditionally, ensuring no authenticated state remains in the client after invocation. +- Best-effort logout swallows exceptions; callers should not rely on server-side termination in all failure scenarios. +- After calling, tokens are cleared and the `Authorization` header is removed, so subsequent requests are unauthenticated until re-authentication occurs. --- @@ -979,15 +895,21 @@ public async Task MuteUserAsync(string username, int? durationMinutes = null, st **Returns:** `Task` -Asynchronously mutes a user identified by username by sending a MuteRequest to the moderation endpoint. It requires authentication, constructs the URL with a safely escaped username, and posts a JSON body containing the optional duration and reason. The call completes once the server confirms success. +MuteUserAsync mutes a specified user by posting a [`MuteRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md) to the moderation API endpoint `"/api/moderation/mute/{username}"`, after ensuring the caller is authenticated via `EnsureAuthenticated()`. The username is escaped with `Uri.EscapeDataString`, and the operation accepts an optional `durationMinutes` and an optional `reason`; the call completes when `EnsureSuccessAsync` confirms the response. ## Remarks -Centralizes moderation API interactions for muting and hides HTTP details from callers. It relies on MuteRequest to carry the mute parameters and on the client’s authentication and success-handling scaffolding to provide consistent behavior across the application. +This method serves as a focused wrapper around the moderation API, consolidating authentication, request payload construction via [`MuteRequest`](../../EchoHub.Core/DTOs/ModerationDtos.cs.md), and centralized success handling via `EnsureSuccessAsync`. It enables client code to perform user moderations without dealing with low-level HTTP details or URL construction, aligning with other moderation helper methods. + +## Example +```csharp +// Example: mute a user for 60 minutes with a reason +await apiClient.MuteUserAsync("user123", durationMinutes: 60, reason: "violation of rules"); +``` ## Notes -- durationMinutes is nullable; if omitted, the server may apply its default mute duration. -- reason is optional; omitting it mutes without a stated reason. -- If the server returns a non-success status, EnsureSuccessAsync will throw, propagating the failure to the caller. +- This method requires authentication; if not authenticated, `EnsureAuthenticated()` will trigger a failure. +- Both `durationMinutes` and `reason` are optional; passing null will rely on server-side defaults or policy. +- The username is URL-escaped to safely form the request URL in `"/api/moderation/mute/{username}"`. --- @@ -1008,25 +930,13 @@ public async Task NukeChannelAsync(string channelName) **Returns:** `Task` -NukeChannelAsync performs a server-side action to nuke a moderation channel identified by channelName. It ensures the client is authenticated, then issues an authenticated HTTP DELETE request to /api/moderation/channels/{Uri.EscapeDataString(channelName)}/nuke, and awaits the server's successful response. The method returns a Task and does not produce a value; it throws on error via EnsureSuccessAsync. +NukeChannelAsync issues an authenticated HTTP DELETE to remove a moderation channel identified by `channelName`. It begins by calling `EnsureAuthenticated()` to enforce credentials, then executes the request wrapped in `AuthenticatedRequestAsync` against the endpoint `/api/moderation/channels/{Uri.EscapeDataString(channelName)}/nuke` using `_http.DeleteAsync`, and finally validates the response with `EnsureSuccessAsync` before returning. ## Remarks - -This wrapper abstracts a destructive moderation action behind a clear name and a consistent authorization pattern. It ensures that the operation is performed only after the client is authenticated and that a non-success HTTP response will surface an exception via EnsureSuccessAsync. This keeps the caller focused on intent (nuke the channel) rather than on HTTP details. - -## Example - -```csharp -// Example: Nuke a moderation channel named "general" -await apiClient.NukeChannelAsync("general"); -``` +This method acts as a focused helper for performing the destructive operation of removing a moderation channel. By encapsulating authentication (`EnsureAuthenticated`) and consistent HTTP handling (`AuthenticatedRequestAsync` plus `EnsureSuccessAsync`), it provides a stable, discoverable entry point for channel-nuking that aligns with other moderation endpoints in the client. ## Notes - -- This is a destructive admin operation; use with caution and ensure proper permissions. -- The channelName is URL-escaped to handle spaces or special characters. -- The method completes only after a successful HTTP response; exceptions are thrown for errors. - +- `channelName` must be non-null; `Uri.EscapeDataString` will throw on null, so callers should validate input before invoking this method. --- @@ -1041,15 +951,7 @@ public async Task RefreshTokenAsync() **Returns:** `Task` -RefreshTokenAsync retrieves a new access token using the stored refresh token. It validates that a refresh token exists, posts it to the server's /api/auth/refresh endpoint as JSON, ensures the HTTP response indicates success, and then updates the client's token state from the returned LoginResponse. - -## Remarks -RefreshTokenAsync centralizes the token renewal flow inside the API client. It encapsulates the end-to-end interaction with the authentication service: validation of prerequisites, request construction, error propagation on HTTP or deserialization failures, and mutation of token state via SetTokens. Callers should rely on it to refresh credentials when needed, rather than handling HTTP details themselves. - -## Notes -- Throws InvalidOperationException when there is no refresh token available. (the early guard against missing _refreshToken) -- If the HTTP response indicates failure or the response body cannot be deserialized into a LoginResponse (i.e., it is null), the method propagates the error or throws InvalidOperationException("Token refresh returned empty response."). - +RefreshTokenAsync refreshes the client's authentication by sending the current `_refreshToken` to the server at `` `/api/auth/refresh` `` via POST and updating tokens with `` `SetTokens` `` on success. It guards against a missing `_refreshToken` by throwing `` `InvalidOperationException` `` when it is null or empty, then posts the request, ensures the HTTP response indicates success with `` `EnsureSuccessAsync` ``, and requires a non-null `` [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) `` to apply new tokens. If the response body is empty, it throws `` `InvalidOperationException` ``. --- @@ -1073,21 +975,16 @@ public async Task RegisterAsync(string username, string password, **Returns:** `Task` -Registers a new user by sending a registration payload to the server and returning the resulting LoginResponse. It constructs a RegisterRequest from the supplied username, password, and optional displayName and inviteCode, posts it to /api/auth/register, ensures the HTTP response indicates success, deserializes a LoginResponse from the response body, stores authentication tokens via SetTokens, and then returns the result. This method encapsulates the end-to-end registration flow and hides HTTP transport and token management details from callers. +Registers a new user by posting a [`RegisterRequest`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) to `/api/auth/register`, validating the response, reading a [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) from the content, and updating the client's tokens with `SetTokens` before returning the result as a [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md). +This is the onboarding entry point you call when creating an account with `username` and `password`, optionally supplying `displayName` and/or `inviteCode`. ## Remarks -RegisterAsync centralizes the end-to-end registration flow: request construction, transport, error handling, response parsing, and token persistence. By funneling these concerns through a single method, it ensures consistent error semantics (via EnsureSuccessAsync) and a single token-management strategy (via SetTokens) for a coherent authentication state on the client. - -## Example -```csharp -// Simple registration using only required parameters -var result = await apiClient.RegisterAsync("alice", "password"); -``` +Encapsulates the end-to-end registration flow: payload creation, HTTP transport, response handling, and token synchronization, so callers don't manage these concerns directly. It relies on `_http` to `PostAsJsonAsync` a [`RegisterRequest`](../../EchoHub.Core/DTOs/AuthDtos.cs.md), `EnsureSuccessAsync` to enforce successful HTTP statuses, and `SetTokens` to persist authentication state after a successful registration. ## Notes -- Deserialization can throw if the response body cannot be parsed as LoginResponse. -- If the server returns a non-success HTTP status, EnsureSuccessAsync will throw before parsing the body. -- Token storage is performed by SetTokens as a side effect; callers should expect authentication state to be updated after a successful registration. +- Non-success HTTP responses throw via `EnsureSuccessAsync`; handle to surface user-friendly errors. +- If the server returns an empty body, an `InvalidOperationException` is thrown with the message `"Registration returned empty response."`. +- Optional parameters `displayName` and `inviteCode` may be omitted (null) and will be serialized accordingly in the [`RegisterRequest`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) payload. --- @@ -1109,7 +1006,13 @@ public async Task RekeyChannelAsync(string channelName, RekeyChanne **Returns:** `Task` -RekeyChannelAsync asynchronously initiates a key-rotation for a specific channel by posting a RekeyChannelRequest to the server and returning the updated ChannelDto. The call first asserts the caller is authenticated, then issues a POST to /api/channels/{escaped channelName}/rekey with the request payload, awaits a successful response, and deserializes the response body into a ChannelDto. The returned value may be null if the response has no content. +Asynchronously rekeys a channel, given by `channelName`, by posting a [`RekeyChannelRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) to the server once the client is authenticated. It sends JSON to the endpoint `/api/channels/{Uri.EscapeDataString(channelName)}/rekey`, awaits a successful response via `EnsureSuccessAsync`, and returns the deserialized [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the response body (or `null` if the response contains no content). + +## Remarks +Centralizes the rekey operation behind the `ApiClient` to hide HTTP details from callers; authentication, request/response handling, and JSON deserialization are encapsulated in one place, making rekey usage simple and consistent across callers. + +## Notes +- Non-success HTTP responses (for example, 404 Not Found or 403 Forbidden) surface as exceptions via `EnsureSuccessAsync`. --- @@ -1130,15 +1033,7 @@ public async Task RevokeInviteAsync(string code) **Returns:** `Task` -RevokeInviteAsync revokes a previously issued invitation identified by the supplied code. It ensures the caller is authenticated, then issues an authenticated HTTP DELETE to /api/invites/{Uri.EscapeDataString(code)} and finally validates the server response via EnsureSuccessAsync. - -## Remarks -By encapsulating the DELETE call behind EnsureAuthenticated/AuthenticatedRequestAsync, this method provides a single, consistent mechanism for removing invites and centralizes error handling and authentication concerns. It shields callers from HTTP details and small failure modes, while aligning with other client methods that perform authenticated operations. - -## Notes -- EnsureSuccessAsync will throw on non-success HTTP responses, so callers should handle exceptions for cases like missing or already-revoked invites. -- The invite code is URL-escaped with Uri.EscapeDataString to safely embed it in the request path. -- This method relies on prior authentication; if credentials are missing or invalid, EnsureAuthenticated will fail before the request is sent. +RevokeInviteAsync revokes a pending invite identified by `code` by issuing an authenticated HTTP DELETE to `/api/invites/{Uri.EscapeDataString(code)}` and then validating the result with `EnsureSuccessAsync`. Use this when you need to remove a specific invite from the server instead of attempting a manual HTTP call; the operation requires authentication (`EnsureAuthenticated()`) and goes through `AuthenticatedRequestAsync` for centralized error handling. --- @@ -1163,90 +1058,81 @@ public async Task SendMessageWithAttachmentsAsync( **Returns:** `Task` -Sends a chat message to a channel with optional text and one or more attachments via multipart/form-data. For end-to-end encrypted channels, each attachment declares a kind and provides an encrypted preview (empty when none), and the caption is encrypted as well to protect metadata. +Sends a message to a channel with optional text and one or more attachments. When used on end-to-end encrypted channels, each attachment includes a declared kind and a room-encrypted preview, and the caption is encrypted as well. The method builds a `MultipartFormDataContent` payload containing the message `content` and, for each attachment, the file stream plus optional `kind` and `preview` fields, then posts to the channel messages API and returns the parsed [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the response. ## Remarks - -The method centralizes the multipart upload workflow for messages with attachments, including per-attachment encryption metadata handling and content-type determination. It performs authentication, constructs the form in a deterministic order so that per-file metadata (kind and preview) remains aligned with the corresponding file, and delegates transport and response handling to helper primitives (AuthenticatedRequestAsync and EnsureSuccessAsync). The optional size parameter enables a server-side formatting variation without changing the message payload. +This method centralizes channel-message sending with attachments behind an authentication boundary. It hides the multipart construction and per-attachment encoding from callers, while preserving the server's expectation that per-file `kind` and `preview` fields appear in the same order as the attachments. The URL path uses `Uri.EscapeDataString` to safely encode the `channelName`, and an optional `size` query parameter can be supplied to influence the shape of the response. ## Notes - -- For encrypted channels, DeclaredKind must be non-null to emit per-attachment kind and preview metadata; if DeclaredKind is null, those fields are omitted for that attachment. -- Each attachment is streamed individually with a Content-Type derived from the file name, and the server expects the metadata arrays (kind/preview) to line up with the corresponding files by index. -- The channel name is URL-escaped to form the request path, and an optional size query is appended when size is provided. - +- Only emit per-file `kind` and `preview` fields when `att.DeclaredKind` is non-null; this supports encrypted channels while keeping behavior sensible for non-encrypted use cases. +- The request is sent via an authenticated wrapper and uses `POST` to `/api/channels/{channelName}/messages` with an optional `size` query parameter when provided. +- Attachment content types are derived from the file name and set on the corresponding `StreamContent`; mismatches between file type and filename may affect server handling. --- -### SetTokens +### SendUrlAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -private void SetTokens(LoginResponse result) +public async Task SendUrlAsync(string channelName, string url, string? size = null) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `result` | [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) | — | +| `channelName` | `string` | — | +| `url` | `string` | — | +| `size` | `string?` | `null` | -**Returns:** `void` +**Returns:** `Task` -Sets the local authentication state from a LoginResponse by storing the access token, refresh token, and expiration, updates the HttpClient to send a Bearer token with every request, and notifies subscribers that tokens have been refreshed. +Posts the provided `url` to a specific channel by issuing a POST with a [`SendUrlRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) and returning the resulting `MessageDto?`. The operation requires authentication, URL-encodes the channel name with `Uri.EscapeDataString(channelName)`, and supports an optional `size` parameter that appends a `?size=...` query; the response is validated via `EnsureSuccessAsync` and deserialized into a [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the response content. ## Remarks -This method centralizes token management for the API client. By updating the shared HttpClient.DefaultRequestHeaders.Authorization, all outgoing requests automatically include the current access token, reducing boilerplate. The OnTokensRefreshed event allows other components to react to token updates (e.g., refresh UI or trigger persistence). It relies on a well-formed LoginResponse; callers should ensure result is non-null and contains valid Token/ExpiresAt values before invoking. + +This method encapsulates a server endpoint for sharing external URLs as channel messages, centralizing authentication, path encoding, and JSON (de)serialization behind a single helper. It keeps API usage consistent across callers and reduces boilerplate by hiding the HTTP details behind `ApiClient`. ## Notes -- No null checks exist for result or its properties; pass a valid LoginResponse to avoid NullReferenceException. -- DefaultRequestHeaders.Authorization is a global header on the HttpClient; concurrent token refreshes could race to set it, so coordinate refresh flows if ApiClient is used from multiple threads. + +- The return type is `MessageDto?`; callers should account for possible null if the response body is empty. +- The `size` query is only appended when `size` is not `null`; otherwise, the request path omits the query. +- The channel name is encoded with `Uri.EscapeDataString` to prevent path injection or malformed URLs. --- -### UnbanUserAsync +### SetTokens > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task UnbanUserAsync(string username) +private void SetTokens(LoginResponse result) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `username` | `string` | — | +| `result` | [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) | — | -**Returns:** `Task` +**Returns:** `void` -UnbanUserAsync lifts a ban on a user by issuing an authenticated POST request to the moderation API. You call this when you need to programmatically remove a ban for a specific username, relying on the client’s authentication state and centralized error handling rather than crafting HTTP requests yourself. +Sets authentication state from a [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) by copying `result.Token`, `result.RefreshToken`, and `result.ExpiresAt` into the private members `_accessToken`, `_refreshToken`, and `_expiresAt`, then configures the HTTP client's authorization header to apply the Bearer token to future requests by assigning a new `AuthenticationHeaderValue` to `_http.DefaultRequestHeaders.Authorization`. It also triggers the `OnTokensRefreshed` event to notify subscribers that the token set has been updated; this method is typically called after a successful login or token refresh to wire the tokens into the HTTP client. ## Remarks -This method encapsulates the moderation action behind a stable API: it first ensures the caller is authenticated, then performs the request via a wrapper that handles authentication context, and finally asserts success through a centralized error-check. The username is URL-encoded to safely embed special characters in the path, and the payload is an empty JSON object, signaling a no-content action beyond the identifer in the URL. This pattern keeps moderation actions consistent across the client and reduces boilerplate for callers. - -## Example -```csharp -await apiClient.UnbanUserAsync("troublemaker42"); -``` - -## Notes -- The call requires a valid authenticated context; unauthenticated callers will be rejected by EnsureAuthenticated. -- The method uses Uri.EscapeDataString to safely include the username in the URL path. -- A non-success HTTP response will throw via EnsureSuccessAsync, so callers may want to handle exceptions to surface user-friendly errors. -- The request body is an empty object, reflecting a command-based action rather than data payload. +By centralizing token handling within the API client, token state and request authorization headers stay in sync across all outgoing calls. The `OnTokensRefreshed` event provides a hook for UI updates or dependent services to react to token changes without scattering header mutations across call sites. --- -### UnmuteUserAsync +### UnbanUserAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task UnmuteUserAsync(string username) +public async Task UnbanUserAsync(string username) ``` **Parameters:** @@ -1258,165 +1144,133 @@ public async Task UnmuteUserAsync(string username) **Returns:** `Task` -UnmuteUserAsync unmutes a previously muted user by issuing a server request to the moderation API. Use this method when you need to lift a mute on a specific username from the EchoHub client. It enforces authentication, posts to the /api/moderation/unmute/{username} endpoint with an empty payload, and verifies the response signals success, abstracting away the HTTP boilerplate from callers. +UnbanUserAsync unbans a previously banned user by issuing an authenticated POST request to the moderation API at /api/moderation/unban/{username}. It first ensures the caller is authenticated, escapes the username for the URL, and sends an empty JSON payload. After receiving the response, it validates success via EnsureSuccessAsync. The method returns a Task and should be awaited by callers when they want to unban a user and await the operation’s completion. ## Remarks -UnmuteUserAsync is a thin wrapper around the service's moderation unmute endpoint. It centralizes authentication and error handling, ensuring consistency across moderation operations. By escaping the username in the URL, it guards against path-breaking characters and injection issues. The method returns a Task and does not expose a value; success is communicated by completing normally or by exceptions produced by EnsureSuccessAsync. - -## Example -```csharp -// Example usage -await client.UnmuteUserAsync("john_doe"); -``` - -## Notes -- The request uses POST to /api/moderation/unmute/{Uri.EscapeDataString(username)} with an empty payload. -- Authentication is required; EnsureAuthenticated() enforces this before the HTTP call. -- The method does not return a value; success is indicated by normal completion, otherwise an exception is thrown by EnsureSuccessAsync. -- Username is URL-encoded to prevent issues with special characters. +This method demonstrates the client’s pattern for authenticated, state-changing moderation actions, wrapping a REST endpoint in a typed API call and using EnsureAuthenticated/AuthenticatedRequestAsync to perform the request before validating the response with EnsureSuccessAsync. --- -### UpdateChannelTopicAsync +### UnmuteUserAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task UpdateChannelTopicAsync(string channelName, string? topic) +public async Task UnmuteUserAsync(string username) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `channelName` | `string` | — | -| `topic` | `string?` | — | +| `username` | `string` | — | + +**Returns:** `Task` -**Returns:** `Task` +UnmuteUserAsync lifts a user's mute by ensuring authentication and issuing an authenticated POST to `/api/moderation/unmute/{Uri.EscapeDataString(username)}` via `AuthenticatedRequestAsync` that calls `_http.PostAsJsonAsync(..., new { })`, followed by `EnsureSuccessAsync` to verify the result. -Updates the topic of a channel by performing an authenticated HTTP PUT to the server. Given a channel name and a desired topic, it builds UpdateTopicRequest and sends it to /api/channels/{channelName}/topic, with the channel name URL-escaped. It then validates the response and deserializes the updated ChannelDto from the response body. The method returns ChannelDto? to reflect a possibly absent payload. Use this when you need to change or clear a channel's topic in an authenticated context instead of composing the HTTP request manually. +Use this when your moderation flow needs to lift a mute on a specific user. ## Remarks -This method serves as the client-facing abstraction over a REST endpoint for channel topics. It centralizes authentication handling via EnsureAuthenticated and ensures consistent error handling with EnsureSuccessAsync, so callers don't need to manage HTTP status codes directly. It also encapsulates URL-encoding of the channel name, preventing issues with special characters. -## Example -```csharp -// Example: update topic for a channel -var updated = await apiClient.UpdateChannelTopicAsync("general", "Discussions about general topics"); -``` +- It relies on the shared authentication pattern (`EnsureAuthenticated` and `AuthenticatedRequestAsync`) to centralize authorization and error handling for moderation calls. +- Encoding the `username` with `Uri.EscapeDataString` prevents malformed URLs when usernames contain special characters. +- The method returns `Task` and does not expose a value; success is indicated by completing normally, while non-success responses throw. ## Notes -- Topic can be null to clear the topic. -- The response is deserialized to ChannelDto; if the server returns no content, the result may be null. -- The channel name in the URL is escaped to handle spaces or special characters. + +- Throws on non-success HTTP responses; callers should handle exceptions or propagate them as part of their error handling strategy. --- -### UploadAvatarAsync +### UpdateChannelTopicAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task UploadAvatarAsync(Stream imageStream, string fileName) +public async Task UpdateChannelTopicAsync(string channelName, string? topic) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `imageStream` | `Stream` | — | -| `fileName` | `string` | — | +| `channelName` | `string` | — | +| `topic` | `string?` | — | -**Returns:** `Task` +**Returns:** `Task` -Uploads the given image as the user's avatar by posting it as a multipart/form-data request to the server endpoint "/api/users/avatar". It ensures the caller is authenticated, builds the form data with the file attached under the field named "file", sets the content type from the file name, sends the request, validates the response, and deserializes the resulting JSON to return the AvatarAscii value (or null if missing). This method encapsulates the HTTP, content creation, and JSON parsing details, allowing callers to simply provide a stream and a filename to obtain the avatar representation. +Updates the topic for a channel by issuing an authenticated HTTP PUT request to the API endpoint `/api/channels/{channelName}/topic` with an [`UpdateTopicRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) payload, and returns the updated [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) from the response when the operation succeeds. This method encapsulates authentication, request serialization, and response deserialization so callers can change a channel’s topic without handling low-level HTTP details. ## Remarks -This method serves as a focused wrapper around the avatar-upload workflow. It centralizes authentication enforcement, multipart payload construction, endpoint contract (field name "file" and route "/api/users/avatar"), and server deserialization into a single, reusable call. By returning AvatarAscii from AvatarUploadResponse, it decouples UI concerns from image handling, enabling lightweight representations of avatars when a full image is not required. +This method acts as a focused API client wrapper that centralizes concerns like authentication, HTTP communication, and error handling behind a strongly-typed surface. It URL-encodes the channel name for safety and uses a dedicated [`UpdateTopicRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) DTO to keep the API contract isolated from domain models, returning a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) that reflects the post-update channel state. ## Example ```csharp -// Assuming 'client' is an instance of ApiClient and the user is authenticated -using var stream = File.OpenRead("path/to/avatar.png"); -string? ascii = await client.UploadAvatarAsync(stream, "avatar.png"); -Console.WriteLine(ascii ?? "No avatar ASCII returned"); +// Most common usage: update the topic of a channel and obtain the updated channel representation +ChannelDto? updated = await client.UpdateChannelTopicAsync("general", "Welcome to the General channel"); ``` ## Notes -- The request uses multipart/form-data with the form field named "file" as required by the server contract. Changes to the field name or endpoint would break this integration. -- Ensure authentication is established before calling; the method invokes EnsureAuthenticated() and may fail if the user is not authenticated. -- The method returns AvatarAscii from AvatarUploadResponse, which may be null if the server omits it or the response cannot be deserialized. +- The server determines how a null `topic` is interpreted (e.g., clearing the topic vs. rejecting the request); rely on the server contract for semantics. +- Non-success HTTP responses will throw via `EnsureSuccessAsync`, so callers should handle potential exceptions accordingly. --- -## SendUrlAsync +### UpdateProfileAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task SendUrlAsync(string channelName, string url, string? size = null) +public async Task UpdateProfileAsync(UpdateProfileRequest request) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `channelName` | `string` | — | -| `url` | `string` | — | -| `size` | `string?` | `null` | +| `request` | [`UpdateProfileRequest`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) | — | -**Returns:** `Task` +**Returns:** `Task` -SendUrlAsync posts a URL to the specified channel via the EchoHub HTTP API and returns the server’s MessageDto for the resulting message. Use this when you need to programmatically share a link in a channel and receive a typed representation of the posted message, with the client handling authentication, request construction, and response parsing. +Ensures the current user is authenticated, then issues an HTTP PUT to `/api/users/profile` with the [`UpdateProfileRequest`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) payload and returns the updated profile as a [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md). It uses `AuthenticatedRequestAsync` and `PutAsJsonAsync` for the request, `EnsureSuccessAsync` to verify the response, and `ReadFromJsonAsync()` to deserialize the result. ## Remarks - -By encapsulating the endpoint path construction, JSON payload, and authentication steps, this method reduces boilerplate for callers and enforces consistent error handling through EnsureSuccessAsync. It also escapes the channel name when building the URL to prevent routing errors caused by special characters. - -## Example - -```csharp -var result = await client.SendUrlAsync("general", "https://example.com", size: "1024"); -``` - -## Notes - -- The size parameter is appended as a raw query component; callers should pass URL-safe values or the method should be extended to URL-encode this value. -- The return value may be null if the response body is empty; callers should handle null appropriately. - +It centralizes authentication and error handling for profile updates, providing a single, strongly-typed contract for client code. This wrapper mirrors the server API surface at `/api/users/profile`, ensuring consistency between client calls and server expectations while keeping callers focused on business logic rather than HTTP plumbing. --- -## UpdateProfileAsync +### UploadAvatarAsync > **File:** `src/EchoHub.Client/Services/ApiClient.cs` > **Kind:** method ```csharp -public async Task UpdateProfileAsync(UpdateProfileRequest request) +public async Task UploadAvatarAsync(Stream imageStream, string fileName) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `request` | [`UpdateProfileRequest`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) | — | +| `imageStream` | `Stream` | — | +| `fileName` | `string` | — | -**Returns:** `Task` +**Returns:** `Task` -Updates the authenticated user's profile by issuing a PUT request to /api/users/profile with the provided UpdateProfileRequest and returning the updated UserProfileDto. It first ensures the caller is authenticated, then performs the HTTP request, validates the response, and deserializes the JSON payload into a UserProfileDto (or null if the response has no body). +Uploads a user avatar by streaming the provided image as multipart/form-data to the server endpoint `/api/users/avatar` after ensuring the caller is authenticated. The image is wrapped in a `MultipartFormDataContent` with a `StreamContent` whose `ContentType` is derived from `GetContentType(fileName)`, the request is posted via `AuthenticatedRequestAsync` to `_http.PostAsync(...)`, the response is deserialized with `ReadFromJsonAsync()`, and the method returns `AvatarAscii` (or `null` if absent). ## Remarks +Encapsulates avatar-upload logic behind a single API that handles authentication, HTTP payload construction, and JSON deserialization, reducing duplication for callers and giving a single contract for server-side [`AvatarUploadResponse`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md). It coordinates the HTTP client, content builders, and the [`AvatarUploadResponse`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) contract to produce a simple ASCII avatar string, decoupling UI concerns from transport details. -This method centralizes the concerns around making authenticated HTTP calls: enforcing authentication, handling HTTP success semantics, and deserializing the server payload into a typed DTO. It provides a clean, reusable surface for updating the user's profile without leaking HTTP details to callers. By wrapping EnsureSuccessAsync and JSON deserialization, it promotes consistent error handling and data shape assumptions across the client. - -## Notes +## Example +```csharp +// Example: typical usage +string? ascii = await client.UploadAvatarAsync(imageStream, "avatar.png"); +``` -- The return value may be null if the response body is empty; callers should guard against null. -- EnsureSuccessAsync will throw on non-success HTTP statuses, so error handling is centralized here. -- Deserialization uses `ReadFromJsonAsync`; ensure the response content is JSON matching UserProfileDto. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/AsyncRunner.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/AsyncRunner.cs.md index 247ed4a..b581617 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/AsyncRunner.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/AsyncRunner.cs.md @@ -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` representing the work, an `Action` (`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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md index 2724154..a8a5c9a 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/AvatarHelper.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/AvatarHelper.cs.md index 1ef8c6b..b598a5f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/AvatarHelper.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/AvatarHelper.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md index 144ef1c..7f1b653 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md @@ -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 server’s 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. \ No newline at end of file +- 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]". \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md index 9290f01..5cb4f7b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardImage.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardImage.cs.md index 87b24d4..e89fc02 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardImage.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/ClipboardImage.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/ConnectionManager.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/ConnectionManager.cs.md index f8ba1b7..ce2e313 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/ConnectionManager.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/ConnectionManager.cs.md @@ -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>` | — | -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` and `Dictionary>`); 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` and `Dictionary>` are mutable; avoid mutating them in place and consider treating the `ConnectResult` as a snapshot that should be cloned if you require immutability downstream. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md index 305846a..4a5ca9b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md @@ -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); - -echo.OnMessageReceived += message => -{ - // MessageDto is provided by the library; content may be the LockedMessagePlaceholder - Console.WriteLine($"Message received in {message.ChannelName}: {message.Content}"); -}; - -if (echo.IsConnected) -{ - Console.WriteLine("Currently connected to the chat hub."); -} - -// Remember to dispose when finished -await echo.DisposeAsync(); +// Assume these are already created: serverUrl (string), apiClient (ApiClient), +// encryption (ClientEncryptionService), roomKeys (RoomKeyStore). +var connection = new EchoHubConnection(serverUrl, apiClient, encryption, roomKeys); + +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"); + +// 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 isn’t 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 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` 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 history = new List(); -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md index e91ab80..8f7d908 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md @@ -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 EchoHub’s 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 cross‑platform 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`. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md index 2c5e31b..6e5548b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md @@ -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. \ No newline at end of file + +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md index 8e26356..e500505 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md @@ -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. \ No newline at end of file +- If `DeclaredKind` is provided for an encrypted attachment, ensure `EncryptedPreview` is also supplied to avoid inconsistent previews. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/PathSetup.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/PathSetup.cs.md index 1127023..80bfe67 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/PathSetup.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/PathSetup.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md index 6dc4b0b..4206869 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md @@ -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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md index 187f28d..e3f31ad 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md @@ -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. \ No newline at end of file +- `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). \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md index ba601d4..43db879 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md @@ -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.Json’s 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 EchoHub’s 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``` 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateChecker.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateChecker.cs.md index a36a8d0..07c8f16 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateChecker.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/UpdateChecker.cs.md @@ -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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Services/UserSession.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Services/UserSession.cs.md index 980a9de..6c71a1e 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Services/UserSession.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Services/UserSession.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Themes/Theme.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Themes/Theme.cs.md index 136525e..c332aee 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Themes/Theme.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Themes/Theme.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/Themes/ThemeManager.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/Themes/ThemeManager.cs.md index bc369b1..6401af5 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/Themes/ThemeManager.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/Themes/ThemeManager.cs.md @@ -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 theme’s 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 GetAvailableThemes() **Returns:** `List` -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` 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,385 +210,350 @@ 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 +### BuiltInThemes > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme ClassicTheme = new() +private static readonly List BuiltInThemes = + [ + DefaultTheme, + TransparentTheme, + TransparentLightTheme, + ClassicTheme, + LightTheme, + HackerTheme, + SolarizedTheme, + DraculaTheme, + MonokaiTheme, + NordTheme, + GruvboxTheme, + OceanTheme, + HighContrastTheme, + RosePineTheme + ] ``` -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. +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 -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. +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` 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 -- 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. +- The `List` 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` and expose a safe accessor. --- -### DefaultTheme +### ClassicTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme DefaultTheme = new() +private static readonly Theme ClassicTheme = 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. +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 -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. +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. --- -### DraculaTheme +### DefaultTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme DraculaTheme = new() +private static readonly Theme DefaultTheme = 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. +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 -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. +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 -- 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. +- 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. + --- -### HackerTheme +### DraculaTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme HackerTheme = new() +private static readonly Theme DraculaTheme = 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. +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 -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. +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 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. +- 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. --- -### HighContrastTheme +### GruvboxTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme HighContrastTheme = new() +private static readonly Theme GruvboxTheme = 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. +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 -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. +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 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. +- Private field scope means external code cannot reference `GruvboxTheme` directly; use the public theming API (e.g., `ThemeManager`) to switch or retrieve themes. --- -### JsonOptions +### HackerTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly JsonSerializerOptions JsonOptions = new() +private static readonly Theme HackerTheme = new() ``` -JsonOptions is a privately scoped, preconfigured JsonSerializerOptions instance used by ThemeManager to serialize JSON with the project’s 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. +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 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. +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. --- -### LightTheme +### HighContrastTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme LightTheme = new() +private static readonly Theme HighContrastTheme = 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. +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 - -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. +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. -- It is a static shared instance; mutating its nested ThemeColors at runtime would have global effects; treat as read-only after initialization. --- -### MonokaiTheme +### JsonOptions > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme MonokaiTheme = new() +private static readonly JsonSerializerOptions JsonOptions = 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. +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 -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. +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 -- 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. +- 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. --- -### OceanTheme +### LightTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme OceanTheme = new() +private static readonly Theme LightTheme = 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. +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 -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 project’s visual system for the palette to render correctly. +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. --- -### SolarizedTheme +### MonokaiTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme SolarizedTheme = new() +private static readonly Theme MonokaiTheme = 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. +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 -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. +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 -- 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. +- `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. --- -### ThemeDir +### NordTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly string ThemeDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes") +private static readonly Theme NordTheme = new() ``` -ThemeDir is a private, static readonly string that resolves to the user-specific themes directory by combining the current user’s profile folder with .echohub/themes. It provides a single, OS-agnostic path for ThemeManager to load and save theme files, avoiding scattered string literals. +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 -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. + +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 -- 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. + +- 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. --- -### TransparentLightTheme +### OceanTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme TransparentLightTheme = new() +private static readonly Theme OceanTheme = 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). +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 -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. +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. --- -### TransparentTheme +### RosePineTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme TransparentTheme = new() +private static readonly Theme RosePineTheme = 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). +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 -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. +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 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. +- 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`. --- -## BuiltInThemes +### SolarizedTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly List BuiltInThemes = - [ - DefaultTheme, - TransparentTheme, - TransparentLightTheme, - ClassicTheme, - LightTheme, - HackerTheme, - SolarizedTheme, - DraculaTheme, - MonokaiTheme, - NordTheme, - GruvboxTheme, - OceanTheme, - HighContrastTheme, - RosePineTheme - ] +private static readonly Theme SolarizedTheme = new() ``` -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. +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 -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. - -## 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. +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. --- -## GruvboxTheme +### ThemeDir > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme GruvboxTheme = new() +private static readonly string ThemeDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes") ``` -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 Status—each 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. +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 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. +- 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 - -- 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. +- 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. --- -## NordTheme +### TransparentLightTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme NordTheme = new() +private static readonly Theme TransparentLightTheme = 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. +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 -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. +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 -- 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. +- 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. --- -## RosePineTheme +### TransparentTheme > **File:** `src/EchoHub.Client/Themes/ThemeManager.cs` > **Kind:** field ```csharp -private static readonly Theme RosePineTheme = new() +private static readonly Theme TransparentTheme = 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. +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 -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. - -## 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. +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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md index f7b343a..1affb7e 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md @@ -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 (?`, 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. -// 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 -{ - new ChatSegment("[alice] ", ChatColors.RailAttr), - new ChatSegment("This is a message", null) -}; -var coloredLine = new ChatLine(segments); -``` - -## 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. --- @@ -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) +{ + // Open the image for viewing +} +else if (action == AttachmentAction.SaveImage) { - case AttachmentAction.OpenImage: - // Open the image in a viewer - break; - case AttachmentAction.SaveImage: - // Persist the image to disk - break; + // 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatListSource.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatListSource.cs.md index dbc48ae..b6798ae 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatListSource.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatListSource.cs.md @@ -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`, 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` (`_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. \ No newline at end of file +- `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). \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md index d3d35b7..0fa5662 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md @@ -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` 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 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`, 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 MentionChannels => _mentionChannels ``` -Exposes the set of chat channels that currently have unread mentions of the current user. The value is returned as an `IReadOnlySet` 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` 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`; 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 ActionHeaderSegments(string time) => **Returns:** `List` -ActionHeaderSegments builds the header used when rendering /me action messages in the chat UI. It returns a `List` 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` 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 message’s 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 channel’s 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 username’s 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 channel’s 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 user’s 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 don’t 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 FormatEmbed(EmbedDto embed, int chatWidth) **Returns:** `List` -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` 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` 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 don’t 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 FormatMessage(MessageDto message) **Returns:** `List` -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 FormatWithDateRules(List messages, out DateTi **Returns:** `List` -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 batch’s 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 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? GetMessages(string channelName) **Returns:** `List?` -Retrieves the current list of ChatLine entries for a specific channel by name from the internal message store. It returns the existing `List` 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` 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` is not cloned; modifications to it affect the internal store unless an external copy is created. +Retrieves the `List` 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 GetUnreadCounts() => _channelUnread **Returns:** `Dictionary` -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` 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` implementation and to the internal state. If you only need to observe values, prefer returning a read-only view such as an `IReadOnlyDictionary` 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` 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 HeaderSegments(string time, string nick, Attrib **Returns:** `List` -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` 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` 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 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 channel’s 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 isn’t 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 channel’s 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 olderMessages) **Returns:** `void` -PrependHistory prepends older messages to the front of a channel’s 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 channel’s date-rule conventions), and inserts them at the beginning of the buffer. If the channel isn’t 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 RailPrefix() => **Returns:** `List` -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` 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` 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` that callers are free to mutate; it does not mutate any shared state. + +- This method produces a fresh `List` 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 channel’s 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 sender’s 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 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 SystemHeaderSegments(string time) => **Returns:** `List` -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` 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,55 +1026,46 @@ private static List WordWrap(string text, int maxCols) **Returns:** `List` -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. --- -### NickColWidth +### ContentIndentCols > **File:** `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs` > **Kind:** field ```csharp -public const int NickColWidth = 12 +public const int ContentIndentCols = 6 + NickColWidth + 3 ``` -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. +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 -Centralizes the presentation detail of the nickname column, avoiding scattered magic numbers across rendering code. By exposing this as a single public constant, it’s 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. +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 -- 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. +- It is a compile-time constant; changing `NickColWidth` or `ContentIndentCols` requires a rebuild of the consuming code. --- -## ContentIndentCols +### NickColWidth > **File:** `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs` > **Kind:** field ```csharp -public const int ContentIndentCols = 6 + NickColWidth + 3 +public const int NickColWidth = 12 ``` -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md index 21f2acd..123092f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md @@ -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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md index db4d20b..f6f474f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md @@ -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. \ No newline at end of file +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, it’s 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md index 9c32c8c..a1a58da 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md @@ -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. \ No newline at end of file +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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md index 51ada2a..d5c9e44 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md index 72cc64a..95c0dfc 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md @@ -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 #, 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. \ No newline at end of file + +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md index 2786e8b..060a085 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md index 5c5d48d..f9e7943 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md index bc438d4..2634374 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md index 4b793ec..8d69f6c 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md @@ -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 user’s 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md index 1e62513..7c78cc0 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md @@ -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` 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 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; +} +``` + --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md index 944d92a..70761c3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md @@ -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` 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md index e4e4dc6..7759362 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md @@ -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. \ No newline at end of file + +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md index 5208c38..573268b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md @@ -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`. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md index 8c286fd..1978f23 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md @@ -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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md index f79d7a8..849a5db 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md @@ -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. \ No newline at end of file +- Leading whitespace before the optional '#' is not trimmed; strings starting with spaces will fail to parse gracefully. diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md index dcbf733..fc53db3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md index 3aaa4ec..c54c033 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md @@ -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. \ No newline at end of file +- `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`. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md index c41bb89..068ca55 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md @@ -8,15 +8,12 @@ public class SearchListSource(List 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. \ No newline at end of file +- `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`. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/UserListSource.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/UserListSource.cs.md index a42c501..9d6db80 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/UserListSource.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/ListSources/UserListSource.cs.md @@ -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. \ No newline at end of file +- `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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Client/UI/MainWindow.cs.md b/docs/auriondocs/Code/src/EchoHub.Client/UI/MainWindow.cs.md index 3da952b..57d6936 100644 --- a/docs/auriondocs/Code/src/EchoHub.Client/UI/MainWindow.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Client/UI/MainWindow.cs.md @@ -13,6 +13,8 @@ - [ApplyColorSchemes](#applycolorschemes) - [BuildMenuBar](#buildmenubar) - [ClearAll](#clearall) + - [ClickChannelRegex](#clickchannelregex) + - [ClickMentionRegex](#clickmentionregex) - [ConfirmDeleteMessage](#confirmdeletemessage) - [CopyToClipboard](#copytoclipboard) - [EnsureChannelInList](#ensurechannelinlist) @@ -60,12 +62,17 @@ - [UpdateStatusBar](#updatestatusbar) - [UpdateTopicBar](#updatetopicbar) - [AltQKey](#altqkey) + - [AppVersion](#appversion) - [CtrlCKey](#ctrlckey) + - [CtrlKKey](#ctrlkkey) - [CtrlVKey](#ctrlvkey) - [CtrlXKey](#ctrlxkey) - [CtrlYKey](#ctrlykey) + - [DefaultInputTitle](#defaultinputtitle) - [EnterKey](#enterkey) - [F2Key](#f2key) + - [F6Key](#f6key) + - [NewlineKey](#newlinekey) - [SlashCommands](#slashcommands) - [SpinnerFrames](#spinnerframes) - [StatusActivityAttr](#statusactivityattr) @@ -76,13 +83,6 @@ - [StatusTransitionalAttr](#statustransitionalattr) - [TabKey](#tabkey) - [UsersPanelWidth](#userspanelwidth) -- [ClickChannelRegex](#clickchannelregex) -- [ClickMentionRegex](#clickmentionregex) -- [AppVersion](#appversion) -- [CtrlKKey](#ctrlkkey) -- [DefaultInputTitle](#defaultinputtitle) -- [F6Key](#f6key) -- [NewlineKey](#newlinekey) --- @@ -95,16 +95,17 @@ public sealed partial class MainWindow : Runnable ``` -Main Terminal.Gui window for the EchoHub chat client: composes channel list, message list, input field, status/topic labels, users panel and menu bar, and exposes events used by the application to react to user actions (channel selection, message submit, file/image paste, connect requests). Reach for this class when you need a ready-made, full-featured chat UI that integrates with an application orchestrator rather than building the UI pieces and event plumbing yourself. +Main Terminal.Gui-backed chat window that composes and coordinates the channel list, message view, input field, users panel, and menus for the EchoHub client. Reach for `MainWindow` when you want a complete, ready-to-run terminal UI for chat (keyboard handling, paste/drag staging, autocomplete and connection UI) rather than composing low-level `Terminal.Gui` controls yourself. ## Remarks -MainWindow encapsulates the Terminal.Gui controls and client-facing UI state for the chat client and acts as the bridge between user interactions and the application logic. It keeps local collections of channels, topics and metadata, hosts a ChatMessageManager for message lifecycle, and exposes a small set of events (OnChannelSelected, OnMessageSubmitted, OnFilesStaged, OnImagePasted, OnConnectRequested) so the rest of the app can respond to user actions without reaching into control internals. Key bindings are represented as KeyCode constants to allow straightforward switch/case handling of raw key codes. +`MainWindow` is the single UI surface that translates user interactions into domain-level events and coordinates the internal UI state. It owns and wires together UI components such as `ListView` (for `*_channelList` and `*_messageList`), `TextView` (`_inputField`), `FrameView` (`_chatFrame`, `_inputFrame`, `_usersFrame`), `Label` (`_statusLabel`, `_topicLabel`), and a `MenuBar` (`_menuBar`). The class exposes application-facing events — `OnChannelSelected`, `OnMessageSubmitted`, `OnFilesStaged`, `OnImagePasted`, and `OnConnectRequested` — so the surrounding orchestration (for example the [`AppOrchestrator`](../AppOrchestrator.cs.md)/`Make`) can react without needing to know UI internals. `MainWindow` also holds UI-centric state such as `AppVersion`, the `SlashCommands` used for Tab completion, per-channel metadata (`_channelNames`, `_channelTopics`, `_channelPublic`, `_channelProtected`, `_systemChannels`), and message handling via `ChatMessageManager`. + +Keyboard handling is intentionally implemented using raw `KeyCode` constants (for example `EnterKey`, `NewlineKey`, `TabKey`, `CtrlKKey`, `F6Key`) so comparisons avoid `Key.Equals` semantics that include the `Handled` flag; this makes the key-switching logic deterministic and suitable for switch statements. The users panel visibility is controlled by `_usersPanelVisible` and sized using the `UsersPanelWidth` constant; toggling it affects layout and available chat width (`_lastChatWidth`). ## Notes -- Key bindings are defined as raw KeyCode constants (e.g. EnterKey, CtrlKKey, F6Key). The code compares KeyCode values directly so handlers should compare against those constants rather than relying on Key.Equals semantics. -- The users panel defaults to visible and has a fixed width (UsersPanelWidth = 22). Toggle/resize behavior is managed internally by the window layout. -- SlashCommands contains the list of available client-side slash commands used for Tab autocomplete; update DefaultInputTitle if you change key bindings or common hints. -- File and image events convey concrete payloads: OnFilesStaged provides the channel plus absolute paths of existing files; OnImagePasted provides the channel plus PNG-encoded image bytes. Consumers should validate and process those payloads appropriately. +- UI thread: `MainWindow` is a `Terminal.Gui`-based UI — subscribers to `OnMessageSubmitted`, `OnFilesStaged`, `OnImagePasted`, and other events are invoked from the UI context. Handlers must avoid long/blocking work and should dispatch to background threads or task queues for I/O, network, or CPU-heavy operations. +- Key comparison detail: key bindings are implemented as `KeyCode` constants and compared by raw value (e.g. `CtrlKKey = KeyCode.K | KeyCode.CtrlMask`). If you add custom key handling, compare against the same raw `KeyCode` values rather than relying on `Key.Equals` or higher-level key abstractions. +- Staged attachments and file paths: `OnFilesStaged` provides absolute filesystem paths for files already accepted by the UI (the code expects existing files). Consumers should still validate and handle missing/removed files; the `_hasStagedAttachments` flag indicates staged state within the `MainWindow` and must be cleared by whatever logic performs the actual upload/send. --- @@ -124,14 +125,13 @@ public MainWindow(IApplication app, ChatMessageManager messageManager) | `messageManager` | `ChatMessageManager` | — | -Initializes the main application window and assembles EchoHub’s client UI: a four-panel layout with a top menu bar, a left channels pane, a center chat area, a bottom input region, and a right online-users pane. It wires data sources to their views, subscribes to message and history events, binds selection and input handlers so the UI stays in sync with channel changes, and includes a small UX safeguard by rebinding Ctrl+W to delete-word-left to avoid clipboard exceptions. +`MainWindow` wires the core UI dependencies (`IApplication` and `ChatMessageManager`), subscribes to message events (`MessagesChanged` and `HistoryPrepended`), and builds the primary terminal UI for the EchoHub client. It lays out a top menu bar, a left channels panel, a center chat area with a messages list and input field, and a right online users panel, wiring up the corresponding list sources and event handlers to reflect data changes and user interactions. ## Remarks -Serves as the UI composition root for the EchoHub client, carefully placing panels with fixed coordinates and dimensions to provide a stable, predictable layout across interactions. It connects ChannelListSource, ChatListSource, and UserListSource to their respective ListView controls, enabling efficient incremental rendering and per-item styling. Event wiring ensures the view updates reflect incoming messages, channel changes, and user activity without polluting business logic. The Ctrl+W rebound is a pragmatic, user-facing compatibility tweak that prevents clipboard-related crashes during text editing. +`MainWindow` serves as the composition root for the client UI, coordinating three specialized list sources ([`ChannelListSource`](ListSources/ChannelListSource.cs.md), [`ChatListSource`](Chat/ChatListSource.cs.md), [`UserListSource`](ListSources/UserListSource.cs.md)) with their corresponding `ListView`s and centralizing interaction wiring (selection, input, rendering). It also incorporates a targeted keyboard-binding workaround to ensure a stable editing experience in the terminal UI by remapping `Key.W.WithCtrl` to `Command.KillWordLeft` to avoid clipboard-related crashes. ## Notes -- Be mindful of unsubscribing event handlers if this window is ever disposed to avoid memory leaks. -- Layout relies on fixed panel dimensions; changing UsersPanelWidth or Y offsets could disrupt alignment. +- The input field rebinds Ctrl+W to delete-word-left to prevent clipboard exceptions when the OS clipboard is involved; if you customize input controls or port to a different UI framework, review keyboard bindings to avoid unintended clipboard interactions. --- @@ -144,13 +144,10 @@ public string CurrentChannel => _messageManager.CurrentChannel ``` -Get the name of the currently active channel by delegating to the underlying message manager via _messageManager.CurrentChannel. This read-only property provides a convenient, UI-friendly way to display or react to the active channel without coupling callers to the message manager. +The `CurrentChannel` property exposes the name of the actively used message channel by delegating to the internal `_messageManager`. It serves as a lightweight, UI-friendly accessor that decouples UI code from the underlying manager while providing a stable source of truth for the current channel. ## Remarks -This property serves as a small abstraction that decouples the UI from the messaging subsystem. It simply forwards to the message manager, so changes in how the active channel is determined won't require changes at call sites. If the active channel can change over time, retrieve it as needed (e.g., for bindings or status displays) rather than caching the value. - -## Notes -- Read-only property; to change the channel, use the message manager's API rather than assigning to this property. If the channel value can be null, callers should handle null values appropriately. +The property acts as a forwarder to `_messageManager.CurrentChannel`, encapsulating the channel retrieval so callers don't need to reference the manager directly. It defines a read-only snapshot of the channel that the UI can display or react to, without incurring additional logic in this wrapper. This separation helps preserve a clean boundary between the UI layer and the message subsystem. --- @@ -163,16 +160,10 @@ public bool HasPendingReplyIndicator => _replyTitleFragment is not null ``` -Indicates whether there is a pending reply by evaluating whether the private field `_replyTitleFragment` is non-null. Use this property when the UI or business logic needs to know if a reply is currently being prepared, without directly touching private fields. +`HasPendingReplyIndicator` reports whether a pending-reply indicator should be shown in the `MainWindow` UI. It returns true when `_replyTitleFragment` is not null, indicating there is a pending reply ready to be presented. ## Remarks - -By exposing the condition as a public property, callers express intent clearly—whether a reply is pending—without relying on the internal field’s exact name or lifecycle. It also centralizes the decision, so changes to the internal representation (for example, signaling a pending state with a different fragment) require updates only to this property. - -## Notes - -- The value reflects the internal condition at the moment of access; it may not guarantee that a UI indicator has already been rendered if the underlying state changes without notification. -- If `_replyTitleFragment` is updated from a background thread, ensure changes to the UI that depend on this property are observed on the appropriate UI thread to avoid threading issues. +By wrapping the internal `_replyTitleFragment` state, this property exposes a stable UI contract for the `MainWindow` without leaking implementation details. It turns on the indicator whenever `_replyTitleFragment` is non-null, and off otherwise, making the visibility rule easy to reason about from the UI layer. This centralizes the visibility logic in one place, so changes to how a pending reply is represented don't ripple through callers. --- @@ -185,14 +176,13 @@ private bool IsCurrentChannelReadOnly => _systemChannels.Contains(_messageManage ``` -IsCurrentChannelReadOnly is a read-only computed property that indicates whether the active channel is a system channel and should be treated as read-only in the UI. -It returns true when _systemChannels.Contains(_messageManager.CurrentChannel), and false otherwise, enabling UI logic to gate actions like message composition accordingly. +IsCurrentChannelReadOnly indicates whether the currently active channel is a system (read-only) channel. It returns true when the current channel is contained in the `_systemChannels` collection; otherwise false. This centralized property allows UI logic to decide, for example, whether message input should be enabled when the user is in a read-only channel, without scattering the `_systemChannels.Contains(_messageManager.CurrentChannel)` check throughout the code. ## Remarks -IsCurrentChannelReadOnly centralizes the concept of a read-only channel, preventing scattered checks throughout the UI logic. It ties together the system-channel collection and the current channel reported by the message manager, ensuring consistent behavior whenever the active channel changes. +By encapsulating the read-only rule in a single property, the codebase gains a single source of truth for what constitutes a system channel. If `_systemChannels` is updated or the current channel reference changes, this property automatically reflects the new state, keeping UI behavior consistent. It also improves readability and testability by naming the intent instead of embedding the containment check in multiple places. ## Notes -- The property is evaluated on access; callers should react to state changes (e.g., by refreshing the UI) to avoid presenting stale read-only state. +- The property is private; external consumers cannot rely on it directly. Tests should exercise the observable behavior (e.g., enabling/disabling input) rather than this accessor. --- @@ -205,15 +195,14 @@ private bool IsTransitionalStatus => _connectionStatus is not ("Connected" or "D ``` -IsTransitionalStatus is a private boolean property that indicates whether the current internal connection status is not one of the stable terminal states 'Connected' or 'Disconnected'. It evaluates the underlying _connectionStatus and yields true for any other value, signaling a transitional or in-progress state (for example 'Connecting', 'Reconnecting', or similar custom statuses). +Determines whether the current `_connectionStatus` represents a transitional, non-final state. Use this property when you need to react to an ongoing change in the connection (for example, showing a loading indicator or deferring user actions) rather than simply checking for `Connected` or `Disconnected`. ## Remarks -By encapsulating this check, the class centralizes the notion of a transitional connection state and avoids repeating string comparisons across the UI logic. The predicate relies on the internal _connectionStatus field, so changes to how statuses are named or stored can be addressed in one place. This abstraction clarifies intent—code that reads IsTransitionalStatus expresses 'we are in flux' without caring about the exact status value. +This property encodes the idea that a connection is in flux rather than settled into a terminal state. By centralizing the check, it prevents scattered string comparisons across the class and makes it easier to adapt if the exact terminal labels change. It works in concert with UI state management to drive indicators or input gating during transitions. ## Notes -- Null or unexpected _connectionStatus yields true; ensure initialization or guard against null. -- It uses exact string literals ('Connected' and 'Disconnected'); if localization or status naming changes, update accordingly. -- It is private; external components cannot rely on it. If you need external access, consider exposing a public wrapper or moving the logic to a shared utility. +- It hinges on the exact string literals `Connected` and `Disconnected`; changes to these labels or localization would require updating this property. +- Being private, external code cannot rely on this property; expose a dedicated API if external components must react to transitional states. --- @@ -228,15 +217,15 @@ public void ApplyColorSchemes() **Returns:** `void` -Applies the currently registered color schemes to all views and updates the UI to reflect the active theme. Call this after changing themes to refresh colors across the interface, ensuring base, menu, and border colors are propagated to the appropriate controls. +Applies the currently registered color schemes to all views and should be invoked after a theme change to refresh UI colors. It queries `SchemeManager` for `Base`, `Menu`, and `Border` schemes; the `Base` scheme is applied to the root view, then propagated to most subviews. If a `Border` scheme is available (falling back to `Base` when absent), it is applied to frame borders via `FrameView.Border` to allow borders to be tinted independently of text. After processing the base scheme, any available `Menu` scheme is applied to `_menuBar`, `_statusLabel`, and `_topicLabel` to ensure menu-related visuals reflect the active theme. ## Remarks -Centralizes theming so color changes are applied consistently through a single call. It applies the base scheme to most subviews and intentionally excludes the menu bar and a few chrome labels that have their own styling. Border colors come from a separate border scheme if available, allowing borders to be tinted independently from text, while the menu-related chrome receives the menu scheme. +By centralizing theming logic here, the UI consistently reflects theme changes without each subview duplicating scheme-updating code. It also accommodates borders that should be tinted differently by applying the `Border` scheme to `FrameView.Border` when present, allowing border tones to diverge from text colors. The propagation intentionally excludes `_menuBar`, `_statusLabel`, and `_topicLabel` from the base propagation so they can be driven by the `Menu` scheme for coherent menu visuals across the interface. ## Notes -- Requires a non-null Base scheme to perform theming; otherwise the method performs no action. -- Border color updates occur only if a FrameView is encountered and a non-null border scheme is available; otherwise borders are left unchanged. -- Some subviews are excluded from the base scheme (the menu bar, status label, and topic label) and are updated via the menu scheme instead. +- If `Base` is null, no base propagation occurs; base-based updates are guarded by a null check. +- The `Border` scheme is optional; `borderScheme` is applied to borders only when it is not null. +- During propagation, frames are updated via `FrameView.Border?.SetScheme(borderScheme)` only when a suitable `borderScheme` exists. --- @@ -251,38 +240,94 @@ private MenuBar BuildMenuBar() **Returns:** `MenuBar` -BuildMenuBar constructs the top-level MenuBar for EchoHub’s main window by assembling the File, Server, and User menus and a dynamic Theme submenu derived from ThemeManager. It wires user actions to corresponding events, conditionally includes a Rollback option when a backup exists, and always exposes update and quit actions; the returned bar is positioned at the origin and stretched to fill width, with a small mouse-handling workaround to ensure clicks target the menu items themselves. +Builds the application's top-level `MenuBar` by composing three root menus — `File`, `Server`, and [`User`](../../EchoHub.Core/Models/User.cs.md) — and appending a dynamic theme submenu sourced from `Themes.ThemeManager.GetAvailableThemes()`. It also conditionally inserts a rollback entry when a backup exists via `UpdateBackupService.BackupExists()` and wires up all actions to their corresponding callbacks (e.g. `OnProfileRequested`, `OnThemeSelected`, `OnConnectRequested`, `OnDisconnectRequested`, `OnLogoutRequested`, `OnCreateChannelRequested`, `OnDeleteChannelRequested`, `OnSavedServersRequested`, `ToggleUsersPanel`, `OnCheckForUpdatesRequested`). Finally, it lays out the menu bar at `(0,0)`, stretches it to fill width, and applies a mouse-through workaround by setting `ViewportSettings` to `TransparentMouse` on each `MenuBarItem`'s `CommandView` to ensure clicks hit the intended item. -## Remarks +--- -It centralizes the composition of the app’s menu, guaranteeing a consistent structure and separator placement between groups. By drawing the theme options from ThemeManager.GetAvailableThemes and conditionally showing a backup rollback, the menu reflects runtime state without scattering logic across call sites. Each action is wired to an event or callback (OnThemeSelected, OnProfileRequested, OnCheckForUpdatesRequested, OnConnectRequested, etc.), making the symbol a single integration point for user interactions. The post-creation mouse-transparency hack fixes input handling in the CommandView so user clicks reliably hit the intended MenuBarItem. +### ClearAll +> **File:** `src/EchoHub.Client/UI/MainWindow.cs` +> **Kind:** method + +```csharp +public void ClearAll() +``` + +**Returns:** `void` + + +Resets the chat state and UI to a clean slate when disconnecting. It clears all channel-related data (`_channelNames`, `_channelTopics`, `_channelPublic`, `_channelProtected`, `_systemChannels`) and the messages via `_messageManager.ClearAll()`, resets the channel and user lists (`_channelListSource`, `_channelList`, `_usersListSource`, `_usersList`), and restores the UI to its initial layout (title set to `Chat`, topic label hidden, frame Y reset, and user frame titled `Users`). Finally, it calls `RefreshMessages()` to purge any displayed content. + +## Remarks +Centralizes cleanup logic for the chat UI, reducing the risk of inconsistent state when disconnecting. By coordinating both in-memory data and their visual bindings, it guarantees the UI starts from a known baseline on the next connection. ## Notes +- This method should only be invoked during disconnect; calling it during an active session will wipe the current UI state. +- Assumes UI thread context since it manipulates UI elements like `_chatFrame`, `_topicLabel`, and `_usersFrame`. + +--- + +### ClickChannelRegex +> **File:** `src/EchoHub.Client/UI/MainWindow.cs` +> **Kind:** method + +```csharp +[GeneratedRegex(@"(? **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** method ```csharp -public void ClearAll() +[GeneratedRegex(@"(? GetChannelNames() => _channelNames.AsReadOnly() **Returns:** `IReadOnlyList` -Returns an `IReadOnlyList` containing the names of all channels that have message buffers, which are used to broadcast status changes. This method is typically used when a caller needs to enumerate the channels to notify about status updates without taking ownership of or mutating the internal collection. +This method returns the set of channel names that have message buffers for broadcasting status changes. It provides a read-only view of the internal `_channelNames` collection, so callers can enumerate available channels without mutating the underlying data. ## Remarks -By wrapping the internal list with AsReadOnly, this method preserves encapsulation: callers can observe which channels exist without gaining permission to modify the collection. It also centralizes how channel names are exposed, so changes to the underlying storage need only be updated here. If the set of channels changes over time, the returned view will reflect those changes; if a stable, unchanging snapshot is required, consider copying the values to a new list. +This method serves as a safe, read-only projection of the internal channel registry used for status broadcasts. By returning `IReadOnlyList` via `_channelNames.AsReadOnly()`, it preserves encapsulation while letting callers enumerate available channels. Because it is a live view of the underlying collection, any subsequent changes to `_channelNames` will be reflected in the returned sequence; if a stable snapshot is required, callers should materialize a copy at the point of use. ## Notes -- The returned view is not a deep freeze; it's a live wrapper over the internal list. For a stable snapshot, copy to a new list. +- The returned `IReadOnlyList` is a live view into `_channelNames`; it does not copy elements, so mutations to the underlying list will be visible to callers. + --- @@ -495,15 +544,15 @@ private static void GuardedClipboardAction(Action action, string operation) **Returns:** `void` -GuardedClipboardAction executes the supplied edit action and swallows transient clipboard failures by catching all exceptions and logging a warning that the clipboard operation failed, including the operation name. Use it when performing clipboard-related edits to prevent OS clipboard contention from propagating to the input loop, keeping the application responsive even if the clipboard is momentarily unavailable. +The `GuardedClipboardAction` method executes the provided `Action` to drive a clipboard-backed edit while swallowing transient clipboard failures that would otherwise bubble up from the input loop and crash the app. If the action throws an exception, it is caught and a warning is logged via `Log.Warning` with the operation name, after which execution continues. ## Remarks -GuardedClipboardAction encapsulates the resilience policy for clipboard interactions, separating error handling from the business logic that mutates the clipboard. By treating clipboard failures as non-fatal, it reduces boilerplate at call sites and provides a consistent user experience when the clipboard is busy or locked by another process. + +This function acts as a resilience boundary around clipboard edits, ensuring that transient OS clipboard contention does not destabilize the UI input loop. By centralizing this behavior, all clipboard-backed actions share consistent error handling and telemetry through the `Log` dependency. ## Notes -- It catches all exceptions, potentially hiding bugs if used in contexts where failures should propagate. -- No retry logic is performed; if a retry is desired, implement it at the call site or extend this helper. -- Logs a warning with the operation name to aid debugging without interrupting the user flow. + +- Broadly catching `Exception` may hide non-transient failures; if you rely on exceptions for debugging, consider narrowing the catch or rethrowing critical exceptions. --- @@ -524,21 +573,18 @@ private void MentionUser(string username) **Returns:** `void` -MentionUser inserts a user mention into the chat composer by placing "@{username} " at the current cursor position and then returning focus to the input field. Use it when the UI needs to prefill a mention (for example after the user selects a contact) so the user can continue typing immediately with a properly formatted mention. +In the chat UI, `MentionUser` inserts a properly formatted user mention into the input field and then returns focus to the input control. It accomplishes this by inserting the text starting with an at-sign followed by the provided `username` and a trailing space via `_inputField.InsertText`, and then calling `_inputField.SetFocus` to restore typing context. ## Remarks -By centralizing the mention formatting (leading '@' and trailing space) and focus restoration, this method ensures all mentions are inserted consistently across the chat UI. It depends on the _inputField control, so it should be invoked on the UI thread where that control exists. +This method encapsulates a small piece of UI behavior: it formats a username into a chat mention and ensures the input remains focused after insertion. Centralizing this logic avoids duplication at call sites and makes it easy to adjust how mentions are presented or how focus is managed in the future. ## Example ```csharp -// When a user is selected for a mention -MentionUser("alice"); +MentionUser("Alice"); ``` ## Notes -- No validation or escaping is performed; the value is inserted as-is after '@'. -- Should be called on the UI thread when the input field is accessible; otherwise UI interactions may fail. -- Assumes _inputField is non-null; if not, it may throw. +- Assumes `_inputField` is non-null and that these calls occur on the UI thread; otherwise, this method may throw or fail to update focus. --- @@ -560,15 +606,13 @@ private void OnChannelListSelectionChanged(object? sender, ValueChangedEventArgs **Returns:** `void` -Handles the channel list selection change by reading the new index from the event args, validating that it points to a valid channel, and switching to that channel only if it's not already the current channel. It then raises OnChannelSelected with the chosen channel name to propagate the change. +Handles changes to the channel list selection in the UI. When the user selects a different channel, this method validates the new index, resolves the corresponding channel name from `_channelNames`, and, if it differs from the current channel exposed by `_messageManager.CurrentChannel`, switches to that channel by calling `SwitchToChannel` and notifies subscribers via `OnChannelSelected`. ## Remarks -Serves as the UI-to-state bridge for channel navigation. It centralizes the validation of the selection index and guards against unnecessary channel switches, ensuring the application only changes channels when asked by the user. It relies on the _channelNames collection as the authoritative mapping from list indices to channel identifiers, and on _messageManager.CurrentChannel to determine whether a switch is needed. By publishing the new channel through OnChannelSelected, other components can react (e.g., updating status, logging, or triggering related UI updates) without directly coupling to the list control. +By centralizing the UI-to-channel-switch logic, this symbol serves as the single decision point for user-driven channel changes, keeping UI concerns separate from channel management. It avoids unnecessary work by only performing a switch when the new channel is different from the current one and by emitting `OnChannelSelected` to any interested listeners. This method relies on the integrity of `_channelNames` and `_messageManager.CurrentChannel` and assumes the UI selection reflects the latest channel list. ## Notes -- Ensure _channelNames stays in sync with the channels presented in the UI; a mismatch can lead to incorrect mappings or no operation. -- The method assumes _messageManager.CurrentChannel reflects the current channel state; if not, behavior may not switch as expected. -- The OnChannelSelected event is invoked using the null-conditional operator, so there may be zero subscribers without causing a crash. +- This method assumes `_channelNames` is non-null and synchronized with the UI list. If `_channelNames` can be null or updated concurrently, this handler may throw or behave inconsistently; ensure proper initialization and synchronization. --- @@ -583,24 +627,14 @@ private void OnChatViewportChanged() **Returns:** `void` -The method reacts to changes in the chat viewport width by reading the current width from the chat list’s viewport. If the width is positive and differs from the last recorded width, it updates the cached value, propagates the new width to the message manager, and refreshes the messages to align the display with the new size. +When the chat viewport width changes, `OnChatViewportChanged` reads the current width from `_messageList.Viewport.Width`, and if the width is positive and differs from `_lastChatWidth`, it updates `_lastChatWidth`, applies the new width to `_messageManager` via [`SetChatWidth`](Chat/ChatMessageManager.cs.md), and refreshes the messages with `RefreshMessages()`. ## Remarks - -This is a focused resize handler that isolates width-change logic from general rendering. By guarding against no-ops (width <= 0 or unchanged width) it avoids unnecessary layout work and keeps the chat display in sync with the viewport through the message manager and a refresh cycle. It relies on the UI-related components (_messageList, _messageManager, and RefreshMessages) and should be invoked within the appropriate UI thread context. - -## Example - -```csharp -// Common case: the chat viewport has been resized to a new positive width -OnChatViewportChanged(); -``` +This method encapsulates the UI's width-responsive behavior for the chat area. By guarding on positive, changed widths, it avoids unnecessary reflows and redraws, delegating the actual rendering adjustments to `_messageManager` and `RefreshMessages()`. ## Notes - -- The method only acts when newWidth > 0 and newWidth != _lastChatWidth, preventing redundant work on unchanged sizes. -- It updates internal state before triggering a layout refresh, ensuring subsequent calls see the updated width. -- Since it touches UI-related components, ensure invocation occurs on the UI thread to avoid cross-thread access issues. +- Guard against zero widths to prevent wasted work during startup or transient layout passes. +- Ensure `_lastChatWidth` is initialized appropriately so the first meaningful width change triggers an update. --- @@ -621,17 +655,16 @@ private void OnHistoryPrepended(string channelName) **Returns:** `void` -Maintains the user's reading position when historical messages are prepended to the current channel's message list. If the incoming channel matches the currently displayed channel, it loads the channel's messages, records how many items were visible before the refresh, refreshes the list, computes how many items were prepended, and, if any, selects the item at that offset to keep the view from jumping to the top. This behavior helps the user remain oriented while older messages are loaded above the current view. +OnHistoryPrepended is a UI helper invoked when older messages are prepended to the chat history for a channel. It only runs for the currently active channel, fetches that channel's messages, and if there are messages to process, it refreshes the message list and then reselects the item that now sits at the top due to the prepend. This preserves the user’s reading position, so they don’t scroll to the newest messages just because history was loaded. ## Remarks - -This method exists to decouple the UI from the underlying data refresh when history is added to the top of the chat. By capturing the pre-refresh count and re-selecting based on the number of newly prepended items, it preserves the visual position in the list and avoids a disruptive jump to the beginning. It relies on the current channel check, the ChatListSource.Count, and the SelectedItem property to compute and apply the offset during the refresh cycle. +This method acts as a focused UX shim around the history-prepend workflow. By capturing the count of items before and after `RefreshMessages()` and then assigning `_messageList.SelectedItem` to the delta, the view stays anchored to the same top-most message despite the updated list. The logic is deliberately scoped to the current channel and the [`ChatListSource`](Chat/ChatListSource.cs.md) used by the UI list; if those assumptions fail, the method exits or does not adjust the selection, preventing misalignment. ## Notes +- It only executes when `channelName` matches `_messageManager.CurrentChannel`; otherwise it returns immediately. +- It relies on `_messageList.Source` being a [`ChatListSource`](Chat/ChatListSource.cs.md) to compute counts; if not, the old count defaults to 0, which can affect the delta calculation. +- The selection repositioning uses the delta (`prependedCount`) as the new `SelectedItem`; changes to how the list interprets `SelectedItem` could alter the visual scroll behavior if the UI contract changes. -- Guard clauses ensure the method is a no-op when the channelName is not the current channel or when there are no messages for the channel. -- The offset calculation assumes RefreshMessages updates the list by prepending items above the existing ones; if the underlying data source changes differently, the preserved position may be off by one or more items. -- This is an internal handler; external callers should not invoke it directly. --- @@ -653,7 +686,10 @@ private void OnInputContentsChanged(object? sender, ContentsChangedEventArgs e) **Returns:** `void` -OnInputContentsChanged handles content changes in the main window's input field. It respects a guard (_suppressEmojiReplace) to avoid re-entrant edits caused by programmatic text changes. If the current input text appears to be a dropped file path and DroppedFileParser can resolve one or more files, and there is a non-empty _messageManager.CurrentChannel, it clears the input and stages the discovered files for sending instead of leaving the raw path text to be dispatched. Otherwise, it runs EmojiHelper.ReplaceEmoji to substitute emoji sequences. If no replacement occurs, the method returns. If replacements occur, it computes the delta in length, repositions the cursor to the corresponding column after the edit, and updates the input field accordingly, wrapping the change in a guard to re-enable emoji processing afterwards. +OnInputContentsChanged is a private event handler that runs when the main input field's contents change. It normalizes user input by (1) detecting a dropped file path that resolves to files and staging them via `StageFiles` instead of sending the raw path, and (2) performing emoji substitutions with `EmojiHelper.ReplaceEmoji`, updating the input and cursor position when needed; all updates are guarded by `_suppressEmojiReplace` to avoid recursive edits. + +## Remarks +This method centralizes input normalization at the UI boundary: it both interprets dropped files and performs emoji normalization, so callers need not handle these concerns separately. It coordinates [`DroppedFileParser`](Helpers/DroppedFileParser.cs.md) and [`EmojiHelper`](Helpers/EmojiHelper.cs.md) to convert user input into the appropriate sent form while preserving the user's cursor position, and uses `_suppressEmojiReplace` to avoid re-entrant updates caused by programmatic text changes. --- @@ -675,17 +711,7 @@ private void OnInputKeyDown(object? sender, Key e) **Returns:** `void` -OnInputKeyDown is the keyboard shortcut handler for the chat message input area. It interprets specific keys to drive UI behaviors such as autocompletion, reply cancellation, message submission, and clipboard-based attachments, ensuring keyboard users can perform common actions without leaving the input context. - -Shortcuts supported include Tab for autocompletion; Esc to cancel a pending reply (when such a reply exists); Newline to insert a line break; Enter to submit the message when there is content or staged attachments and there is a valid channel; Alt+Q to quit; Ctrl+K to open the search dialog; F6 to focus the message list; Ctrl+V/Ctrl+Y to paste, with clipboard-aware behavior: paste files from the clipboard as attachments, paste PNG image data as an image, or fall back to a normal text paste; Ctrl+X to cut; Ctrl+C to copy. Unrecognized keys are ignored and the event is left unhandled. - -## Remarks -OnInputKeyDown centralizes input-related keyboard interactions for the main window, so the user experience remains consistent between typing, editing, and navigation. It delegates the actual actions to collaborators (the input field, the message manager, and the event publishers like OnMessageSubmitted and OnImagePasted), keeping key handling isolated from business logic. It also respects channel permissions by bypassing text-entry actions when the current channel is read-only, preventing invalid edits. - -## Notes -- Enter submission guard: the message is submitted only if there is text (after trimming) or there are staged attachments, and the current channel is non-empty. -- Clipboard behavior: Ctrl+V/Ctrl+Y first try to stage clipboard files as attachments; if none are files, they attempt to paste PNG image data from the clipboard; if neither applies, they fall back to a standard text paste via the input field. -- Read-only channels: text entry and attachment staging are skipped when the channel is read-only, ensuring the UI adheres to channel permissions. +OnInputKeyDown processes key presses in the message-entry area and dispatches the appropriate actions for common chat shortcuts, such as autocompletion (`TabKey`), canceling a pending reply (`Esc` when `HasPendingReplyIndicator` is true), inserting a newline (`NewlineKey`), and submitting a message (`EnterKey`) when the channel is writable and there is text or staged attachments. It also handles app-level commands (`AltQKey`), opening the search dialog (`CtrlKKey`), and focus management (`F6Key`) to move focus to the message list. It additionally implements clipboard-based behavior for paste (`CtrlVKey`, `CtrlYKey`), cut (`CtrlXKey`), and copy (`CtrlCKey`), including attachment staging and image pasting via [`ClipboardFiles`](../Services/ClipboardFiles.cs.md) and [`ClipboardImage`](../Services/ClipboardImage.cs.md). Clear coordination with `_inputField`, `_messageManager`, and events like `OnMessageSubmitted` and `OnImagePasted` is essential to keep the chat input responsive and consistent across channels. --- @@ -708,15 +734,7 @@ private void OnMessageListAccepting(object? sender, CommandEventArgs e) **Returns:** `void` -Activates the selected chat line in the message list, validating the source and index, then dispatching a context-appropriate action based on what the line represents. It first resolves quote targets for replies, then handles attachments (audio, file, or image), detects @mentions and #channels within the line text, and finally falls back to opening the sender's profile. Each path marks the event as handled to stop further processing. - -## Remarks -All user interactions for a list entry funnel through this method, providing a single, predictable entry point for keyboard or programmatic activation. It keeps UI concerns decoupled from the data model by routing actions through dedicated callbacks (OnAudioPlayRequested, OnFileDownloadRequested, OnImageOpenRequested, OnUserProfileRequested, OnChannelJoinRequested) and by inspecting ChatLine properties such as AttachmentKind, JumpToMessageId, and SenderUsername. This design minimizes scattered conditional logic across the UI layer and centralizes the decision-making about what happens when a line is activated. - -## Notes -- Early returns guard invalid state (null source, invalid index) and keep no-op paths contained. -- Attachment priority: if an attachment exists, media actions take precedence over textual interactions (mentions/channels). -- Fallback behavior: if no attachment, mention, channel, or sender username applies, the method completes without invoking any callbacks. +OnMessageListAccepting is a private event handler that runs when the user activates a selected item in the chat message list. It first validates that the list's `Source` is a [`ChatListSource`](Chat/ChatListSource.cs.md), then obtains the currently selected line via `GetLine` and guards against nulls or invalid indices. Depending on the line's state, it dispatches the appropriate action: if the line exposes a `JumpToMessageId`, it calls `ScrollToMessage(jumpTarget)` and marks the event as handled (`e.Handled = true`); if the line has an attachment (both `AttachmentUrl` and `AttachmentFileName` non-null), it prioritizes the attachment type by invoking `OnAudioPlayRequested` for audio, `OnFileDownloadRequested` for files, or `OnImageOpenRequested` for images, each time setting `e.Handled` to true. If no attachment applies, the method converts the line to text and checks for an `@mention` using `ClickMentionRegex()`; on success it requests the mentioned user’s profile via `OnUserProfileRequested`. It then checks for a channel reference with `ClickChannelRegex()` and, on success, requests joining that channel via `OnChannelJoinRequested`. If none of the above apply, it falls back to opening the sender’s profile if a `SenderUsername` is present, again setting `e.Handled` to true for the activation. --- @@ -738,7 +756,15 @@ private void OnMessageListKeyDown(object? sender, Key e) **Returns:** `void` -This event handler processes keyboard input for the chat message list. It provides two primary shortcuts: F6 moves focus back to the input box to enable quick replies, and Delete or Backspace initiates the deletion flow for the currently selected message. If F6 is pressed, focus is transferred to the input field and the event is marked as handled. If Delete or Backspace is pressed, the code first validates that the message list source is a ChatListSource, that a valid item is selected, and that the selected line exposes a MessageId. When these conditions are met, the handler invokes ConfirmDeleteMessage with the message's ID and marks the event as handled, delegating the actual permission check to the server. The server enforces real permissions (own message or Mod+ over a lower role); the client simply confirms intent and relies on the server to reject disallowed actions. +OnMessageListKeyDown is a private key-down handler for the message list. It focuses the input when the user presses `F6Key` and, for `Key.Delete.KeyCode` or `Key.Backspace.KeyCode`, initiates a delete for the currently selected message by obtaining its `MessageId` from the line at the current index and calling `ConfirmDeleteMessage`, with the server enforcing permission and the client only signaling intent. + +## Remarks +This symbol centralizes keyboard interactions for the message list so common shortcuts translate into UI and server actions rather than scattered ad-hoc logic. It guards against invalid states by ensuring the source is a [`ChatListSource`](Chat/ChatListSource.cs.md), that there is a valid selection within bounds, and that the retrieved line contains a `MessageId` before invoking `ConfirmDeleteMessage`. The actual permission check occurs on the server; the client simply signals intent when the user presses delete-related keys. + +## Notes +- If the currently selected line cannot provide a `MessageId`, the delete flow is bypassed and nothing is sent to the server. +- The handler marks the event as handled for both focus and deletion-related keys to prevent default behavior and potential duplicate processing. +- This is a UI-level interception; changes to the underlying [`ChatListSource`](Chat/ChatListSource.cs.md) shape or the line retrieval API may require corresponding updates to maintain the delete flow. --- @@ -760,15 +786,15 @@ private void OnMessageListMouseEvent(object? sender, Mouse e) **Returns:** `void` -Handles mouse interactions for items in the chat message list. The method processes left- and right-clicks to either activate attachment actions on a line or present a context menu; if the click doesn't map to a known action, it returns without side effects. It determines the targeted line from the list's TopItem and the mouse position, validates bounds, and, for left-button clicks, dispatches OpenImage or SaveOriginal actions when the click occurs on a matching ActionSpan with a non-null AttachmentUrl and AttachmentFileName. It invokes OnImageOpenRequested(url, name) when the Action is OpenImage, otherwise OnImageSaveRequested(url, name). It then marks the event as handled and returns. For right-clicks, it selects the row, focuses the list, and shows the message context menu at the screen position. If none of these conditions apply, the method simply returns, leaving other listeners to handle the event as appropriate. +This private event handler processes mouse input from the chat message list (`_messageList`). It distinguishes left-clicks from right-clicks and ignores other inputs. For a left-click performed on a line that contains an attachment action span and provides an `AttachmentUrl` and an `AttachmentFileName`, it dispatches the corresponding operation by invoking `OnImageOpenRequested` or `OnImageSaveRequested` with the `url` and `name`, and marks the event as handled. For a right-click, it selects the clicked row, focuses the list, and shows the message context menu via `ShowMessageContextMenu`. ## Remarks -By centralizing this logic in OnMessageListMouseEvent, the UI separates interaction details from the concrete actions (open/save). The event-based callbacks (OnImageOpenRequested, OnImageSaveRequested) enable the host to implement corresponding behavior without the control needing to know how to present or fetch attachments. It coordinates among the message list, its source lines, and the context menu, ensuring left-clicks only trigger attachment-related actions and right-clicks prepare the selection and menu. +Separation of concerns: UI input handling is isolated here, mapping raw mouse events to high-level actions (attachment operations or context-menu invocation). It coordinates with the chat line data (via `AttachmentUrl`, `AttachmentFileName`, and `ActionSpans`) and with the event callbacks (`OnImageOpenRequested`, `OnImageSaveRequested`) to perform the appropriate operation. ## Notes -- Left-click triggers only when the target line has both an AttachmentUrl and an AttachmentFileName and the click lies within an ActionSpan; otherwise the method returns without performing the action. -- When an action path is taken, OnImageOpenRequested or OnImageSaveRequested is invoked with the attachment's URL and file name, and e.Handled is set to true to suppress further processing. -- Right-click path selects the row, focuses the list, and shows the context menu; the event is marked as handled in those cases. +- Left-click path only triggers an action when the click lies within an `ActionSpan` for the line; otherwise, the code returns and preserves normal selection behavior. +- Right-click path selects the row and shows the context menu, enabling per-row actions. +- All action branches set `e.Handled = true` to prevent further processing and ensure predictable UI behavior. --- @@ -790,16 +816,14 @@ private void OnMessageListVerticalScrollBarScrolled(object? sender, EventArgs channels) **Returns:** `void` -SetChannels takes a list of ChannelDto, clears the local channel state, fills in names, topics, and visibility, and marks protected and system channels, before refreshing the channel list UI. Use this when you receive a full snapshot of channels from the server and want the UI to reflect that snapshot in one operation rather than mutating individual channels. +Rebuilds the internal channel state from the provided `List` and refreshes the channel list UI. It clears the internal collections `_channelNames`, `_channelTopics`, `_channelPublic`, `_channelProtected`, and `_systemChannels`, then populates them from each [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) in `channels` (adding `ch.Name` to `_channelNames`, setting `_channelTopics[ch.Name]` to `ch.Topic`, and `_channelPublic[ch.Name]` to `ch.IsPublic`). If a channel is protected or system, it additionally tracks those names in `_channelProtected` and `_systemChannels`, respectively, before finally calling `RefreshChannelList()`. ## Remarks -By converting the ChannelDto data into the UI state, this method isolates the UI from the raw DTOs and centralizes the mapping logic. It ensures a consistent representation of channel topics and visibility, while grouping channels by protected or system roles to simplify downstream behavior. +Bulk-replaces rather than incrementally updating; this is intended when the server-provided channel list changes in full. By consolidating all channel attributes (topic, visibility, and category flags) under the channel name key, it keeps the UI and internal views consistent with a single refresh. ## Notes -- Null input will cause an exception; callers should pass an empty list or validate before calling. -- Assumes channel names are unique identifiers; duplicate names will create duplicates in _channelNames and may overwrite entries in _channelTopics or misclassify in protected/system lists. +- Assumes `channels` is non-null and that all channel names are unique; nulls or duplicates can cause runtime errors or inconsistent state because the method does not validate input. --- @@ -1133,14 +1114,10 @@ public void SetCurrentUser(string username) **Returns:** `void` -Sets the current user name and delegates to the message manager for @mention detection. Use this when the active user changes (for example, after login or user switch) so that subsequent messages and mentions are evaluated against the correct user. +Updates the active user name by forwarding the provided `username` to the message manager's `SetCurrentUser` method, enabling proper @mention detection. This wrapper lets the UI layer update the current user without needing to know how the message manager stores or uses the username. ## Remarks -This method is a thin wrapper around _messageManager.SetCurrentUser; it centralizes the UI's interaction with the messaging subsystem and keeps the rest of the UI agnostic about how mentions are detected. By routing the update through the message manager, changes to mention resolution are applied consistently across the system and encapsulated in a single component. In practice, the UI can continue to call SetCurrentUser without needing to know about the underlying messaging implementation. - -## Notes -- No input validation is performed here; ensure the provided username meets the expectations of the message manager and is non-null. -- As a pass-through, any validation or side effects originate from _messageManager; callers should be prepared to handle its behavior. +Because this symbol is a forwarding wrapper, it preserves a clean separation of concerns: the UI layer remains decoupled from the internal mention-processing logic, with the `_messageManager` handling the actual behavior. It centralizes the current-user context in a single component, making future changes to mention handling easier to apply without touching UI code. --- @@ -1161,23 +1138,7 @@ public void SetReplyingTo(string? label) **Returns:** `void` -Shows/clears the tiny contextual indicator in the input frame's title that reflects the current reply target. When you pass a non-null label, the title displays a fragment like "↩ Replying to {label} │ Esc=cancel" to provide context for the reply; passing null clears the indicator. After computing the fragment, the method calls UpdateInputTitle() to refresh the UI immediately so the change is visible to the user. - -## Remarks - -By centralizing this UI state, SetReplyingTo encapsulates the presentation detail of the reply workflow. It separates the action of initiating a reply from the mechanics of rendering the updated title, and ensures a consistent reply-indicator string is used across the input area. - -## Example - -```csharp -// Within the same class context -SetReplyingTo("Alice"); -SetReplyingTo(null); -``` - -## Notes - -- This method mutates UI-related state and should be invoked on the UI thread to avoid cross-thread access issues when updating the window chrome. +SetReplyingTo updates the input frame's title to reflect the message you're replying to. When a non-null `label` is supplied, it builds the title fragment ``↩ Replying to {label} │ Esc=cancel`` and stores it in the internal field ``_replyTitleFragment``; passing `null` clears the indicator. It then refreshes the UI by calling ``UpdateInputTitle()``. --- @@ -1202,10 +1163,18 @@ public void SetStagedAttachments(IReadOnlyList fileNames, string asciiSi Updates the attachment staging indicator shown on the input frame's title, including the current ASCII-art size for images. Passing an empty list restores the default hint. ## Remarks -Centralizes attachment-state presentation in the input UI, so the title reflects what is staged without scattering formatting logic across callers. It updates a staged flag and builds a compact label that shows the count, a short preview of names, and the ASCII size label, then refreshes the title via UpdateInputTitle(). This makes it easy to adjust formatting (e.g., truncation length) in one place while keeping the input frame in sync. +This method centralizes the UI logic for reflecting the current set of staged attachments in the input title, ensuring callers don't assemble the title text themselves. It constructs a compact label beginning with the 📎 emoji, followed by the count and a comma-separated list of filenames (truncated when too long), then the ASCII-size hint and quick actions. After updating `_stagedTitleFragment`, it calls `UpdateInputTitle()` to refresh the on-screen title. + +## Example +```csharp +// Example: attach two files and display their names with a 128x64 ASCII size hint +SetStagedAttachments(new[] { "image1.png", "image2.png" }, "128x64"); +``` ## Notes -- The method assumes fileNames is non-null; passing null will throw a NullReferenceException when accessing Count. +- If the joined filenames exceed 45 characters, they're truncated to preserve layout, appending '...'. +- Passing an empty list resets the hint by clearing `_stagedTitleFragment` and updating the title. +- This method updates UI state and should be invoked on the UI thread where the input frame lives to avoid cross-thread issues. --- @@ -1226,20 +1195,13 @@ public void ShowError(string message) **Returns:** `void` -Shows an error message to the user by presenting a modal dialog with the title 'Error' and the provided message. It delegates to MessageBox.ErrorQuery to render the dialog, using the current application context (_app) to display the UI. This lightweight wrapper ensures a consistent, centralized way to surface user-facing errors across the EchoHub client UI. +ShowError is a small UI helper that presents an error dialog to the user by calling `MessageBox.ErrorQuery(_app, "Error", message, "OK")`. Use this method when you want to surface a user-facing error with consistent styling and boilerplate centralized in one place. ## Remarks -By consolidating error presentation here, callers don't need to know about the underlying message box details; the wrapper encapsulates the UI intent. This helps ensure a consistent dialog structure (title 'Error', an 'OK' button) while leaving room to enhance behavior later (e.g., localization, logging) without touching every call site. - -## Example -```csharp -ShowError("Unable to load user data."); -``` +This wrapper centralizes error presentation, enforcing a consistent user experience by always using the same dialog title and button label via `MessageBox.ErrorQuery(_app, "Error", message, "OK")`. It also isolates UI-dialog boilerplate so changes to the underlying dialog surface can be made in one place without changing call sites. It assumes `_app` is a valid UI context; if `_app` is null or the call is made on a non-UI thread, it can fail or cause exceptions. ## Notes -- Ensure _app is non-null and this call runs on the UI thread. -- This method is synchronous and blocks until the user dismisses the dialog; avoid calling from long-running background tasks. - +- Requires a valid UI thread context; ensure `_app` is initialized before calling. --- @@ -1261,16 +1223,8 @@ private void ShowMessageContextMenu(ChatLine line, System.Drawing.Point screenPo **Returns:** `void` -It builds and shows a per-message right-click context menu for a chat line at a given screen position, combining attachment actions, reply/mention/profile options, copy operations, and a delete action subject to server-side permissions. Use it to provide a consistent, feature-rich set of line-specific actions when a user invokes the context menu on a message line (e.g., via right-click). +Shows a right-click context menu for a chat line, consolidating attachment actions, reply handling, mention/profile commands, text copying, and deletion with server-side permission checks. The method builds the menu by inspecting the [`ChatLine`](Chat/ChatLine.cs.md) for an attachment (its [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md), `AttachmentUrl`, and `AttachmentFileName`), the presence of a sender, and whether the line is a reply target (`MessageId`). It then populates a `PopoverMenu` with `MenuItem`s such as `Open image`, `Save original image`, `Play audio`, `Download file`, `Reply`, `Mention @sender`, `View {sender}'s profile`, `Copy text`, `Copy message ID`, and `Delete message` (the latter gated by the existence of `MessageId`). Actions are wired to events like `OnImageOpenRequested`, `OnImageSaveRequested`, `OnAudioPlayRequested`, `OnFileDownloadRequested`, `OnReplyRequested`, `OnUserProfileRequested`, and helper calls such as `CopyToClipboard` and `ConfirmDeleteMessage`. The UI is shown via `_app.Popovers` by registering the menu and displaying it at the given `screenPosition`. -## Remarks -This method centralizes the per-line action surface for chat messages, deriving available options directly from the message state. It conditionally adds actions based on the presence and type of attachments, existence of a sender, and whether the message can be replied to or deleted. By assembling a flat list of MenuItem actions and delegating to a PopoverMenu, it decouples the action presentation from the rest of the UI and ensures a uniform user experience across message kinds. The method also demonstrates how user interactions trigger higher-level callbacks (e.g., opening attachments, replying, mentioning, viewing profiles, copying text, or requesting deletion) while managing UI focus and popover lifecycle. - -## Notes -- The context menu is short-circuited if there are no actionable items, avoiding an empty popover. -- Reply content is derived by stripping the leading header from the line's textual representation; if the header pattern is absent, the full line is used. -- Delete actions route through ConfirmDeleteMessage, reflecting server-side permission checks rather than performing client-side deletion. -- Attachment-related actions are selected based on AttachmentKind, with sensible defaults for unknown kinds. --- @@ -1286,14 +1240,15 @@ private void ShowSearchDialog() **Returns:** `void` -Signals the UI to display the search dialog by invoking OnSearchRequested, but only if there are subscribers. This private helper is called when a search action is requested, decoupling the action from the actual dialog presentation and allowing the UI to respond without the ShowSearchDialog method needing to know how the dialog is shown. +ShowSearchDialog is a private helper that triggers the search UI by raising the `OnSearchRequested` event. It encapsulates the mechanism of opening a search dialog so UI controls can simply invoke it without depending on a concrete dialog implementation. ## Remarks -This method acts as a small abstraction layer between the user action and the dialog presentation. By centralizing the emission of the OnSearchRequested event, the MainWindow class remains focused on orchestration rather than UI rendering specifics. Subscribers can provide or adjust the search behavior without modifying callers, aiding testability and future UI changes. + +By funneling the open-search action through this private method, the class remains decoupled from how the search dialog is presented. The event-driven approach lets any subscriber decide how to respond to a search request, enabling easier testing and customization. The null-conditional invocation (`OnSearchRequested?.Invoke()`) ensures a safe no-op when no components are listening, avoiding the need for explicit subscriber checks in call sites. ## Notes -- The invocation is synchronous on the calling thread; a long-running event handler will block the caller unless it offloads work. -- The method does not itself create or show any UI; it merely signals interested parties via OnSearchRequested. + +- If no subscribers exist for `OnSearchRequested`, this method does nothing, which is a deliberate no-op. Calling code should subscribe to the event if it needs a visible search UI. --- @@ -1314,15 +1269,14 @@ private void StageFiles(IReadOnlyList files) **Returns:** `void` -Stages the specified files as a single batch of attachments for the active channel. If there is no current channel, the method exits without staging. When a channel exists, it triggers the OnFilesStaged event to hand off the batch to the sending workflow; the next Enter press will transmit these attachments together with any caption the user has typed. +Staging files (from a drop or a file-clipboard paste) as attachments in one batch; the next Enter sends them with any typed caption. The method retrieves the current channel from `_messageManager.CurrentChannel` and, if a channel is present, notifies listeners by invoking `OnFilesStaged` with the channel and the provided file paths; if there is no active channel, it returns without action. ## Remarks -StageFiles acts as a tiny adapter between the user action (dropping or pasting files) and the message-sending pipeline. By emitting OnFilesStaged instead of performing the send itself, it keeps the UI concerns separate from transport mechanics and allows multiple components to react to the staging event. The method relies on the presence of a current channel to decide whether staging is meaningful, embodying a guard that prevents accidental attachment uploads when not in a channel. +`StageFiles` serves as a small adapter between the UI action of dropping or pasting files and the sending workflow. By emitting `OnFilesStaged`, it decouples the staging concern from the actual send operation, allowing different parts of the UI or logic to respond to staged files without the method needing to know what happens next. ## Notes -- If there is no active channel, this method is a no-op. -- OnFilesStaged is invoked only when there is a channel; if there are no subscribers, nothing happens. -- There is no in-method validation of file paths; validation is delegated to downstream handlers. +- No action occurs if there is no active channel (`_messageManager.CurrentChannel` is null or empty). +- The invocation uses `OnFilesStaged?.Invoke(channel, files)`; if there are no subscribers, nothing happens without throwing. --- @@ -1343,7 +1297,7 @@ public void SwitchToChannel(string channelName) **Returns:** `void` -SwitchToChannel switches the chat view to the specified channel. It updates the underlying message state to reflect the new current channel, updates the chat window title to show the channel (prefixed with a #), clears the unread count for that channel, and triggers a sequence of UI refreshes to keep the display in sync: the channel list, the displayed messages, the topic bar, and the input’s read-only state. It also marks the status area for redraw and, if the channel exists in the known list, selects it in the channel list to align the selection with the active channel. +SwitchToChannel switches the chat view to the specified channel by name, resets its unread count, and refreshes related UI so the active channel is clearly reflected to the user. It updates the underlying current channel in the `_messageManager`, updates the chat window title to the channel header prefixed with a hash (`#` + channelName), clears unread markers for that channel via `_messageManager.ClearUnread(channelName)`, refreshes the channel list and messages, updates the topic bar and input state, redraws the status label, and aligns the channel list selection when the channel exists in `_channelNames` by setting `_channelList.SelectedItem` to the channel's index if found. --- @@ -1358,13 +1312,7 @@ public void ToggleUsersPanel() **Returns:** `void` -ToggleUsersPanel flips the panel visibility flag and then refreshes the layout to reflect the new state. This method is typically bound to the F2 keyboard shortcut, enabling users to quickly show or hide the online users panel without interacting with UI controls. - -## Remarks -Encapsulating the state change and the layout refresh in a single method keeps the UI logic cohesive and discoverable. Callers can rely on this method to perform the complete show/hide action, rather than mutating internal fields directly, which helps prevent inconsistent presentation. If the binding for F2 changes, the toggle behavior remains centralized here. - -## Notes -- Ensure calls occur on the UI thread; invoking from a background thread may require marshaling to the UI thread before touching UI state. +`ToggleUsersPanel` flips the private field `_usersPanelVisible` to its opposite value and then calls `UpdateLayout()` to refresh the UI accordingly. It is typically invoked in response to the user pressing the `F2` key to show or hide the online users panel. --- @@ -1379,15 +1327,7 @@ private void TryAutocompleteCommand() **Returns:** `void` -Tab-complete slash commands in the input field. This method provides a lightweight command-entry UX by auto-completing a slash command when the user types a leading slash with no spaces, using the available SlashCommands list. If exactly one match exists, it completes to that command plus a trailing space; if multiple matches exist, it computes the longest common prefix among them and applies it when it extends beyond the current input. Finally, it moves the caret to the end of the input to prepare for continued typing. - -## Remarks -Provides an in-place UX enhancement for command entry; it reads from the in-memory SlashCommands collection and does not trigger any external calls. It treats matches case-insensitively and updates the input field accordingly, leaving the user to continue typing after the completion. - -## Notes -- Mutates only when the input begins with '/' and contains no spaces; otherwise it exits without changes. -- When multiple matches exist, the prefix calculation starts from the first match in the list; the resulting auto-prefix can depend on the list ordering. -- The caret is always moved to the end of the text, regardless of whether any text was changed. +It tab-completes slash commands entered into the input field. When the user begins typing a command (text starting with `/` and containing no spaces yet), it matches against the known `SlashCommands` using a case-insensitive comparison (`StringComparison.OrdinalIgnoreCase`). If there is a single match, it replaces the input with that command plus a trailing space; if there are multiple matches, it computes the longest common prefix among the matches and updates the input to that prefix to guide refinement, and finally moves the cursor to the end via `_inputField.InsertionPoint`. --- @@ -1402,14 +1342,10 @@ private void UpdateInputReadOnly() **Returns:** `void` -Disables the input for read-only (system) channels so nothing can be typed there, and reflects the state in the input frame title. It does this by setting the input field's ReadOnly flag based on IsCurrentChannelReadOnly and then calling UpdateInputTitle to synchronize the title with the current state. +Disables user input when the current channel is read-only by assigning `ReadOnly` on `_inputField` based on `IsCurrentChannelReadOnly`, and then refreshes the input frame title via `UpdateInputTitle` to reflect the new state. ## Remarks -This small helper centralizes UI state synchronization: the input interactivity and the title reflect the channel's read-only status from a single source of truth (IsCurrentChannelReadOnly). By keeping UpdateInputReadOnly as the single place that applies this policy, changes to channel permissions automatically propagate to the input control and its label, ensuring consistent feedback to the user. - -## Notes -- Ensure this runs on the UI thread to avoid cross-thread access issues when manipulating UI controls. -- If IsCurrentChannelReadOnly changes, callers should ensure UpdateInputReadOnly is invoked so the input state and title stay in sync. +By encapsulating this behavior in a single method, the UI consistently represents interactivity and state across channel changes. It prevents input in system/read-only channels and ensures the input frame title communicates the current mode, avoiding drift between interactivity and labeling. This approach also centralizes the read-only logic around the `IsCurrentChannelReadOnly` state source, simplifying future changes. --- @@ -1424,16 +1360,10 @@ private void UpdateInputTitle() **Returns:** `void` -Updates the input frame title to reflect the current editing state. When the current channel is read-only, the method forces the title to a fixed read-only message and requests a redraw; otherwise it derives the title from the reply and staged title fragments using a small switch expression: if both fragments are absent it uses the default title; if only one exists it uses that one; if both exist it concatenates them with a separator (" │ "). The method ends by signaling the input frame that it needs to redraw. +UpdateInputTitle refreshes the `_inputFrame` title to reflect the current reply and staged hints; if the current channel is read-only (`IsCurrentChannelReadOnly`), it writes the fixed message `Read-only channel — you cannot type here` to `_inputFrame.Title` and immediately requests a redraw via `_inputFrame.SetNeedsDraw()`. Otherwise it computes the title from `_replyTitleFragment` and `_stagedTitleFragment` with a four-case switch: both null → `DefaultInputTitle`, only reply → `reply`, only staged → `staged`, or both present → `"{reply} │ {staged}"`, followed by a redraw. ## Remarks - -Centralizes the logic for how the input title is computed from the live fragments, ensuring consistent UI behavior in both read-only and writable channels. It prevents scattering of title-construction logic across the window code and makes it easier to adjust the title policy in one place. By honoring the read-only constraint at this point, it guarantees users always see an accurate, explicit hint about their ability to type. - -## Notes - -- The read-only branch takes precedence over any fragment values. -- SetNeedsDraw() is invoked after updating the title to refresh the UI; callers should not rely on drawing happening elsewhere. +This method centralizes the UI title logic, ensuring read-only channels take precedence and that the title cleanly represents combined states when both a reply and a staged title exist. It couples state fragments with the input frame’s rendering, reducing duplication and keeping the UI consistent across edits. --- @@ -1448,28 +1378,13 @@ private void UpdateLayout() **Returns:** `void` -Reflows the main UI by resizing the chat, topic, and input frames based on whether the users panel is visible. It computes a right margin equal to the panel width when visible, applies Dim.Fill(rightMargin) to the relevant frames, toggles the users panel frame visibility, and then requests a redraw. +Recomputes and applies the main window layout whenever the users panel visibility changes. It determines a right margin based on `_usersPanelVisible` (using `UsersPanelWidth` when the panel is visible, or 0 otherwise), updates `_chatFrame.Width`, `_topicLabel.Width`, and `_inputFrame.Width` using `Dim.Fill(rightMargin)`, toggles `_usersFrame.Visible` accordingly, and then calls `SetNeedsDraw()` to refresh the UI. ## Remarks - -Centralizes the layout reflow logic so callers toggle _usersPanelVisible without duplicating width calculations. It coordinates the main content frames and the users panel visibility, ensuring the UI stays visually aligned whenever the panel appears or disappears. Because it's private, it's meant to be invoked by internal state changes rather than external consumers. - -## Example - -```csharp -// Example: toggle the Users panel and refresh layout -_usersPanelVisible = true; -UpdateLayout(); - -_usersPanelVisible = false; -UpdateLayout(); -``` +By centralizing width calculations in a single private helper, this method keeps the layout logic consistent and minimizes layout drift as the panel appears or disappears. It acts as the synchronization point between the panel visibility state and the content frames, ensuring the chat, topic label, and input areas always use the remaining horizontal space. ## Notes - -- Must be called on the UI thread after changing _usersPanelVisible. -- Relies on UsersPanelWidth; ensure it is defined and non-negative when the panel is shown. -- Invoking SetNeedsDraw() schedules a redraw; avoid rapid, consecutive calls from non-UI threads. +- Ensure this runs on the UI thread; UI elements are updated here, and invoking from a background thread can lead to race conditions or exceptions. --- @@ -1490,8 +1405,14 @@ public void UpdateOnlineUsers(List users) **Returns:** `void` -UpdatesOnlineUsers updates the online users list display by transforming a `List` into the UI representation used by the user panel. For each user it computes a status icon from their UserStatus, selects a display name (DisplayName if present, otherwise Username), and applies a role tag derived from their ServerRole. If the user is connected through IRC, it appends a [irc] suffix to convey feature context. The name color is determined by first attempting to parse NicknameColor as a hex color; if that fails, it falls back to a deterministic per-nick color from NickColorHelper.GetAttribute, ensuring colors stay consistent with the user's chat messages. The resulting collection is written to the underlying _usersListSource and the frame title is updated to reflect the current number of online users. +Updates the on-screen list of online users by converting each [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) into a display tuple that includes a status glyph, an optional role glyph, the user's display name (falling back to the username), and a color derived from either the nickname color or the username. It also marks IRC-connected users with an `[irc]` suffix and then updates `_usersListSource`, binds it to `_usersList`, and refreshes the frame title with the current user count. This method centralizes the formatting logic for the user list and ensures color and status presentation stay consistent with chat messages. +## Remarks +It centralizes the presentation of user entries for the online users panel, ensuring status, role, name, and color are consistently derived from the same sources ([`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md), [`ServerRole`](../../EchoHub.Core/Models/ServerRole.cs.md), and nickname/username color palettes). By reusing the deterministic color logic and the inline role and status icons, the UI remains consistent with chat message coloring and user identity. + +## Notes +- This method updates UI state and should be executed on the UI thread to avoid threading issues. +- If `NicknameColor` is not parseable, the color falls back to `NickColorHelper.GetAttribute(u.Username)`. --- @@ -1506,16 +1427,10 @@ private void UpdateSpinner() **Returns:** `void` -Starts the spinner timer when entering a transitional connection state (Connecting, Reconnecting, …); the timer stops itself once the state settles. - -The method immediately returns if there is no transitional status or if a spinner timer is already running, ensuring only a single spinner animation is active at a time. When invoked in the proper state, it schedules a recurring callback via _app.AddTimeout with a 120-millisecond interval. Each tick first checks whether the status remains transitional; if it does not, the method clears the timer token and stops frequency updates. If the status is still transitional, it advances the spinner frame by one, wraps around using the length of SpinnerFrames, and requests a redraw of the status label. This creates a smooth, looping spinner animation that runs only as long as the transitional state persists. - -Dependencies: TimeSpan, SpinnerFrames - -Dependency APIs (verified signatures) - -- field SpinnerFrames (src/EchoHub.Client/UI/MainWindow.cs) +Starts the spinner timer when entering a transitional connection state (e.g., `Connecting`, `Reconnecting`); the timer stops itself once the state settles. It guards against starting multiple timers by returning early when `_spinnerToken` is not null, then schedules a 120ms callback via `_app.AddTimeout` that, if still transitional, rotates the spinner frame by incrementing `_spinnerFrame` modulo `SpinnerFrames.Length` and triggers a redraw of `_statusLabel`; the callback returns true to continue scheduling and false to stop when `IsTransitionalStatus` becomes false. +## Remarks +Encapsulates the spinner animation logic so the UI keeps a single, self-terminating indicator while the connection state is in flux. It relies on `SpinnerFrames` for the frame sequence and uses `_statusLabel.SetNeedsDraw()` to refresh the display. The `_spinnerToken` field ensures only one active timer exists at a time, avoiding overlapping animation loops. --- @@ -1536,20 +1451,12 @@ public void UpdateStatusBar(string status) **Returns:** `void` -Updates the connection status displayed in the status bar by updating the internal state, refreshing the spinner, and invalidating the status label for redraw. Call this method whenever the connection state changes (for example, during connecting, when connected, or on disconnection) to keep the status bar in sync with the actual connection status without duplicating UI update logic elsewhere. - -## Remarks -Centralizes the status bar update sequence in one place, ensuring the internal state and the visual feedback stay in sync. By encapsulating the setter of _connectionStatus, the spinner refresh, and the redraw trigger, callers avoid partially updated UI states and reduce the risk of stale visuals. +Updates the connection status displayed in the status bar. This method assigns the new status to the internal `_connectionStatus`, triggers the spinner update via `UpdateSpinner()`, and requests a redraw of the label by calling `_statusLabel.SetNeedsDraw()`. -## Example -```csharp -// Example: update the status bar to reflect a connected state -UpdateStatusBar("Connected"); -``` +Call this method whenever the connection state changes, to keep the status bar in sync without spreading UI update logic elsewhere. -## Notes -- Should be invoked on the UI thread to safely update UI elements. -- Frequent updates will trigger spinner refreshes and redraws; consider batching rapid status changes. +## Remarks +This method centralizes the UI update path for connection state changes in the main window. It ensures consistency between the underlying `_connectionStatus` and the visuals by coordinating state assignment, spinner visibility, and redraw scheduling through `_statusLabel.SetNeedsDraw()`. --- @@ -1564,11 +1471,10 @@ private void UpdateTopicBar() **Returns:** `void` -Show or hide the topic bar based on the current channel's topic. +Updates the topic bar for the current channel by reading the topic from `_channelTopics` for the current channel via `_messageManager.CurrentChannel`. If a non-empty topic is found, `_topicLabel.Text` is set to `Topic: {topic}`, `_topicLabel` is made visible, and `_chatFrame.Y` is set to 2 to make room for the topic bar. If there is no topic, `_topicLabel` is hidden and `_chatFrame.Y` is set to 1. -The UpdateTopicBar method reads the topic associated with the current channel from the _channelTopics collection and updates the topic label and chat layout accordingly. If a non-empty topic is found, it sets the label text to " Topic: {topic}", makes the label visible, and adjusts the chat frame position (Y = 2) to accommodate the topic bar. If there is no topic (or the topic is whitespace), it hides the topic label and resets the chat frame position (Y = 1). - -This method uses TryGetValue to avoid exceptions when a channel has no entry and relies on string.IsNullOrWhiteSpace to determine whether a topic should be shown. It encapsulates the small but important UI logic that bridges channel-topic data with the visual layout, ensuring a consistent presentation whenever the current channel or topic changes. +## Remarks +Keeping this logic in one place decouples topic data from layout decisions, ensuring a consistent UI state whenever channels switch or topics change. It coordinates `_channelTopics` with `_messageManager.CurrentChannel` to update `_topicLabel` and `_chatFrame` in lockstep, so the presence or absence of a topic immediately reflects in the UI. --- @@ -1581,500 +1487,406 @@ private const KeyCode AltQKey = KeyCode.Q | KeyCode.AltMask ``` -AltQKey is a private constant KeyCode that encodes the keyboard shortcut Alt+Q by OR-ing KeyCode.Q with KeyCode.AltMask. It’s intended for input checks in the class, allowing a single, readable comparison (e.g., Input.GetKeyDown(AltQKey)) instead of reassembling the combination at every usage. +Defines the keyboard shortcut Alt+Q as a single `KeyCode` value by bitwise OR-ing `KeyCode.Q` with `KeyCode.AltMask`. This private constant centralizes the Alt+Q pattern so input checks can reference `AltQKey` instead of composing the combination inline, improving readability and reducing the risk of inconsistencies in the UI input handling. ## Remarks -This abstraction centralizes the Alt+Q shortcut so changes to the shortcut can be made in one place rather than scattered across the codebase. It also communicates intent more clearly than a scattered KeyCode.Q | KeyCode.AltMask in multiple checks. Because it’s a compile-time constant, it’s cheap to inline in input checks. - -## Notes -- The Alt+Q shortcut relies on the legacy input system’s KeyCode and AltMask semantics; if migrating to a different input system, this approach may need adjustment. -- Since AltQKey is private, it cannot be referenced from outside this class; expose a public/internal alias or a helper if cross-class usage is required. +Private to the enclosing class, `AltQKey` serves as a single source of truth for the Alt+Q shortcut within the main window's input handling. This encapsulation keeps the shortcut localized and ensures future changes (e.g., modifying the modifier or the base key) only need to modify this constant. By naming the combination, it communicates intent and reduces cognitive load when reviewing input checks. --- -### CtrlCKey +### AppVersion > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode CtrlCKey = KeyCode.C | KeyCode.CtrlMask +internal static readonly string AppVersion = + typeof(MainWindow).Assembly.GetName().Version?.ToString(3) ?? "?" ``` -CtrlCKey defines the keyboard shortcut Ctrl+C as a single KeyCode value by bitwise OR-ing the C key with the Ctrl modifier. Use this constant when your input handling needs to react to the Ctrl+C shortcut, instead of composing KeyCode.C and KeyCode.CtrlMask in every check. +AppVersion provides a concise, ready-to-display version string for the UI by taking the `Version` of the assembly containing `MainWindow`, formatting it with three components via `ToString(3)`, and falling back to `?` when the version cannot be determined. Developers typically reference `AppVersion` when showing the application version in the UI or logs to avoid duplicating assembly-lookup logic. ## Remarks -Centralizes the copy shortcut into a single, reusable symbol. It prevents duplicated magic-number logic across input checks and makes future changes to the shortcut trivial. Because the field is private const, it stays encapsulated within its containing class and provides a stable value for all internal listeners that rely on the same representation. This relies on KeyCode supporting modifier flags, enabling concise expression of keyboard shortcuts. ---- - -### CtrlVKey -> **File:** `src/EchoHub.Client/UI/MainWindow.cs` -> **Kind:** field - -```csharp -private const KeyCode CtrlVKey = KeyCode.V | KeyCode.CtrlMask -``` +By deriving from `typeof(MainWindow).Assembly`, the value is tied to the UI assembly's metadata, ensuring the string reflects exactly the UI binary the user interacts with. It is evaluated once during type initialization and remains constant for the lifetime of the process; the null-coalescing ensures a non-null string even if the UI assembly lacks a `Version`. +## Notes -Represents the Ctrl+V keyboard shortcut used to trigger paste-like actions within the UI. Implemented as a private const KeyCode that combines KeyCode.V with KeyCode.CtrlMask into a single composite value. Use CtrlVKey in input handling within MainWindow to detect paste attempts instead of duplicating the key-check logic in multiple handlers. +- Computed once per process lifecycle; subsequent reads are a cheap field access. +- This value is internal to the containing assembly, so external components cannot rely on it being visible; use a public API if you need to surface the version externally. -## Remarks -Centralizing the shortcut reduces duplication and keeps the paste-trigger logic consistent across the UI layer. The private visibility confines the behavior to the MainWindow class, supporting cohesive input handling without leaking implementation details. If cross-platform consistency is required, consider exposing a platform-aware abstraction (for example, mapping Cmd on macOS to Ctrl on Windows) to avoid surprising users. - -## Notes -- This is a compile-time constant; it cannot be reconfigured at runtime, so any need to support dynamic key bindings would require a different approach (e.g., a settings-backed binding). -- Relying on KeyCode.CtrlMask ties the value to the framework's modifier encoding; ensure it aligns with input handling elsewhere in the app to prevent mismatches. --- -### CtrlXKey +### CtrlCKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode CtrlXKey = KeyCode.X | KeyCode.CtrlMask +private const KeyCode CtrlCKey = KeyCode.C | KeyCode.CtrlMask ``` -CtrlXKey encodes the Ctrl+X keyboard shortcut as a single KeyCode value by performing a bitwise OR between KeyCode.X and KeyCode.CtrlMask. This provides a readable, centralized way for the main window's input handling to detect the Ctrl+X combination, avoiding scattered modifier checks throughout the code. +Encodes the Ctrl+C keyboard shortcut as a single `KeyCode` value by combining `KeyCode.C` with `KeyCode.CtrlMask`. This private constant is used by the UI to detect when the user presses the `Ctrl+C` shortcut, consolidating the detection logic in one location to avoid duplicating the combo throughout `MainWindow`. ## Remarks -Centralizes the shortcut definition to reduce duplication and make future changes easier. Keeping the constant private confines the shortcut to the UI input logic, preventing misuse from unrelated parts of the codebase. If the underlying input system evolves to treat modifiers separately from keys, this constant may need to be revisited to ensure Ctrl+X is still detected correctly. + +Centralizes input handling for a common shortcut, so the detection is consistent across the class. If the shortcut changes, update this single constant rather than scattered inline checks. The private scope marks it as an internal wiring detail of the UI, not part of the public API. It encodes a Ctrl-based shortcut; consider platform-specific Cmd+C handling for macOS if cross-platform parity is needed. ## Notes -- The approach relies on KeyCode being a flags-like enum so that combining X with the Ctrl modifier via a bitwise OR yields a meaningful single value. If the input API changes, CtrlXKey may no longer reflect the intended shortcut. -- Because CtrlXKey is private, always reference this constant within the class that handles keyboard input to avoid diverging shortcuts; duplicating the literal elsewhere risks inconsistency. + +- This member is private to `src/EchoHub.Client/UI/MainWindow.cs`; it cannot be accessed from outside. If external components need to reference the shortcut, consider exposing it via an internal or public API or by providing a helper method. --- -### CtrlYKey +### CtrlKKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode CtrlYKey = KeyCode.Y | KeyCode.CtrlMask +private const KeyCode CtrlKKey = KeyCode.K | KeyCode.CtrlMask ``` -Represents the Ctrl+Y keyboard shortcut as a single KeyCode value. Use this constant in the MainWindow's input handling to detect the Ctrl+Y combination without duplicating the modifier logic at each call site. +This private constant named `CtrlKKey` encodes the Ctrl+K keyboard shortcut as a `KeyCode` value by combining `KeyCode.K` with `KeyCode.CtrlMask`. It is used in the `src/EchoHub.Client/UI/MainWindow.cs` keyboard input handling to detect when the user presses `Ctrl+K`, centralizing the shortcut representation rather than duplicating the bitwise expression across the code. ## Remarks -Centralizes the hotkey for the main window, ensuring consistent behavior and making future changes easy to propagate. Keeping it private encapsulates the shortcut within the MainWindow class, reducing the risk of misuse and enabling compiler-level inlining for performance. +By keeping the shortcut in a single `CtrlKKey` field, the codebase gains a single source of truth for this shortcut. This reduces duplication and makes future changes to the `CtrlKKey` binding easier to maintain within `MainWindow.cs`. ## Notes -- Platform and input-system differences can affect how modifiers are interpreted; verify behavior on all target platforms. -- If other components need the same shortcut, avoid duplication by exposing a controlled API instead of re-declaring the same KeyCode combination. +- On some platforms or Unity configurations, modifier bits may vary; if you need to support Cmd on macOS or other modifiers, you may need a broader input check rather than relying solely on `KeyCode.CtrlMask`. --- -### EnterKey +### CtrlVKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode EnterKey = KeyCode.Enter +private const KeyCode CtrlVKey = KeyCode.V | KeyCode.CtrlMask ``` -EnterKey is a private constant field of type KeyCode that represents the Enter key. It exposes KeyCode.Enter as a named value so input-handling code can compare against EnterKey directly, for example in switch statements, without relying on the raw KeyCode.Enter literal or invoking Key.Equals (which also accounts for a Handled state). This small alias keeps the Enter binding centralized and makes the code's intent clearer. - -## Remarks -Using EnterKey communicates intent and reduces the use of magic constants in input logic. Since it's private, only members within the containing type can reference it, keeping the binding decision encapsulated. If you later need to reuse the same binding from other types, consider exposing a non-private alias or extracting this pattern to a shared helper. - -## Notes -- Because it is const, its value is inlined at compile time; changing it requires recompilation of all assemblies that reference it. -- Encapsulation matters: private scope confines use to this type; expose it with a public/internal alias if cross-type reuse is required. -- The alias assumes the Enter key maps to KeyCode.Enter; if the underlying enum changes, update this constant accordingly. +The `CtrlVKey` field encodes the Ctrl+V shortcut as a composite `KeyCode` value by combining `KeyCode.V` with `KeyCode.CtrlMask` (`KeyCode.V | KeyCode.CtrlMask`). As a private constant in `src/EchoHub.Client/UI/MainWindow.cs`, it provides a single canonical value for detecting paste commands in input handling, avoiding repeated bitwise construction scattered through the code. Use this symbol whenever you need to detect paste-like input from the user, rather than comparing against `KeyCode.V` or the control modifier separately. --- -### F2Key +### CtrlXKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode F2Key = KeyCode.F2 +private const KeyCode CtrlXKey = KeyCode.X | KeyCode.CtrlMask ``` -Defines a private, compile-time alias for the F2 keyboard key as a KeyCode value. Within the MainWindow class, this F2Key field is used instead of sprinkling KeyCode.F2 directly in input handling, improving readability and making future key-rebinding easier to manage in one place. +Represents the keyboard shortcut Ctrl+X as a `KeyCode` value by combining `KeyCode.X` with `KeyCode.CtrlMask` and is intended for use in the UI input handling within `MainWindow` to detect the Ctrl+X shortcut. By centralizing the combination in this `private const`, code that reacts to Ctrl+X can simply compare against `CtrlXKey` instead of duplicating the bitwise OR expression in multiple places. ## Remarks -This symbol centralizes the F2-key representation, reducing duplication and making the code intent clear when handling keyboard input. As a private const, it is inlined at all call sites and cannot be reassigned at runtime, preserving a stable mapping inside the class. It helps decouple the key's meaning from its concrete enum value, so refactoring the underlying KeyCode reference requires changing only this single declaration. +This constant serves as a single source of truth for the Ctrl+X shortcut within the `MainWindow` input pipeline. It improves readability by exposing the intent of the key combination (X with the Ctrl modifier) and makes future changes to the shortcut straightforward—update the constant in one place rather than hunting through the codebase. Being `private`, its usage is intentionally confined to the class boundary, reinforcing encapsulation around the UI's keyboard handling. ## Notes -- Const fields are implicitly static and are inlined at compile time. If you need to support runtime reconfiguration of the key binding, convert this to a visible, non-const field or a configurable option. +- `CtrlXKey` is a `private const`, so the value is inlined by the compiler and not accessible from outside. +- If you need to reuse the same shortcut elsewhere, consider extracting it to a shared location or exposing a public/internal member to avoid duplication. +- Ensure all input checks compare against `CtrlXKey` with the same modifier semantics (i.e., modifiers represented by `KeyCode.CtrlMask`). --- -### SlashCommands +### CtrlYKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly string[] SlashCommands = - [ - "/status", "/nick", "/color", "/theme", "/send", "/me", "/banner", - "/avatar", "/profile", "/servers", "/join", "/passwd", "/leave", "/clear", "/size", "/downloadpath", - "/topic", "/users", "/kick", "/ban", "/unban", - "/mute", "/unmute", "/role", "/invite", "/export", "/deleteaccount", - "/nuke", "/test-sound", "/quit", "/help" - ] +private const KeyCode CtrlYKey = KeyCode.Y | KeyCode.CtrlMask ``` -SlashCommands is a private static readonly array of strings that enumerates the available slash commands used by the tab-autocomplete in the main window. The UI consults this list to offer command suggestions as the user types a leading slash. +Represents the Ctrl+Y keyboard shortcut as a single `KeyCode` value by combining `KeyCode.Y` with `KeyCode.CtrlMask`. Use this constant in input handling to recognize Ctrl+Y presses without duplicating the bitwise expression, keeping the code readable and maintainable when checking for shortcuts in the UI. ## Remarks -This centralized catalog ensures consistency across the autocomplete experience and acts as the single source of truth for which commands are supported. Its private scope keeps coupling tight to the UI implementation, and the readonly modifier prevents reassigning the array reference at runtime, preserving the integrity of the command set. The list includes commands like /status, /nick, /color, /theme, /send, /me, /banner, /avatar, /profile, /servers, /join, /passwd, /leave, /clear, /size, /downloadpath, /topic, /users, /kick, /ban, /unban, /mute, /unmute, /role, /invite, /export, /deleteaccount, /nuke, /test-sound, /quit, and /help. - -## Notes -- The initializer syntax shown in the snippet uses square brackets [], which is not valid in C# for an array initializer; the actual source should use a braces-based initializer such as: private static readonly string[] SlashCommands = new[] { "/status", "/nick", ... } or private static readonly string[] SlashCommands = { "/status", "/nick", ... }. +Because it is declared as a private constant inside `src/EchoHub.Client/UI/MainWindow.cs`, the shortcut is kept private to the UI layer and serves as a single source of truth for this particular binding. This encapsulation makes it easy to update the shortcut in one place and ensures all input checks against `CtrlYKey` stay consistent across the related methods. --- -### SpinnerFrames +### DefaultInputTitle > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly string[] SpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] +private const string DefaultInputTitle = "Message │ Enter=send │ Tab=complete │ Ctrl+K=search │ F6=pick message" ``` -SpinnerFrames defines the frames of the Braille spinner shown while the connection is in a transitional state. Use this sequence as the source of frames for a timer-driven animation in the UI when signaling a transitional connection state; external code should not rely on this private field directly. +`DefaultInputTitle` is a private compile-time constant string that defines the default title shown for the message input area in the main window. It is initialized with the user-facing hint text `Message │ Enter=send │ Tab=complete │ Ctrl+K=search │ F6=pick message`, which communicates the available keyboard shortcuts to users during initial UI presentation. This value is sourced from `src/EchoHub.Client/UI/MainWindow.cs` and is used to initialize the UI to provide consistent guidance at startup. ## Remarks -SpinnerFrames centralizes the frame sequence for the connection-status animation, enabling a single place to tweak the visual rhythm without touching animation logic in multiple places. Its private, static readonly nature encapsulates the detail of the glyphs from consumers and ensures consistency across any UI updates that rely on this spinner. The glyphs are Unicode braille patterns, chosen to render as a compact and legible motion, but their appearance depends on font support in the host UI. +Because this constant is private to the UI class, it is solely used during initialization to ensure a consistent hint is presented across the session. If you need localization or runtime configurability, this hard-coded value should be moved to a resources file or exposed through a non-const property. ## Notes -- Because the field is readonly, you cannot reassign SpinnerFrames to a new array, but its contents can still be mutated if code within the class changes elements; to enforce true immutability consider exposing as `IReadOnlyList` or copying to an immutable collection. -- Braille glyph rendering depends on font support; ensure the UI uses a font that includes these characters, otherwise fallback glyphs will appear. +- As a `private const string`, the value is compiled into the assembly and cannot be changed at runtime; consider localization if the application targets multiple languages. --- -### StatusActivityAttr +### EnterKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly Attribute StatusActivityAttr = new(new Color(80, 200, 220), Color.None) +private const KeyCode EnterKey = KeyCode.Enter ``` -StatusActivityAttr is a private static readonly field that holds a preconfigured Attribute instance used to style status-activity indicators in the UI. It is initialized with a Color(80, 200, 220) and a secondary color of Color.None, providing a consistent visual token that other UI components can apply without duplicating construction logic. +EnterKey is a private constant of type `KeyCode` set to `KeyCode.Enter`. It provides a single, reusable value for comparing input against the Enter key using raw `KeyCode` values, avoiding the extra semantics of `Key.Equals` (which also checks `Handled`). This approach keeps Enter-key handling in switch or conditional checks straightforward within the class. ## Remarks -It centralizes the styling token for status activity, ensuring a uniform look across the MainWindow's status displays. Because the field is static and readonly, the same Attribute instance is reused by all usages within its declaring type, reducing allocations and keeping styling decisions centralized. If the Attribute type is mutable, modifications to the instance would propagate to every consumer; prefer treating StatusActivityAttr as effectively immutable or clone it when variations are required. +By encapsulating the Enter key mapping in this private field, the class avoids duplicating the `KeyCode.Enter` literal and ensures consistent semantics across all internal input checks. It also decouples the input comparison logic from the `Key` class's equality semantics, making intent clearer and future changes to the binding easier to manage. Because the field is private, reuse across other classes would require a shared abstraction. ## Notes -- This field is private; it is intended for internal use within its containing type. -- Mutating the underlying Attribute would affect all referents of StatusActivityAttr if allowed. -- If you need a different color or variant, instantiate a new Attribute rather than adjusting this shared field. +- Private visibility means this constant isn't accessible outside its containing type; if cross-class usage is needed, expose a public constant or centralize key bindings in a shared utility. --- -### StatusBrandAttr +### F2Key > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly Attribute StatusBrandAttr = new(new Color(218, 165, 32), Color.None) +private const KeyCode F2Key = KeyCode.F2 ``` -This private static readonly field holds a pre-constructed Attribute instance used to apply the application's status branding color in the UI. It centralizes the gold brand color (RGB 218,165,32) and uses Color.None for the secondary color, enabling consistent styling of status indicators across MainWindow without repeatedly allocating new Attribute objects. +Defines the F2 keyboard binding as a private constant `F2Key` set to `KeyCode.F2` for use in the `MainWindow` input handling. This avoids repeating the raw `KeyCode.F2` value scattered through the class and makes future changes to the F2 shortcut straightforward by updating a single symbol. ## Remarks -StatusBrandAttr serves as a small branding primitive: it encapsulates the branding color data in a single, shared value so UI rendering code can consistently decorate status indicators. As a private member, its usage is confined to the class, reducing risk of styling drift and making updates to the brand color straightforward. The static readonly pattern also avoids per-instance allocations, which helps keep the UI responsive during frequent status refreshes. +Replaces magic key literals with a named binding inside the class, improving readability and reducing the risk of inconsistent shortcuts. Being private ensures this mapping is encapsulated within the UI logic and not exposed to external components; if sharing the binding is needed, consider exposing it via a property or moving it to a shared constants location. ## Notes -- This field is private; external code cannot access StatusBrandAttr. If you need external reuse, add an accessor or an API that exposes the color or attribute. -- Color.None is used as the secondary color; callers should not rely on a non-null accent color being provided through this attribute. -- Because it's static and initialized inline, the field is created once per AppDomain; changes to the initializer are global to the class consumers. - ---- - -### StatusConnectedAttr -> **File:** `src/EchoHub.Client/UI/MainWindow.cs` -> **Kind:** field - -```csharp -private static readonly Attribute StatusConnectedAttr = new(new Color(0, 200, 0), Color.None) -``` - - -StatusConnectedAttr is a private static readonly field that provides a prebuilt Attribute instance representing the UI styling for a connected status. It is constructed with a green primary color (0, 200, 0) and Color.None as the secondary color, and it is intended to be reused wherever a connected indicator is needed in the MainWindow UI. - -## Remarks -Centralizes the appearance of the connected-state styling to avoid duplicating color values across the UI. Being static readonly, it is allocated once and reused, which reduces allocations during frequent UI updates. As a private member, it keeps concerns localized to MainWindow, making it easy to swap or adjust the connected appearance in one place if the theme changes. +- Private visibility means it cannot be referenced from outside the class; reuse across components would require a public or internal accessor or moving the binding to a shared constants file. +- Because it is a `const`, its value is baked at compile time; if runtime configurability is required, switch to a non-const static field. --- -### StatusDisconnectedAttr +### F6Key > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly Attribute StatusDisconnectedAttr = new(new Color(220, 50, 50), Color.None) +private const KeyCode F6Key = KeyCode.F6 ``` -StatusDisconnectedAttr is a private static readonly Attribute that encapsulates the UI styling used to communicate a disconnected state in MainWindow. It specifies a reddish foreground color (RGB 220, 50, 50) and no background, enabling a clear, consistent visual cue when the application loses its connection. +Provides a single, immutable reference to the F6 key as a private constant `KeyCode` named `F6Key` in `src/EchoHub.Client/UI/MainWindow.cs`; this removes magic literals from input handling and makes it easy to adjust the binding in one place if needed. The value is fixed at compile time as `KeyCode.F6`. ## Remarks -This member centralizes the visual representation of the 'disconnected' state, ensuring all indicators share the same look. Its static readonly nature guarantees the attribute is created once and reused, promoting performance and consistency across the UI. Because the field is private, it is not directly reusable by other components; if cross-component reuse is needed, consider extracting the styling into a shared resource or exposing a controlled accessor. +By centralizing the binding in a private constant, the surrounding input logic can rely on a single source of truth for the F6 key, reducing drift and typos. Keeping it private confines the binding to `MainWindow`, signaling that this is an internal convention rather than a public API. If future needs require rebinding at runtime, replace this constant with a configurable alternative. ## Notes -- Not accessible outside the declaring type; if you need to reuse this styling elsewhere, factor it into a shared resource or provide a public accessor. +- Because `F6Key` is a `const`, its value is baked in at compile time and cannot be changed at runtime. If you anticipate needing to rebind the key, switch to a mutable configuration-based approach. --- -### StatusMentionAttr +### NewlineKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly Attribute StatusMentionAttr = new(new Color(230, 140, 60), Color.None) +private const KeyCode NewlineKey = KeyCode.N | KeyCode.CtrlMask ``` -StatusMentionAttr defines a shared, immutable Attribute instance used to render status mentions with a warm orange color in the UI. A developer would reference this field when they need a consistent highlight for status mentions instead of constructing a new Attribute each time. +Represents the private, compile-time constant `NewlineKey` of type `KeyCode` that encodes the `Ctrl+N` keyboard shortcut by combining `KeyCode.N` with `KeyCode.CtrlMask`. It is used in input handling to detect the `Ctrl+N` sequence without duplicating the bitwise expression across the class, enabling a single source of truth for this shortcut and a consistent trigger (such as initiating a new item or inserting a newline) wherever the UI logic responds to that keystroke. ## Remarks -Centralizes the visual treatment for status mentions, ensuring consistent appearance across the UI and simplifying future theming. Because the field is static readonly, the color choice is determined at type initialization and cannot be changed at runtime, which prevents accidental mutation. Keeping the field private confines its usage to the containing type, making the intended styling an internal concern that can be adjusted without leaking implementation details. +This abstraction localizes the keyboard shortcut within the `MainWindow` class, reducing duplication and clarifying intent when handling input. Because the field is `private const`, its value is fixed at compile time and inaccessible from outside the class; if the shortcut needs to change, a code change and recompilation are required for the update to propagate. ## Notes -- The field is private, so external consumers cannot reference it directly; reuse must occur within the declaring class or through a controlled API. -- The color is baked into the initialization; updates require recompilation, so plan color theming accordingly. +- Because `NewlineKey` is a `const`, its value is inlined at call sites by the compiler, so changes require recompiling all dependents that reference it. --- -### StatusTransitionalAttr +### SlashCommands > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private static readonly Attribute StatusTransitionalAttr = new(new Color(220, 180, 0), Color.None) +private static readonly string[] SlashCommands = + [ + "/status", "/nick", "/color", "/theme", "/send", "/me", "/banner", + "/avatar", "/profile", "/servers", "/join", "/passwd", "/leave", "/clear", "/size", "/downloadpath", + "/topic", "/users", "/kick", "/ban", "/unban", + "/mute", "/unmute", "/role", "/invite", "/export", "/deleteaccount", + "/nuke", "/test-sound", "/quit", "/help" + ] ``` -StatusTransitionalAttr is a private static readonly Attribute that encapsulates the visual styling for elements representing a transitional state in the EchoHub client’s main window. It is initialized with an amber color (RGB 220, 180, 0) and a secondary color of Color.None, providing a single, reusable styling token to ensure consistent amber emphasis for in-progress or transitioning UI elements rather than sprinkling color literals throughout the code. +This private static readonly field `SlashCommands` defines the set of slash commands available for tab-autocomplete in the chat input of the main window. It lists commands like `/status`, `/nick`, `/color`, `/theme`, `/send`, `/me`, `/banner`, `/avatar`, `/profile`, `/servers`, `/join`, `/passwd`, `/leave`, `/clear`, `/size`, `/downloadpath`, `/topic`, `/users`, `/kick`, `/ban`, `/unban`, `/mute`, `/unmute`, `/role`, `/invite`, `/export`, `/deleteaccount`, `/nuke`, `/test-sound`, `/quit`, and `/help`. ## Remarks -By centralizing the transitional-state styling in a single field, the codebase gains a clear semantic signal for 'in-progress' statuses and can adapt to theme changes in one place. Because StatusTransitionalAttr is static and readonly, it acts as a stable styling token that can be applied wherever a transitional state needs highlighting without risking inconsistencies. The amber color choice communicates a cautionary or temporary state to users, and the absence of a secondary color keeps the emphasis on the primary transition cue. +This field serves as the single source of truth for the UI's autocomplete behavior in `MainWindow.cs`. By making it `private static readonly`, the list is a shared, effectively constant source of suggestions that the tab-autocomplete logic can rely on at runtime, ensuring consistent user experience. If new slash commands are introduced elsewhere in the application, they must be added here to keep the autocomplete in sync with the available commands. ## Notes -- The field is private to MainWindow.cs; external code cannot reuse StatusTransitionalAttr directly. -- The second constructor argument is Color.None; its exact meaning depends on the Attribute API—consult its documentation if you need to extend this with a secondary color or outline. +- Because the list is hard-coded in source, changes require recompilation and redeployment for the UI to pick up new commands. +- The field is private; if cross-component reuse is needed, consider exposing a public accessor or moving the list to a shared configuration. --- -### TabKey +### SpinnerFrames > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode TabKey = KeyCode.Tab +private static readonly string[] SpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] ``` -TabKey is a private constant alias for KeyCode.Tab used within the class to refer to the Tab key in a more readable and centralized way. It maps directly to the Tab key code and is typically used wherever the code needs to detect or respond to tab-navigation input without scattering KeyCode.Tab throughout the logic. +SpinnerFrames is a private static readonly array of strings containing the braille spinner glyphs used to animate a spinner while the connection is in a transitional state. The UI cycles through these frames to convey progress during connectivity changes. ## Remarks - -By providing a private const alias, this symbol communicates intent (tab-navigation handling) while keeping the public surface area uncluttered. It also ensures the tab key value is inlined at call sites for performance, without exposing the alias to consumers. +By making the field static and readonly, the frame sequence is created once and cannot be mutated at runtime, ensuring a consistent animation across all usages within the class. Centralizing the frame sequence here avoids repeating literal frame values throughout the UI code and makes it straightforward to adjust the spinner's appearance in a single place. ## Notes - -- Const inlining: TabKey's value is baked into call sites; changing KeyCode.Tab in the framework requires recompilation to reflect the new value. -- Scope: TabKey is private; it's not accessible outside this class. If cross-class usage is needed, consider exposing it or using KeyCode.Tab directly. +- Do not attempt to modify the frames at runtime; `SpinnerFrames` is `readonly`, so reassignment isn’t possible. +- Ensure the source file encoding supports the braille glyphs used in the frames; using an incompatible encoding may lead to garbled or lost characters. --- -### UsersPanelWidth +### StatusActivityAttr > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const int UsersPanelWidth = 22 +private static readonly Attribute StatusActivityAttr = new(new Color(80, 200, 220), Color.None) ``` -Defines the fixed width of the Users panel in the main window UI. Use this constant when sizing or laying out the Users panel to ensure a consistent width without sprinkling magic numbers in the UI code; it is a private const within MainWindow.cs, so its value is inlined at compile time and encapsulated from external code. +StatusActivityAttr is a private, static, readonly field of type `Attribute` used to style status-activity visuals within the `MainWindow` UI. It is initialized with a single `Attribute` instance created as `new(new Color(80, 200, 220), Color.None)`, pairing a cyan primary color with no secondary color. Because the field is `static readonly`, the same configured attribute is reused across the class, ensuring a consistent look for status indicators throughout the UI. ## Remarks -Centralizing this width as a private constant prevents magic numbers from scattering through layout logic and clarifies the intent behind the panel's sizing. It keeps the responsibility for the UI's sizing localized to MainWindow.cs, reducing cross-cutting dependencies. When design changes are needed, updating this single constant updates all layout paths that reference it, lowering the risk of inconsistent widths. It also communicates that this width is a design-time decision rather than a user-configurable setting. - -## Notes -- Because it's const, the value is baked into compiled code at each usage, so changing it requires recompilation of all assemblies that reference it. -- Being private, external classes cannot rely on this constant; if sharing is needed, consider making it internal or exposing it via a property. +Centralizes the color styling for status-activity visuals within the `MainWindow` UI, providing a single source of truth and reducing color-value duplication. Its `private` scope signals that this is an internal implementation detail, while the `static readonly` nature guarantees a single, immutable instance used consistently across all code paths that render status indicators. --- -## ClickChannelRegex +### StatusBrandAttr > **File:** `src/EchoHub.Client/UI/MainWindow.cs` -> **Kind:** method +> **Kind:** field ```csharp -[GeneratedRegex(@"(? **File:** `src/EchoHub.Client/UI/MainWindow.cs` -> **Kind:** method +> **Kind:** field ```csharp -[GeneratedRegex(@"(? **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -internal static readonly string AppVersion = - typeof(MainWindow).Assembly.GetName().Version?.ToString(3) ?? "?" +private static readonly Attribute StatusDisconnectedAttr = new(new Color(220, 50, 50), Color.None) ``` -AppVersion is a read-only string that captures the application's version by reading the MainWindow assembly version and formatting it as major.minor.build. If the version isn’t available, it falls back to a single question mark. Because it is internal, static, and readonly, the value is computed once and can be consumed by UI code or logging without repeating assembly lookups. A developer would reach for it when displaying the application version (for example in an About dialog) or when including the version in diagnostic output. +Defines a prebuilt `Attribute` for `StatusDisconnectedAttr` that represents the UI treatment for the 'Disconnected' state. It is initialized with a red color (`new Color(220, 50, 50)`) and no secondary color (`Color.None`), ensuring a consistent visual cue when signaling disconnection. ## Remarks -It serves as a centralized, read-only source of the app’s version for the UI layer, ensuring a single, consistent string is used across dialogs and logs rather than duplicating assembly-version lookups. +Centralizes the visual cue for disconnection as a single immutable token, guaranteeing that all disconnected indicators use the same color semantics. It is private to the containing class, so reuse is internal; if cross-class reuse is needed, expose a public or internal accessor or move the token to a shared theming resource. + +## Notes +- Private accessibility limits reuse outside the declaring type; to share styling, expose an accessor or place the token in a shared theme. --- -## CtrlKKey +### StatusMentionAttr > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode CtrlKKey = KeyCode.K | KeyCode.CtrlMask +private static readonly Attribute StatusMentionAttr = new(new Color(230, 140, 60), Color.None) ``` -Represents the keyboard shortcut Ctrl+K as a KeyCode value by combining the K key with the Ctrl modifier. This private constant centralizes the hotkey used within MainWindow, enabling the code to detect or respond to Ctrl+K without scattering the modifier logic across methods. +Defines a shared, preconfigured instance of `Attribute` named `StatusMentionAttr` that provides the standard color styling for status mentions in the UI. Created as a private static readonly field, it initializes with a primary color of `new Color(230, 140, 60)` and a secondary color of `Color.None`, ensuring a consistent orange highlight without a background tint. Use this field whenever the UI needs the canonical status-mention appearance instead of constructing a new `Attribute` on each use. ## Remarks - -By centralizing the shortcut into CtrlKKey, the codebase avoids duplicating the same key-combination logic and makes future changes straightforward (e.g., changing the shortcut would only require updating this single declaration). The naming makes intent clear: it is a Control-K hotkey, distinct from plain K or other modifiers. +By centralizing this styling in a single static field, the UI maintains a consistent look for status mentions across controls and avoids duplicating color configuration. It also makes the intent explicit: the orange highlight is reserved for status mentions and should be reused. ## Notes - -- Private scope means external code cannot rely on this constant; if external access is needed, expose a public API or event. -- The value depends on the KeyCode and CtrlMask semantics of the project's input system; confirm that CtrlMask is the intended modifier representation to avoid misdetections on other platforms. +- If the `Attribute` type exposes mutable state, avoid mutating `StatusMentionAttr` after initialization, as doing so would propagate changes across all uses within the class. --- -## DefaultInputTitle +### StatusTransitionalAttr > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const string DefaultInputTitle = "Message │ Enter=send │ Tab=complete │ Ctrl+K=search │ F6=pick message" +private static readonly Attribute StatusTransitionalAttr = new(new Color(220, 180, 0), Color.None) ``` -This private constant defines the default text shown for the message input title in the main window. It provides an on-screen cue about how to interact with the input, listing shortcuts such as Enter to send, Tab to complete, Ctrl+K to search, and F6 to pick a message, which helps users discover available actions without opening a help screen. +The `StatusTransitionalAttr` is a private static readonly field of type `Attribute` that provides a preconfigured styling primitive for UI elements in a transitional state. It is initialized with a primary `Color` of `new Color(220, 180, 0)` and a secondary color of `Color.None`, enabling consistent usage across the UI without constructing new attributes repeatedly. ## Remarks -Centralizing this label ensures consistent user guidance and avoids duplicating the hint in multiple places. Because the field is private and declared as a const, its value is baked into the assembly and cannot be changed at runtime or localized without refactoring to resources. If localization or runtime configurability is required, this should be moved to a resource string or a configuration mechanism and wired into the UI initialization. +This field centralizes the visual cue for transitional statuses, ensuring a uniform appearance wherever it’s used within this class. Because it is private, reuse is limited to the defining type; if external components need the same look, expose a controlled accessor or move the attribute to a shared styling utility. It effectively acts as a single source of truth for the transitional color, so updates to the tone can be made in one place. ## Notes -- Hard-coded strings hinder localization; consider turning this into a resource string if multi-language support is needed. -- As a private const, the value is fixed at compile time; changing the default requires recompilation and re-deployment. +- Being private, external code cannot reference `StatusTransitionalAttr`. If broader reuse is required, consider exposing an internal/public accessor or relocating the attribute to a shared styling layer. --- -## F6Key +### TabKey > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode F6Key = KeyCode.F6 +private const KeyCode TabKey = KeyCode.Tab ``` -F6Key is a private compile-time constant that represents the F6 keyboard key. Use F6Key in input-handling logic within MainWindow to detect the F6 press instead of sprinkling the literal KeyCode.F6 throughout the code. +This private constant, `TabKey`, provides a single, named reference to the `KeyCode.Tab` value used in the UI input handling within `src/EchoHub.Client/UI/MainWindow.cs`. It avoids scattering the literal `KeyCode.Tab` across the codebase, making tab-key checks more readable and easier to update if the navigation key changes. ## Remarks -Centralizes the key mapping to avoid duplicating KeyCode.F6 and to express intent clearly within the class. Since it is private and const, its value is inlined at compile time and not exposed publicly, keeping the wiring internal to MainWindow. If the F6 binding ever needs to be shared or changed, you would introduce a more general configuration mechanism or expose a public abstraction rather than duplicating the literal in multiple places. - -## Example -```csharp -// Example: demonstrate using the F6Key constant in a simple comparison -KeyCode current = KeyCode.F6; -if (current == F6Key) -{ - // handle F6 action -} -``` - -## Notes -- As a const, the value is baked into the assembly; changing it requires recompilation. -- Because it's private, external code cannot rely on this constant; testing and usage should interact with the class's public surface that uses F6Key. - +By centralizing the tab key choice in a private constant, this symbol communicates intent clearly within the class and reduces duplication in tab-navigation checks. If you ever need to switch the navigation key, update `TabKey` in one place rather than modifying multiple conditional branches. --- -## NewlineKey +### UsersPanelWidth > **File:** `src/EchoHub.Client/UI/MainWindow.cs` > **Kind:** field ```csharp -private const KeyCode NewlineKey = KeyCode.N | KeyCode.CtrlMask +private const int UsersPanelWidth = 22 ``` -NewlineKey is a private compile-time constant that represents the keyboard shortcut used to insert a newline in the UI. It encodes the N key combined with the Ctrl modifier by performing a bitwise OR between KeyCode.N and KeyCode.CtrlMask, allowing input handling to recognize the Ctrl+N shortcut as a single KeyCode value rather than separate checks for a key and a modifier. +Defines the fixed width of the users panel in the main window as a private constant `UsersPanelWidth`, centralizing the panel’s sizing decisions. When adjusting the layout, developers should reference this constant rather than sprinkling literal values, ensuring consistent alignment across the UI and a single point for future tweaks. ## Remarks -Centralizes the shortcut in a single symbol, reducing duplication and avoiding magic numbers in input logic. Being private, it remains an implementation detail of the MainWindow UI, so external code should not rely on it. The input-handling code likely compares the current KeyCode to NewlineKey to trigger newline insertion; using a named constant makes the intent explicit and easier to modify if the shortcut changes. - -## Notes -- The value is a compile-time constant; changing the shortcut requires modifying the code and recompiling. -- The combo uses KeyCode.CtrlMask; ensure consistency with other Ctrl-modified shortcuts in the same area. -- On platforms where modifier handling differs, verify that Ctrl+N is recognized as intended. +This constant encapsulates a UI sizing decision that would otherwise be repeated across multiple layout expressions. Keeping it private to the `MainWindow` class communicates that the width is an implementation detail of the window’s layout, while still enabling reuse and straightforward changes if the design evolves. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Constants/HubConstants.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Constants/HubConstants.cs.md index 16fd638..6f75104 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Constants/HubConstants.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Constants/HubConstants.cs.md @@ -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 hub’s 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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Constants/MessageConventions.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Constants/MessageConventions.cs.md index ab95b11..091fab3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Constants/MessageConventions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Constants/MessageConventions.cs.md @@ -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. \ No newline at end of file +- `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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md index b4a0fe8..3b2fb2c 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChannelService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChannelService.cs.md index 19003f4..7aff830 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChannelService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChannelService.cs.md @@ -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"] -PaginatedResponse -->|"items"| ChannelDto["ChannelDto"] -IChannelService -->|"GetChannelByNameAsync"| ChannelDto -IChannelService -->|"CreateChannelAsync / UpdateTopicAsync / SetChannelPasswordAsync / RekeyChannelAsync / DeleteChannelAsync"| ChannelOperationResult["ChannelOperationResult"] -IChannelService -->|"GetChannelListAsync"| ChannelListItem["List"] -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md index 995df3f..ce79df4 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md @@ -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 { 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. \ No newline at end of file +- `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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatService.cs.md index 5255846..d783839 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IChatService.cs.md @@ -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. \ No newline at end of file +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` (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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md index 2fbbc96..714be07 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md index 68bb85d..1d699c2 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md @@ -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 = ...; -string plain = "customer-secret"; -string cipher = service.Encrypt(plain); -string decrypted = service.Decrypt(cipher); // == plain +```csharp +// Assume you have an instance of IMessageEncryptionService named `service` +string ciphertext = service.Encrypt("TopSecret"); +string plaintext = service.Decrypt(ciphertext); // "TopSecret" -string? nullablePlain = null; -string? nullableCipher = service.EncryptNullable(nullablePlain); // null -string? nullableDecrypted = service.DecryptNullable(nullableCipher); // null +// Decrypting non-encrypted content yields the original value (pass-through) +string passthrough = service.Decrypt("plain-text"); // "plain-text" -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IUserService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IUserService.cs.md index 624a30d..fc22192 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IUserService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Contracts/IUserService.cs.md @@ -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` for mutating operations and `Task` 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. \ No newline at end of file + +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AccountDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AccountDtos.cs.md index a0e7746..cac28fb 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AccountDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AccountDtos.cs.md @@ -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 user’s 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` | — | -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md index 0646906..3ca24aa 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md index 1bb86b0..41bffae 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md @@ -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 channel’s 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 channel’s 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 history = new List(); -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` `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. It’s 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 user’s `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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md index cba7cad..efcdff9 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md @@ -32,14 +32,21 @@ public record ApiResponse(bool Success, string? Message = null, List? Er | `Errors` | `List?` | `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?` `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`, 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 { "Name is required", "Email is invalid" }); +``` ## Notes -- The Errors property is a mutable `List`—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`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList` 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(bool Success, string? Message = null, List? | `Data` | `T?` | `default` | -`ApiResponse` 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` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`. ## Remarks -`ApiResponse` 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` 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; -// success with data -var result = new ApiResponse(true, "Operation completed", null, "payload"); +```csharp +// Successful response with data +var success = new ApiResponse(true, "Operation completed", null, "payload-data"); -// error with details -var failure = new ApiResponse(false, "Validation failed", new List { "Email is invalid" }, null); +// Failed response with errors +var failure = new ApiResponse(false, "Validation failed", new List { "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` 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(List 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` 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( - Items: new List { 1, 2, 3 }, - Total: 10, - Offset: 0, - Limit: 3 -); -``` +Using a `record` for `PaginatedResponse` 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`, which is mutable. Mutating the list after construction will affect the PaginatedResponse instance. If you require immutability of the collection, consider exposing `ReadOnlyCollection` or `IReadOnlyList` instead of `List`, or wrap the list before returning. -- Because `PaginatedResponse` is a record, the wrapper itself uses value-based equality, but the `List` contained in Items is compared by reference. Two instances with equal contents but different `List` instances will not compare equal. +- The `Items` collection is a `List`, 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/InviteDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/InviteDtos.cs.md index ac4f63b..ed8f37a 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/InviteDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/InviteDtos.cs.md @@ -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 don’t 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md index 484e9b7..5afb92d 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md index c062098..499227a 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md @@ -27,21 +27,7 @@ public record AvatarUploadResponse(string AvatarAscii) | `AvatarAscii` | `string` | — | -AvatarUploadResponse is a tiny, immutable data container that represents the server’s 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ServerDtos.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ServerDtos.cs.md index f060c2a..66042a3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ServerDtos.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/DTOs/ServerDtos.cs.md @@ -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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/Attachment.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/Attachment.cs.md index 0a8dffe..1d244b5 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/Attachment.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/Attachment.cs.md @@ -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. \ No newline at end of file + +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/AttachmentKind.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/AttachmentKind.cs.md index a91f351..52675ff 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/AttachmentKind.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/AttachmentKind.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/Channel.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/Channel.cs.md index 7ec5eb8..37e4cd3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/Channel.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/Channel.cs.md @@ -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). \ No newline at end of file +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<Message>`) contains the related [`Message`](Message.cs.md) entities that belong to this channel. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/ChannelMembership.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/ChannelMembership.cs.md index 6043003..b792a5e 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/ChannelMembership.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/ChannelMembership.cs.md @@ -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. \ No newline at end of file +- `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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/InviteCode.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/InviteCode.cs.md index d4cc18a..c922ba8 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/InviteCode.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/InviteCode.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/Message.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/Message.cs.md index 64d0dee..27c5abe 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/Message.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/Message.cs.md @@ -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. \ No newline at end of file +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 + { + 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/MessageType.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/MessageType.cs.md index f58d73e..5ac2381 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/MessageType.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/MessageType.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/RefreshToken.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/RefreshToken.cs.md index dbae3c8..0cb0d78 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/RefreshToken.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/RefreshToken.cs.md @@ -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 token’s 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerRole.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerRole.cs.md index 72d67b6..8305092 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerRole.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerRole.cs.md @@ -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 (0–3) can affect serialized data; coordinate evolution across all consumers to preserve compatibility. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerStatsReport.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerStatsReport.cs.md index d41701f..fcf5bb0 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerStatsReport.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/ServerStatsReport.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/User.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/User.cs.md index 83a9caf..de2eda5 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/User.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/User.cs.md @@ -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. It’s 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Models/UserStatus.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Models/UserStatus.cs.md index 5b390c8..833cb31 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Models/UserStatus.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Models/UserStatus.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Security/RoomCrypto.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Security/RoomCrypto.cs.md index 5d2e3c3..33ba8cb 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Security/RoomCrypto.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Security/RoomCrypto.cs.md @@ -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). \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md index 162c456..0134210 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md @@ -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. \ No newline at end of file +- 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. diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md index e3d49e8..faddf6f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md @@ -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 name’s 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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md b/docs/auriondocs/Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md index e0f2d8c..fc79fbf 100644 --- a/docs/auriondocs/Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md @@ -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. \ No newline at end of file +- 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. _options.ServerName ``` -This private read-only property exposes the server name configured in the handler’s 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 isn’t 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 user’s 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 connection’s 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 : PONG :. - -## 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 { "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: ()`. --- @@ -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 RequireRegisteredAsync() **Returns:** `Task` -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 isn’t 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 `# :` when a topic is set, or sends `RPL_NOTOPIC` with `# :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 class’s _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 channel’s 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. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcGatewayService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcGatewayService.cs.md index f734e4a..e0a7aab 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcGatewayService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcGatewayService.cs.md @@ -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. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessage.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessage.cs.md index 12e4dcb..f352242 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessage.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessage.cs.md @@ -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` 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. \ No newline at end of file +- The `Parameters` collection is a mutable `List`; 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md index 22f08cb..5d0a33c 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md @@ -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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md index d2f0257..4d45503 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md @@ -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 protocol’s 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"). \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcOptions.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcOptions.cs.md index bd298b7..d5b1875 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcOptions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcOptions.cs.md @@ -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". \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md index 0e43015..d7f3606 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md @@ -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().() \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Auth/JwtTokenService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Auth/JwtTokenService.cs.md index 17b497a..1b65992 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Auth/JwtTokenService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Auth/JwtTokenService.cs.md @@ -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`. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Config/ServerLogsOptions.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Config/ServerLogsOptions.cs.md index 9fd7d77..7e53eb6 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Config/ServerLogsOptions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Config/ServerLogsOptions.cs.md @@ -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. \ No newline at end of file +`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`. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Config/SpamOptions.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Config/SpamOptions.cs.md index 4be0af9..c3f923d 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Config/SpamOptions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Config/SpamOptions.cs.md @@ -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 won’t 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 don’t 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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Config/StatsOptions.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Config/StatsOptions.cs.md index 4e89dca..2ef2a03 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Config/StatsOptions.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Config/StatsOptions.cs.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Config/UploadLimits.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Config/UploadLimits.cs.md index 286e96a..5f36137 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Config/UploadLimits.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Config/UploadLimits.cs.md @@ -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. \ No newline at end of file +- 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`. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/AuthController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/AuthController.cs.md index 11a7eeb..a7c409b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/AuthController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/AuthController.cs.md @@ -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. \ No newline at end of file +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). \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md index 6dcc10b..9da09be 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md @@ -5,18 +5,18 @@ ## Contents - [ChannelsController](#channelscontroller) - - [ChannelsController (constructor)](#channelscontroller-constructor) - [CreateChannel](#createchannel) - [DeleteChannel](#deletechannel) - [GetChannelCrypto](#getchannelcrypto) + - [GetChannelMeta](#getchannelmeta) + - [GetChannels](#getchannels) - [MapChannelError](#mapchannelerror) - [ParseKind](#parsekind) -- [GetChannelMeta](#getchannelmeta) -- [GetChannels](#getchannels) -- [RekeyChannel](#rekeychannel) -- [SendMessageWithAttachments](#sendmessagewithattachments) -- [SendUrl](#sendurl) -- [UpdateTopic](#updatetopic) + - [RekeyChannel](#rekeychannel) + - [SendMessageWithAttachments](#sendmessagewithattachments) + - [SendUrl](#sendurl) + - [UpdateTopic](#updatetopic) +- [ChannelsController (constructor)](#channelscontroller-constructor) --- @@ -33,60 +33,15 @@ public class ChannelsController : ControllerBase ``` -Exposes the HTTP surface for channel-related operations under the route prefix api/channels. Authenticated clients use this controller to list and create channels, retrieve public crypto metadata and human-facing channel summaries, perform passphrase rewraps (rekey), update topics, delete channels, and post messages (including multipart uploads). Prefer calling these endpoints from client code or tests; use the underlying services (IChannelService, IMessageEncryptionService, etc.) directly only when you need to bypass HTTP semantics or perform server-side orchestration. +Exposes the channel-oriented HTTP API beneath `api/channels` for listing, creating, updating and deleting channels, for retrieving channel metadata and public crypto parameters, for changing an encrypted channel's passphrase, and for posting messages (including attachments). Reach for `ChannelsController` when implementing server-side channel management or wiring client HTTP calls: it is the main HTTP surface that enforces authentication, rate limits and upload policies for channel operations. ## Remarks -This controller is a thin HTTP façade that orchestrates several backend services rather than implementing business logic itself. It enforces [Authorize] and configurable rate-limiting (attributes show general and upload policy groups) and delegates persistence, file storage, ASCII preview generation, encryption operations, and chat routing to injected dependencies such as IChannelService, EchoHubDbContext, FileStorageService, ImageToAsciiService, IMessageEncryptionService, IChatService and UploadLimits. Upload size and multipart limits are applied at runtime using the UploadLimits configuration rather than compile-time attributes so the controller can honor configurable limits for large attachments. +`ChannelsController` is a thin HTTP façade that delegates domain work to services such as [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md), [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) and [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) while persisting metadata via [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md). It centralizes cross-cutting concerns: request authorization (`[Authorize]`), rate limiting (the controller is annotated with `EnableRateLimiting("general")` and the attachment upload endpoint uses `EnableRateLimiting("upload")`), runtime-configured upload limits via the injected [`UploadLimits`](../Config/UploadLimits.cs.md), file handling via [`FileStorageService`](../Services/FileStorageService.cs.md), and image preview generation via [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md). The controller intentionally keeps cryptographic secrets off the public endpoints — for example, `GetChannelCrypto` returns only public metadata (including the PBKDF2 salt) and never hands out the wrapped room key; `RekeyChannel` re-wraps a channel's room key without re-encrypting historical messages. ## Notes -- GetChannelCrypto returns public crypto metadata and the PBKDF2 salt clients need to derive a join credential; it never returns the wrapped room key (that is issued only after a successful join). -- RekeyChannel re-wraps the room key to change the passphrase; historical messages are not re-encrypted (the room content key itself does not change). -- SendMessageWithAttachments applies upload limits at runtime from UploadLimits; the controller trusts clients for encrypted-channel attachments (clients must declare each file's kind and provide room-encrypted previews), while for non-encrypted channels the server may inspect files and generate ASCII previews for images. - ---- - -### ChannelsController (constructor) -> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` -> **Kind:** constructor - -```csharp -public ChannelsController( - IChannelService channelService, - EchoHubDbContext db, - FileStorageService fileStorage, - ImageToAsciiService asciiService, - IHttpClientFactory httpClientFactory, - IChatService chatService, - IMessageEncryptionService encryption, - UploadLimits uploadLimits, - ILogger logger) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `channelService` | [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) | — | -| `db` | [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) | — | -| `fileStorage` | [`FileStorageService`](../Services/FileStorageService.cs.md) | — | -| `asciiService` | [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md) | — | -| `httpClientFactory` | `IHttpClientFactory` | — | -| `chatService` | [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) | — | -| `encryption` | [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) | — | -| `uploadLimits` | [`UploadLimits`](../Config/UploadLimits.cs.md) | — | -| `logger` | `ILogger` | — | - - -The ChannelsController constructor wires up the controller by receiving its dependencies through dependency injection and assigning them to private fields. This pattern allows the controller to orchestrate channel-related functionality by delegating to dedicated services such as IChannelService, EchoHubDbContext, FileStorageService, ImageToAsciiService, IHttpClientFactory, IChatService, IMessageEncryptionService, UploadLimits, and `ILogger`. The framework supplies these collaborators at creation time, enabling a testable, loosely coupled design where concerns are separated and easily mockable for unit tests. This constructor is invoked by the ASP.NET Core runtime during request handling, not by consumer code directly. - -## Remarks -The constructor centralizes the wiring of the controller's collaborators, which supports clean separation of concerns and testability. It enables the ChannelsController to delegate specialized tasks (e.g., data access, file handling, image processing, HTTP calls, chat interactions, and encryption) to dedicated services rather than embedding logic directly. - -The lack of explicit null validation means misconfigured dependency injection (missing service registrations) could surface as NullReferenceExceptions later when members are used. Relying on the DI container to validate registrations is common, but tests should provide explicit mocks to ensure predictable behavior. - -## Notes -- The constructor does not perform null checks; ensure all dependencies are registered in the DI container to avoid runtime null reference issues. -- When writing unit tests for ChannelsController, provide concrete or mock implementations for all injected services to exercise behavior reliably. +- The server does not attempt to decrypt or inspect message contents for end-to-end encrypted channels; encrypted attachments must be uploaded as ciphertext and the client must provide the declared `kind` and the room-encrypted `preview` aligned with attachment order. The controller treats those blobs as opaque. +- Request body and multipart limits are applied at runtime from the injected [`UploadLimits`](../Config/UploadLimits.cs.md) rather than using compile-time attributes like `[RequestSizeLimit]`. The implementation raises the request body ceiling from [`UploadLimits`](../Config/UploadLimits.cs.md) before the body is read to support configurable upload maxima. +- `RekeyChannel` changes how the room key is wrapped (the passphrase) but does not re-encrypt existing history — the underlying room content key remains the same, so historical ciphertext is not rewritten. --- @@ -109,47 +64,7 @@ The lack of explicit null validation means misconfigured dependency injection (m public async `Task`` -Source Code -The CreateChannel action handles the HTTP POST to create a new channel for the authenticated user. It first verifies authentication by pulling the user ID from the current user’s claims; if the claim is missing, it responds with Unauthorized and an ErrorResponse indicating that authentication is required. It then delegates the actual creation to the channel service via CreateChannelAsync, passing the caller’s GUID along with the channel properties supplied in the request (Name, Topic, IsPublic, Password, EncryptionSalt, WrappedRoomKey). If the service reports a failure, the action returns a mapped error via MapChannelError. If a channel is successfully created and it is public, it broadcasts the updated channel through the chat service to notify connected clients. Finally, it returns a 201 Created response with the location of the new channel and the Channel data in the response body. - -Dependencies -- IActionResult -- ErrorResponse -- User -- ClaimTypes -- Guid -- Channel - -Dependency APIs (verified signatures) -- record [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) (`src/EchoHub.Core/DTOs/CommonDtos.cs`) -- property [`User`](../../EchoHub.Core/Models/User.cs.md) (`src/EchoHub.Core/Models/RefreshToken.cs`) -- class [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) (`src/EchoHub.Core/Models/Channel.cs`) - - `Guid Id` - - `string Name` - - `string? Topic` - - `bool IsPublic` - - `bool IsSystem` - - `string? PasswordHash` - - `string? EncryptionSalt` - - `string? WrappedRoomKey` - - `DateTimeOffset CreatedAt` - - `Guid CreatedByUserId` - - `List Messages` - -Symbol To Document -- Name: CreateChannel -- Kind: method -- File: src/EchoHub.Server/Controllers/ChannelsController.cs -- Language: csharp -- ID: 373f63c8-2a87-460c-9821-46640d93a9fc - -## Remarks -Creates a channel on behalf of the authenticated user and encapsulates the orchestration between the domain service and the HTTP response surface. It relies on _channelService to enforce business rules and persistence, and on _chatService to refresh client views when appropriate. This action adheres to RESTful semantics by returning 401 for unauthenticated requests, propagating domain errors via MapChannelError, broadcasting updates for public channels, and signaling successful creation with 201 and the new channel resource. - -## Notes -- Be aware that Guid.Parse is used on the user ID claim. If the claim value is not a valid GUID, this will throw. Consider validating with Guid.TryParse at the call site if you anticipate non-GUID claim values. -- The publication check (IsPublic) gates whether a channel update is broadcast to clients; non-public channels skip broadcasting to peers. - +Creates a new channel for the authenticated user via HTTP POST. It first authenticates by reading the `NameIdentifier` from `ClaimTypes.NameIdentifier` in [`User`](../../EchoHub.Core/Models/User.cs.md); if missing, it returns `Unauthorized` with an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md). On success, it calls `_channelService.CreateChannelAsync` with the parsed GUID from the `NameIdentifier` claim and the fields from `request` (`Name`, `Topic`, `IsPublic`, `Password`, `EncryptionSalt`, `WrappedRoomKey`). If the result indicates failure, it returns the mapped error via `MapChannelError`. If the created channel is public, it notifies clients by calling `_chatService.BroadcastChannelUpdatedAsync`. Finally it returns `Created` with the new channel at `/api/channels/{result.Channel.Name}`. --- @@ -169,15 +84,15 @@ Creates a channel on behalf of the authenticated user and encapsulates the orche | `"{channel}"` | — | — | -Deletes a channel for the authenticated user by handling an HTTP DELETE request to the channel route. It reads the user's ID from the authentication claims, delegates the deletion to the channel service using that ID and the channel name, and, on success, broadcasts the deletion to the chat service before returning HTTP 204 No Content. If authentication is missing, the method responds with 401 Unauthorized and an ErrorResponse. +Deletes a channel for the currently authenticated user. It first validates authentication by reading the `NameIdentifier` claim from [`User`](../../EchoHub.Core/Models/User.cs.md) and returns `Unauthorized` with an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) if missing; otherwise it calls `_channelService.DeleteChannelAsync` with the parsed `Guid` user id and the provided `channel` name. If the deletion succeeds it broadcasts the channel deletion to the chat subsystem via `_chatService.BroadcastChannelDeletedAsync` (the channel name lowercased and trimmed) and returns `NoContent`; if it fails, it returns the mapped error using `MapChannelError`. ## Remarks -This endpoint acts as a thin HTTP boundary that orchestrates authentication, domain deletion, and cross-service notification. It centralizes HTTP-level error handling (Unauthorized, error mapping) while delegating business rules to the channel service and the side-effect of notifying the chat service. The normalization of the channel name for the broadcast (lowercase and trimmed) helps ensure consumers react to a consistent channel identifier. + +This method acts as an orchestration boundary, ensuring only authenticated users can delete their channels and coordinating the domain operation with cross-service notification to keep clients in sync. ## Notes -- Authentication is required; requests without a valid NameIdentifier claim result in 401 Unauthorized with an ErrorResponse. -- The broadcast step uses channel.ToLowerInvariant().Trim(); differences between input casing and broadcast casing could affect downstream consumers. -- If the channel is deleted successfully but the broadcast fails, the method will surface a failure (no explicit retry here); consider compensating actions if eventual consistency is important. + +- Potential exception if the `NameIdentifier` claim isn't a valid GUID; consider using `Guid.TryParse` or additional validation. --- @@ -197,137 +112,123 @@ This endpoint acts as a thin HTTP boundary that orchestrates authentication, dom | `"{channel}/crypto"` | — | — | -GetChannelCrypto is an HTTP GET action on ChannelsController that exposes essential cryptographic metadata for a channel. It indicates whether the channel is end-to-end encrypted and provides the PBKDF2 salt clients need to derive their join credential. The endpoint deliberately does not return the wrapped room key; that secret is only handed out after a successful join. The action delegates retrieval to the channel service and translates the result into standard HTTP responses: 200 OK with the crypto data when the channel exists, or 404 Not Found with an ErrorResponse if the channel does not exist. +GetChannelCrypto is an HTTP GET endpoint that returns the public crypto metadata for a given channel, including whether the channel is end-to-end encrypted and the PBKDF2 salt used to derive the join credential. It never returns the wrapped room key; if the channel doesn't exist, the endpoint responds with `NotFound` and an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md); otherwise it returns the metadata with an `Ok(crypto)` result. ## Remarks -By wrapping the service call behind a minimal HTTP surface, this symbol centralizes how cryptographic metadata is surfaced while keeping the actual cryptographic material protected. It demonstrates a clear separation of concerns: business logic lives in the ChannelService, while the controller handles HTTP semantics and error translation. The exposed salt enables client-side credential derivation, while the wrapped key remains strictly withheld until the proper join flow. +This endpoint centralizes crypto-configuration retrieval for a channel, keeping actual keys out of reach and clarifying that the response is metadata only. It delegates to `_channelService.GetChannelCryptoAsync(channel)` to obtain the data and uses the 404/not-found path to signal missing channels or missing crypto metadata. It sits in the `ChannelsController` and complements the security model by exposing minimal, auditable information required by clients to participate in encrypted joins. ## Notes -- The action does not perform explicit authorization; ensure the surrounding middleware or route configuration enforces the intended access policy. -- It returns 404 with a generic ErrorResponse when the channel does not exist; clients should handle this scenario as an absence of channel crypto metadata. -- Do not rely on this endpoint to retrieve any sensitive material beyond allowed cryptographic metadata; the wrapped key must never be exposed through this action. +- If `_channelService.GetChannelCryptoAsync(channel)` returns null, the API responds with 404 via the same messaging, conflating a missing channel with missing crypto metadata. +- The endpoint does not expose any cryptographic material beyond the publicly exposable metadata; actual keys are never returned. --- -### MapChannelError +### GetChannelMeta > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method ```csharp -private IActionResult MapChannelError(ChannelOperationResult result) => result.Error switch +[HttpGet("{channel}/meta")] + public async Task GetChannelMeta(string channel) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `result` | [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) | — | - -**Returns:** `IActionResult` - - -Converts a ChannelOperationResult into an API response by pattern-matching on result.Error and returning an appropriate HTTP result that wraps an ErrorResponse. It centralizes the translation from channel-domain errors to standard HTTP status codes (400, 403, 404, 409) so the rest of the controller does not duplicate error handling logic. - -## Remarks -This encapsulates the error-handling policy for channel operations, ensuring clients see consistent HTTP semantics across all channel actions. It decouples domain error codes from HTTP choices, so updates to status codes or payload shapes can be made in one place rather than at every call site. +| `"{channel}/meta"` | — | — | -## Notes -- The branches pass result.ErrorMessage! into ErrorResponse; if ErrorMessage can be null for any mapped error, this will throw at runtime. -- New ChannelError values require extending this switch to preserve the API's error contract. +GetChannelMeta exposes channel metadata for a given `channel` via HTTP GET to `"{channel}/meta"`. It delegates to `_channelService.GetChannelMetaAsync(channel)` to assemble metadata such as message count, unique posters, estimated size, creation date, and room id. This remains available for encrypted channels as well since the server tracks this metadata independent of the messages. If the channel does not exist, the endpoint returns `NotFound(new ErrorResponse($"Channel '{channel}' does not exist."))`; otherwise it returns the metadata payload with `Ok(meta)`. --- -### ParseKind +### GetChannels > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method ```csharp -private static AttachmentKind ParseKind(string? kind) => kind?.ToLowerInvariant() switch +[HttpGet] + public async Task GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `kind` | `string?` | — | +| `offset` | `int` | `0` | +| `limit` | `int` | `50` | -**Returns:** [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md) +**Returns:** `[HttpGet] + public async `Task`` -This private helper translates a nullable string that labels an attachment into a concrete AttachmentKind enum. It uses a case-insensitive comparison (ToLowerInvariant) to recognize 'image' and 'audio' and map them to AttachmentKind.Image and AttachmentKind.Audio, respectively; any other label (including null) falls back to AttachmentKind.File. Callers rely on this mapping when normalizing incoming attachment metadata before further processing in the channel/server pipeline. +GetChannels is an HTTP GET endpoint on the `ChannelsController` that returns a paged list of channels for the currently authenticated user. It reads the query parameters `offset` and `limit`, clamps them to sane bounds, parses the user GUID from the `NameIdentifier` claim, delegates to `_channelService.GetChannelsAsync(Guid.Parse(userIdClaim), offset, limit)`, and returns the data in an `Ok` response. ## Remarks -Centralizes the normalization logic so all attachment-kind labels are interpreted consistently across the server. By funneling strings through this method, the rest of the attachment processing can operate on a well-defined enum, reducing branching and potential mismatches. +As an HTTP boundary, this method coordinates authentication and paging concerns, keeping the controller thin by delegating data retrieval to `_channelService.GetChannelsAsync(...)`. It relies on [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) to signal authentication failures and on the service to fetch domain data, forming a simple, testable conduit between the HTTP layer and business logic. ## Notes -- Unknown labels are treated as File by design; if a new kind is introduced, update this method or extend the enum. -- Because the method is private, it's exercised via the class's public APIs; ensure tests cover scenarios that exercise this mapping through those entry points. +- It assumes the `NameIdentifier` claim contains a valid GUID; if not, `Guid.Parse` will throw. Consider using `Guid.TryParse` or stricter claim validation to avoid runtime exceptions. --- -## GetChannelMeta +### MapChannelError > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method ```csharp -[HttpGet("{channel}/meta")] - public async Task GetChannelMeta(string channel) +private IActionResult MapChannelError(ChannelOperationResult result) => result.Error switch ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `"{channel}/meta"` | — | — | +| `result` | [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) | — | +**Returns:** `IActionResult` -Retrieves the channel metadata for a given channel identifier via HTTP GET. It returns key overview details such as message count, the number of unique posters, an estimated size, the creation date, and the room id. These metadata are tracked by the server and are available even for encrypted channels, where the server cannot access the actual messages. If the channel does not exist, it responds with 404 and an ErrorResponse; otherwise it returns the metadata payload with a 200 OK. + +MapChannelError is a private helper in `ChannelsController` that translates a [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) into an HTTP response by switching on `result.Error`. It centralizes the mapping from domain channel errors (the [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) enum) to HTTP status results, covering common cases: `ChannelError.ValidationFailed` yields a `BadRequest` with an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) payload containing the error message, `ChannelError.AlreadyExists` yields `Conflict`, `ChannelError.NotFound` yields `NotFound`, `ChannelError.Forbidden` yields a 403 via `StatusCode(403, ...)`, and `ChannelError.Protected` yields `BadRequest`; any unlisted error falls back to a `BadRequest` with either the provided `ErrorMessage` or the string `"Unknown error."`. All branches construct the error payload with `new ErrorResponse(result.ErrorMessage!)` (except the fallback) to deliver structured error information to the client. ## Remarks -This endpoint provides a read-only surface for obtaining channel overview information without exposing message contents. It enables clients to populate channel lists or dashboards while preserving message privacy, including for encrypted channels. By delegating the data retrieval to _channelService.GetChannelMetaAsync, the API keeps data access concerns centralized and allows the underlying storage/collection strategy to evolve without changing the surface contract. +This helper encapsulates the error-to-HTTP translation for channel operations, ensuring consistent client-facing semantics across the controller. By funneling all channel-related errors through a single switch, changes to HTTP status mappings or payload shape can be made in one place. The method returns an `IActionResult` and always uses an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) payload to provide a predictable error contract to clients; callers do not need to repeat boilerplate error handling. ## Notes -- The caller must handle a 404 NotFound with an ErrorResponse when the channel is missing. The error payload documents the failure reason. -- The endpoint exposes only metadata about a channel; actual messages remain inaccessible, preserving privacy for encrypted channels. -- The operation is asynchronous; consider service performance characteristics or potential caching strategies if metadata is requested frequently. +- The code uses the null-forgiving operator on `ErrorMessage` in most branches; ensure `ErrorMessage` is populated for those [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) values, or risk a runtime null allocation. +- The default branch returns a `BadRequest` with either the provided message or a fallback of `"Unknown error."`, which avoids leaking a null payload but may obscure the underlying error if messages are not consistently set. --- -## GetChannels +### ParseKind > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method ```csharp -[HttpGet] - public async Task GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50) +private static AttachmentKind ParseKind(string? kind) => kind?.ToLowerInvariant() switch ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `offset` | `int` | `0` | -| `limit` | `int` | `50` | +| `kind` | `string?` | — | -**Returns:** `[HttpGet] - public async `Task`` +**Returns:** [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md) -Gets a paged list of channels for the authenticated user. It enforces authentication by checking the user claims, reads the user's GUID from the claims, normalizes paging parameters (offset non-negative; limit clamped to 1–100), and delegates to the channel service to retrieve the channels, returning the result in an HTTP 200 response. +Converts an optional string describing an attachment into the corresponding [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md) enum value. It normalizes the input with `ToLowerInvariant()` and returns `AttachmentKind.Image` for `image`, `AttachmentKind.Audio` for `audio`, or `AttachmentKind.File` for any other value (including when the input is `null`). ## Remarks - -This action is intentionally thin: it performs authentication, input normalization, and orchestration between the API layer and the domain service. Centralizing paging bounds and user identification here provides consistent behavior and error handling for per-user channel retrieval across clients. +By centralizing this mapping in a private helper, the server ensures consistent classification of attachments across callers and makes future changes to the mapping straightforward. The use of `ToLowerInvariant()` guarantees predictable behavior regardless of the runtime culture. ## Notes - -- Be aware that if the NameIdentifier claim is present but is not a valid GUID, Guid.Parse will throw. Prefer Guid.TryParse or ensure identity claims are well-formed. -- The limit is clamped to the range 1–100; requests outside that range are adjusted to the nearest bound. +- If a new attachment kind is introduced, this method must be updated; otherwise unknown values default to `AttachmentKind.File`. --- -## RekeyChannel +### RekeyChannel > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method @@ -343,18 +244,20 @@ This action is intentionally thin: it performs authentication, input normalizati | `"{channel}/rekey"` | — | — | -Changes an encrypted channel's passphrase by re-wrapping its room key. The caller proves knowledge of the existing passphrase via the old authentication key, and this operation preserves history by not changing the room content key. +RekeyChannel rotates an encrypted channel's passphrase by re-wrapping its room key. It authenticates the caller via the `ClaimTypes.NameIdentifier` claim and requires knowledge of the old password (provided as `OldPassword` in the request) to authorize the change; the operation preserves history by not re-encrypting the room content key. ## Remarks -RekeyChannel acts as a thin HTTP boundary that enforces authentication and delegates the cryptographic work to the channel service. By re-wrapping the existing room key instead of re-encrypting the historical content, it minimizes disruption while changing access controls. The controller handles authentication and error translation, while RekeyChannelAsync encapsulates the cryptographic policy in the domain layer. +RekeyChannel delegates the actual rotation to `_channelService.RekeyChannelAsync`, which performs the rewrapping logic and returns a result. If the operation succeeds, the updated channel is returned with `Ok`, otherwise `MapChannelError` translates failures into the appropriate HTTP error response. The endpoint is exposed at the route `"{channel}/rekey"`, enforcing authentication at the boundary via the user identity claim. ## Notes -- Authentication is mandatory; if the user is not authenticated, the endpoint returns 401 Unauthorized with ErrorResponse("Authentication required."). -- The code uses Guid.Parse on the NameIdentifier claim; if the claim is present but not a valid GUID, a runtime exception may be thrown. +- Authentication relies on the presence of the `NameIdentifier` claim in [`User`](../../EchoHub.Core/Models/User.cs.md); if it is missing, the method responds with `Unauthorized(new ErrorResponse("Authentication required."))`. +- The code calls `Guid.Parse(userIdClaim)` on the claim value; if the `NameIdentifier` claim is present but not a valid GUID, an exception could be thrown at runtime. +- This operation re-wraps the room key to rotate the channel's passphrase without altering the underlying room content key, preserving historical data while changing access material. + --- -## SendMessageWithAttachments +### SendMessageWithAttachments > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method @@ -371,22 +274,21 @@ RekeyChannel acts as a thin HTTP boundary that enforces authentication and deleg | `"{channel}/messages"` | — | — | -Use SendMessageWithAttachments when you need to post a chat message to a channel that includes optional text and one or more attachments, while enforcing per-channel upload limits and channel permissions. +SendMessageWithAttachments posts a single chat message to a named channel, optionally including plaintext `content` and zero or more attachments delivered as multipart form data. -It supports both cleartext and end-to-end encrypted channels: in cleartext channels the server inspects file kinds to render ASCII previews and decrypts the content, while in encrypted channels the client uploads ciphertext with per-file kind and a pre-rendered encrypted preview and the server never inspects the ciphertext. +For non-encrypted channels the server may inspect attachments and render ASCII previews; for room-encrypted channels the client supplies ciphertext with per-file `kind` and a pre-rendered `preview`, and the server never inspects the ciphertext. -## Remarks +The action enforces runtime upload limits, validates authentication and channel state, requires multipart content with at least one attachment, and observes per-channel constraints such as maximum attachments per message and maximum message length for non-encrypted content. -This endpoint centralizes the server-side orchestration for uploading messages with attachments, coordinating authentication via user claims, channel validation and mutability checks, multipart form handling, per-attachment processing, and interaction with the encryption and upload-limit subsystems. It relies on collaborators such as the channel service, the database context, and the encryption helper to enforce read-only channels, mute state, and maximum message length in a consistent manner. By encapsulating these concerns, it ensures secure, policy-compliant message delivery and prevents plaintext exposure of encrypted payloads. In short, it is the single integration point for sending rich messages with attachments in EchoHub.Server. +## Remarks +`SendMessageWithAttachments` is a boundary between the chat surface and the attachment pipeline. It coordinates authentication, channel resolution, and per-channel policy (read-only channels, allowed attachment counts, and length limits), then delegates the heavier lifting of encryption handling and persistence to the underlying services (`_encryption`, `_channelService`, and the database context). By centralizing multipart request handling and per-file metadata (such as `Attachment.Kind` and `Attachment.AsciiPreview`), it provides a single, secure entry point for composing rich messages that may include both plaintext and encrypted payloads, while ensuring that encrypted channels never disclose raw attachment data to the server. ## Notes - -- The request size is governed at runtime by UploadLimits; configure this to control maximum allowed payloads. -- For encrypted channels, ensure that per-file previews are provided in the ciphertext workflow and that file order remains aligned with the declared previews to avoid misrendering on the client. +- If the channel is encrypted, the endpoint relies on the client-provided per-file metadata (e.g., `kind` and `preview`) and does not perform server-side inspection of the ciphertext blobs; ensure consistency between client-provided metadata and channel state to avoid mismatches. --- -## SendUrl +### SendUrl > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method @@ -403,27 +305,33 @@ This endpoint centralizes the server-side orchestration for uploading messages w | `"{channel}/send-url"` | — | — | -SendUrl is an HTTP POST endpoint on ChannelsController that accepts a channel name, a request body containing an image URL, and an optional size parameter. It authenticates the caller, validates the channel, enforces channel policies (rejects system/read-only and end-to-end encrypted channels), validates the URL, downloads the image server-side, enforces size limits, validates the image format, saves the file, and generates an ASCII preview for display in the channel. +SendUrl is an HTTP POST action that enables an authenticated user to attach an image to a channel by URL. It coordinates authentication, channel validation, image download, format and size validation, storage, and ASCII preview generation, applying channel policies (such as read-only `IsSystem` channels and end-to-end encrypted `IsEncrypted` channels) before persisting the asset. ## Remarks +This action centralizes URL-based image delivery, delegating channel lookup to `` `_channelService` ``, remote download/validation to the HTTP client path, and persistence to ``_fileStorage``. It ensures that content is only added to writable channels and that encrypted channels disallow URL-based image sending, thereby reducing risk and keeping concerns isolated. The composition makes testing and reuse consistent with other upload flows in the codebase, leveraging collaborators such as [`FileValidationHelper`](../../EchoHub.Core/Services/FileValidationHelper.cs.md) for image validation and [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md) for the ASCII preview. -Centralizes remote image ingestion with strict, server-side validation to prevent improper content, inconsistent client behavior, or abuse. The endpoint relies on the application's security and storage abstractions: it checks user claims, ensures channel permissions, uses FileStorage to persist the file, and uses ImageToAsciiService to produce a lightweight ASCII representation for previews. The EnableRateLimiting("upload") attribute signals this is a potentially resource-intensive operation and should be throttled to guard against abuse. +## Example +```csharp +using System.Net.Http; +using System.Text; +using System.Text.Json; + +var payload = new { Url = "https://example.com/image.png" }; +var json = JsonSerializer.Serialize(payload); +using var content = new StringContent(json, Encoding.UTF8, "application/json"); +using var client = new HttpClient(); // configure base address and authentication as needed +var response = await client.PostAsync("/channels/general/send-url?size=1024", content); +``` ## Notes - -- Requires authentication; missing user claims yield Unauthorized responses with a helpful error. -- Validates channel state: if the channel does not exist, is system (read-only), or is encrypted, it responds with NotFound/403/400 and an ErrorResponse explaining the reason. -- Validates the supplied URL and only accepts http/https URLs; invalid URLs or unsupported schemes produce a BadRequest with a descriptive message. -- Downloads the image server-side using an HttpClient named "ImageDownload". It handles timeouts and HTTP errors by returning BadRequest with a clear message. -- Enforces file size limits via _uploadLimits.MaxImageSizeBytes before and after downloading the content. -- Validates that the downloaded content is a real image (JPEG, PNG, GIF, WebP) before persisting. -- Determines a filename from the URL or Content-Type; if missing, it falls back to a generated name with an appropriate extension. -- Persists the file and creates an ASCII representation (via ImageToAsciiService) for downstream use. - +- Requires authentication; requests without credentials yield `Unauthorized` with an [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md). +- Validates channel name via `ValidationConstants.ChannelNameRegex` and checks channel existence (`NotFound`) and state (`IsSystem` / `IsEncrypted`). +- Downloads the image using the named HttpClient `"ImageDownload"`, enforces the maximum size via `_uploadLimits.MaxImageSizeBytes`, and validates the actual image content with `FileValidationHelper.IsValidImage`. +- If the downloaded data cannot be interpreted as a supported image, returns a `BadRequest` with an explanatory message. --- -## UpdateTopic +### UpdateTopic > **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` > **Kind:** method @@ -439,15 +347,52 @@ Centralizes remote image ingestion with strict, server-side validation to preven | `"{channel}/topic"` | — | — | -Updates a channel's topic for the currently authenticated user via HTTP PUT. It verifies authentication, calls ChannelService.UpdateTopicAsync with the user's ID, the channel, and the new topic, and on success broadcasts the channel update before returning the updated channel; on failure or missing authentication, it yields an HTTP error. +UpdateTopic handles PUT requests to update a channel's topic for the authenticated user. It reads the `NameIdentifier` claim and, if missing, returns `Unauthorized(new ErrorResponse("Authentication required."))`; otherwise it calls `_channelService.UpdateTopicAsync(Guid.Parse(userIdClaim), channel, request.Topic)`, maps errors via `MapChannelError` on failure, and on success broadcasts the update with `_chatService.BroadcastChannelUpdatedAsync(result.Channel!, channel.ToLowerInvariant().Trim())` before returning `Ok(result.Channel)`. ## Remarks +This endpoint centralizes authentication checks and cross-service coordination for topic changes. It ensures only authenticated users can modify a channel topic and that updates are propagated to connected clients via the `_chatService.BroadcastChannelUpdatedAsync` call. -Acts as the HTTP API boundary for updating a channel topic, delegating the actual update to the domain service and handling authentication. It centralizes error translation via MapChannelError and ensures clients are informed of changes in real time by broadcasting after a successful update. +--- -## Notes +## ChannelsController (constructor) +> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs` +> **Kind:** constructor -- Be aware that Guid.Parse could throw if the user claim is not a valid GUID; consider Guid.TryParse to avoid runtime exceptions. -- The broadcast channel is normalized by lowercasing and trimming the channel name; this affects how subscribers perceive channel identifiers in updates. +```csharp +public ChannelsController( + IChannelService channelService, + EchoHubDbContext db, + FileStorageService fileStorage, + ImageToAsciiService asciiService, + IHttpClientFactory httpClientFactory, + IChatService chatService, + IMessageEncryptionService encryption, + UploadLimits uploadLimits, + ILogger logger) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `channelService` | [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) | — | +| `db` | [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) | — | +| `fileStorage` | [`FileStorageService`](../Services/FileStorageService.cs.md) | — | +| `asciiService` | [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md) | — | +| `httpClientFactory` | `IHttpClientFactory` | — | +| `chatService` | [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) | — | +| `encryption` | [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) | — | +| `uploadLimits` | [`UploadLimits`](../Config/UploadLimits.cs.md) | — | +| `logger` | `ILogger` | — | + + +The `ChannelsController` constructor wires the controller to its collaborators by accepting all required services via dependency injection and storing them for use in action methods. It is invoked by the ASP.NET Core DI container when handling channel-related requests, meaning developers should avoid manual instantiation and instead provide mocks or fakes for its dependencies in tests. + +## Remarks +By composing [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md), [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), [`FileStorageService`](../Services/FileStorageService.cs.md), [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md), `IHttpClientFactory`, [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md), [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md), [`UploadLimits`](../Config/UploadLimits.cs.md), and `ILogger` in a single place, the constructor positions `ChannelsController` as a coordinator that delegates work to specialized services. This composition reflects a separation of concerns across persistence, media processing, HTTP communication, chat orchestration, encryption, and logging. + +## Notes +- Do not instantiate `ChannelsController` yourself; rely on the DI container so tests can provide mocks or fakes. +- A constructor with many dependencies can indicate the controller has multiple responsibilities; consider extracting a higher-level service if you find yourself needing to mock many collaborators in tests. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/FilesController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/FilesController.cs.md index 12b3a1c..890055c 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/FilesController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/FilesController.cs.md @@ -12,12 +12,14 @@ public class FilesController : ControllerBase ``` -Serves an uploaded file anonymously by design: the unguessable GUID in the URL is the access token (Discord-CDN-style capability URL), so attachment links can be opened directly in a browser and shared to IRC clients. E2E-encrypted room blobs are ciphertext at rest, so anonymous access reveals nothing for those channels. +Serves an uploaded file via the GET endpoint `api/files/{fileId}` and intentionally allows anonymous access through the unguessable GUID in the URL, enabling direct browser viewing or sharing of attachments. The controller uses [`FileStorageService`](../Services/FileStorageService.cs.md) (via `_fileStorage`) to resolve the file path and returns the file with an appropriate `Content-Type`; images and audio render inline in the browser, while other types trigger a download with the original filename. ## Remarks -This symbol provides a minimal, token-based file access surface that does not require user authentication. It delegates path resolution to FileStorageService and consolidates content-type handling in one place, so callers can rely on consistent delivery behavior across file types. The design emphasizes shareable, browser-friendly links while safeguarding sensitive payloads behind the GUID-based URL. + +FilesController decouples storage concerns from HTTP delivery, enabling shareable, tokenized links without per-request authentication. It relies on `_fileStorage.GetFilePath` to verify existence and obtain a path, while validating the input with `Guid.TryParse` to guard against malformed requests. The design relies on the GUID in the URL as an access token, so the security model hinges on the token being effectively unguessable to limit exposure of attachments. ## Notes -- The endpoint validates the fileId as a GUID before attempting any storage access; invalid IDs yield a BadRequest response. -- Content types are determined by file extension with a broad fallback to application/octet-stream; unknown extensions will download as a generic binary. -- Images and audio files are rendered inline in the browser, while other types are delivered as attachments with the original file name. + +- Anonymous access means links can be shared; treat the `fileId` as a security token and rotate or revoke links as needed. +- The MIME type is derived from the file extension via `Path.GetExtension`; ensure file extensions are correct to avoid misrepresented MIME types or unintended inline rendering. +- Images and audio render inline (`Content-Type` starts with `image/` or `audio/`); all other files are delivered as attachments with the file name. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/InvitesController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/InvitesController.cs.md index 979d344..74fec54 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/InvitesController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/InvitesController.cs.md @@ -12,12 +12,14 @@ public class InvitesController : ControllerBase ``` -InvitesController provides admin-only endpoints to manage invite codes used for invite-gated registration. It stores and governs the lifecycle of codes within this server's own database, rather than delegating to a central service, and should be used whenever an administrator needs to issue, review, or revoke invites. +InvitesController is the Admin-only API surface that manages invite codes used to gate registrations on this server. It stores codes in the server’s own database (via `EchoHubDbContext.InviteCodes`) and exposes endpoints to create, list, and revoke codes, keeping the lifetime and usage policy centralized on the local instance rather than a central service. Codes are generated by `GenerateCode` as unguessable, human-friendly strings in the form `XXXX-XXXX` drawn from a carefully chosen alphabet, and are surfaced to clients through a lightweight [`InviteDto`](../../EchoHub.Core/DTOs/InviteDtos.cs.md) via `ToDto`. Each operation is guarded by `GetCallerAsync(ServerRole.Admin)` to ensure only administrators can participate, with additional server-side validation of usage and expiration constraints. A hard cap of active invites (`MaxActiveInvites`) prevents unbounded growth, ensuring revocation activities stay in sync with availability. The controller logs creation and revocation events for auditability, reinforcing accountability around invite management. + ## Remarks -Centralizing invite data in this controller creates a self-contained, auditable lifecycle for invites without relying on external services. It enforces admin ownership of creation, supports expiration and usage limits, and records actions for traceability via logs. By separating persistence (InviteCodes) from presentation (DTOs) and API responses, the design keeps concerns well-scoped and maintainable in this deployment. +InvitesController encapsulates invite-code policy in a single server-local component, enabling straightforward auditing and revocation without cross-service coordination. By keeping codes in this server’s own database, it offers immediate effect when invites are revoked and aligns with admin-driven registration flows. The combination of strict input validation, cryptographically strong code generation, and explicit admin authorization provides a clear boundary around who can issue or revoke invites and under what constraints. + ## Notes -- There is a potential race around the MaxActiveInvites check with concurrent create requests; consider transactional safeguards if concurrent admins can issue invites. -- ExpiresInHours is optional; omitting it yields non-expiring invites; the code only applies an expiration when a value is provided. -- Generated codes use a reserved alphabet that excludes ambiguous characters and follow the XXXX-YYYY pattern, aiding readability and reducing mis-typing. +- The active-invite cap enforces a maximum of 200 invites with unused uses; attempting to create a new invite when this cap is reached results in a BadRequest and requires revoking unused invites first. +- Codes are stored in uppercase and normalized on revoke (via `FirstOrDefaultAsync` with a case-insensitive match); clients can supply codes in any case, but the server stores and compares in a normalized form. +- If an `ExpiresInHours` value is not supplied, `ExpiresAt` remains null, meaning the invite never expires. If supplied, it must be between 1 and 8760 hours (1 year). diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ModerationController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ModerationController.cs.md index a139182..f436629 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ModerationController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ModerationController.cs.md @@ -8,46 +8,32 @@ ```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["POST api/moderation/role - AssignRoleRequest"] -GetCaller["Call GetCallerAsync(ServerRole.Admin)"] -CallerError{"GetCaller returned error?"} -ReturnError["Return ErrorResponse and stop"] - -CheckOwnerReq{"request.Role == ServerRole.Owner?"} -BadRequestOwner["Return BadRequest(ErrorResponse: Cannot assign the Owner role.)"] - -FindTarget["Query EchoHubDbContext.Users for request.Username.ToLower()"] -TargetNotFound{"target is null?"} -ReturnNotFound["Return NotFound(ErrorResponse: user not found)"] - -TargetIsOwner{"target.Role == ServerRole.Owner?"} -BadRequestOwner2["Return BadRequest(ErrorResponse: Cannot change the server owner role.)"] - -RoleTooHigh{"request.Role >= caller.Role?"} -BadRequestRole["Return BadRequest(ErrorResponse: Cannot assign a role equal to or above your own.)"] - -ApplyChange["Set previousRole, assign request.Role to target, call EchoHubDbContext.SaveChangesAsync()"] -ReturnOk["Return Ok(message: user is now role)"] - -Start --> GetCaller -GetCaller --> CallerError -CallerError -->|Yes| ReturnError -CallerError -->|No| CheckOwnerReq - -CheckOwnerReq -->|Yes| BadRequestOwner -CheckOwnerReq -->|No| FindTarget - -FindTarget --> TargetNotFound -TargetNotFound -->|Yes| ReturnNotFound -TargetNotFound -->|No| TargetIsOwner - -TargetIsOwner -->|Yes| BadRequestOwner2 -TargetIsOwner -->|No| RoleTooHigh - -RoleTooHigh -->|Yes| BadRequestRole -RoleTooHigh -->|No| ApplyChange - -ApplyChange --> ReturnOk +A["ModerationController POST role receives AssignRoleRequest"] +A --> B["Call GetCallerAsync(ServerRole.Admin)"] +B -->|"error != null"|C["Return ErrorResponse"] +B -->|"caller authorized"|D["If request.Role == ServerRole.Owner -> BadRequest(ErrorResponse)"] +D -->|"true"|C +D -->|"false"|E["Query EchoHubDbContext.Users for target Username (toLower)"] +E -->|"not found"|F["Return NotFound(ErrorResponse)"] +E -->|"found"|G["If target.Role == ServerRole.Owner -> BadRequest(ErrorResponse)"] +G -->|"true"|C +G -->|"false"|H["If request.Role >= caller.Role -> BadRequest(ErrorResponse)"] +H -->|"true"|C +H -->|"false"|I["Set previousRole and assign target.Role = request.Role"] +I --> J["Call EchoHubDbContext.SaveChangesAsync()"] +J --> K["Log information about role change"] +K --> L["Return Ok with success message"] + +M["ModerationController POST kick/{username} receives KickRequest?"] +M --> N["Call GetCallerAsync(ServerRole.Mod)"] +N -->|"error != null"|C +N -->|"caller authorized"|O["Query EchoHubDbContext.Users for target Username"] +O -->|"not found"|F +O -->|"found"|P["If target.Role >= caller.Role -> BadRequest(ErrorResponse)"] +P -->|"true"|C +P -->|"false"|Q["channels = PresenceTracker.GetChannelsForUser(target.Username)"] +Q --> R["For each Channel in channels: broadcast kick via IChatBroadcaster and clean presence"] +R --> S["Proceed to perform broadcast and cleanup (truncated)"] ``` ```csharp @@ -59,13 +45,14 @@ public class ModerationController : ControllerBase ``` -Exposes HTTP endpoints under api/moderation for server moderation operations such as assigning roles, kicking users, and banning users. Reach for this controller when implementing administrative or moderation features (web UI, automated moderation tools, or internal scripts) that must enforce role hierarchy, persist changes to the user store, notify connected clients, and clean up presence/connection state. +Provides HTTP endpoints under `api/moderation` for server moderation operations such as assigning roles, kicking and banning users. Use `ModerationController` when you need a centralized, authenticated API surface to perform privileged user-management actions that update persistent state and notify connected clients. ## Remarks -This controller centralizes server-side moderation logic and enforces policy at the API boundary: callers must be authenticated and possess the appropriate ServerRole before actions are performed. It coordinates several responsibilities through injected services — persisting role changes via the DbContext, enumerating and notifying affected channels via the PresenceTracker and IChatBroadcaster implementations, forcing connection teardown and cleanup, and recording moderation metrics with ServerStatsCollector. The design keeps authorization and business rules (for example, preventing Owner reassignment and preventing actors from assigning or acting on users with equal or higher roles) inside the controller so callers cannot bypass them. +`ModerationController` centralizes moderation workflows: it validates the caller's privileges (via the controller's caller-checking helpers), performs database updates through [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), emits real-time notifications to connected clients through [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implementations, and updates runtime state via [`PresenceTracker`](../Services/PresenceTracker.cs.md) and [`ServerStatsCollector`](../Services/Stats/ServerStatsCollector.cs.md). The class is decorated with `[Authorize]` and `[EnableRateLimiting("general")]`, so all endpoints require an authenticated caller and are subject to the configured rate limits. Actions that modify user connectivity (for example kicking a user) will both broadcast the event to affected channels and invoke the controller's disconnect/cleanup logic to remove presence and force client disconnects. ## Notes -- Usernames are normalized (lowercased) before lookup; callers should supply usernames case-insensitively. -- Role comparisons rely on the numeric ordering of ServerRole (the controller rejects assigning or acting on roles that are equal to or higher than the caller). -- Methods have observable side effects: database updates, broadcasts to connected clients, forced disconnects and presence cleanup, and server-stat increments — consumers should treat these endpoints as state-changing and potentially long-running operations. -- The controller logs moderation actions (role changes, kicks, etc.); ensure logging and monitoring are configured appropriately for audit purposes. \ No newline at end of file +- User lookup uses a lowercased username (e.g. `username.ToLowerInvariant()`), so callers should supply the canonical username form; mismatched casing can lead to `NotFound` responses. +- Role hierarchy is enforced: the controller prevents assigning the `ServerRole.Owner`, prevents changing the server owner's role, and disallows assigning or acting on users with roles equal to or higher than the caller (see the `AssignRole` and `KickUser` checks). +- Persistent changes are saved via `EchoHubDbContext.SaveChangesAsync()` and important actions are logged with the injected `ILogger`, so moderation operations are durable and auditable. +- Because the controller broadcasts moderation events using [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) and may call `ForceDisconnectAndCleanupAsync`, clients connected to channels may be forcibly disconnected as part of an action — callers should expect immediate real-time side effects beyond the HTTP response. +- The `[EnableRateLimiting("general")]` attribute can cause requests to be throttled under high load; plan client-side retry/backoff for operator tooling that calls these endpoints. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ServerController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ServerController.cs.md index 9aac17c..80b277d 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ServerController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/ServerController.cs.md @@ -10,14 +10,13 @@ public class ServerController : ControllerBase ``` -ServerController is an ASP.NET Core API controller that exposes the EchoHub server's administrative surface: endpoints to fetch live server statistics, retrieve the configured encryption key (when authorized), and inspect the directory registration state without exposing the claim token itself. Use it when you need operational visibility or admin actions, rather than wiring multiple components yourself. +ServerController is an ASP.NET Core API controller that exposes server-wide information and administrative operations under the `/api/server` route. It wires together runtime configuration, persistence, and directory-state to provide a concise snapshot of the server and a small admin surface for privileged tasks. The public `GetInfo` endpoint returns a [`ServerStatusDto`](../../EchoHub.Core/DTOs/ServerDtos.cs.md) containing the server name, description, user and channel counts, and the current registration mode derived from config. The `GetEncryptionKey` endpoint is protected by `[Authorize]` and returns an [`EncryptionKeyResponse`](../../EchoHub.Core/DTOs/ServerDtos.cs.md) containing the configured key, or a 503 if encryption is not configured. The `GetDirectoryStatus` endpoint is admin-only and surfaces directory registration state, including the server identifier and whether a claim token exists, while never exposing the token itself. A private helper `GetCallerAsync` centralizes authentication and authorization checks for admin actions. ## Remarks -This symbol acts as a unified HTTP boundary for server-wide concerns, coordinating three collaborators: EchoHubDbContext for live data (Users and Channels), IConfiguration for server configuration (name, description, and registration mode), and DirectoryClaimStore for directory registration state. The private GetCallerAsync helper centralizes authentication and role checks, ensuring privileged endpoints (e.g., GetDirectoryStatus) are accessible only to Admins. By composing a ServerStatusDto from runtime metrics and configuration-derived values, the controller provides a lightweight, admin-focused surface without leaking sensitive tokens. +By centralizing server-wide information and admin operations in a single controller, the architecture cleanly separates concerns: data access ([`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md)), configuration (`IConfiguration`), and directory registration state ([`DirectoryClaimStore`](../Services/DirectoryClaimStore.cs.md)) are coordinated behind stable, contract-driven DTOs ([`ServerStatusDto`](../../EchoHub.Core/DTOs/ServerDtos.cs.md), [`EncryptionKeyResponse`](../../EchoHub.Core/DTOs/ServerDtos.cs.md)). Authorization boundaries are explicit: open information through `GetInfo`, authenticated access for the encryption key, and admin-only access for directory status. The internal `GetCallerAsync` encapsulates common identity/role validation, reducing duplication and potential security gaps across admin endpoints. ## Notes -- GetEncryptionKey returns 503 if Encryption:Key is not configured on the server, signaling that encryption readiness is unavailable. -- GetDirectoryStatus is admin-only; if the caller lacks Admin rights, the endpoint yields an Unauthorized/403 response via GetCallerAsync. -- GetCallerAsync enforces authentication by reading the NameIdentifier claim, loading the user from the database, and validating their ServerRole; failures surface as Unauthorized or 403 with a clear message. +- The admin surface is guarded: `GetDirectoryStatus` relies on `GetCallerAsync` to enforce that the caller has at least `ServerRole.Admin`; non-admins will receive an appropriate 403/Unauthorized response. +- If encryption is not configured on the server, the `GetEncryptionKey` endpoint returns a 503 Service Unavailable, signaling to clients that encryption is not currently available despite the endpoint being accessible. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/UsersController.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/UsersController.cs.md index 27fcac3..ead75ef 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Controllers/UsersController.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Controllers/UsersController.cs.md @@ -8,36 +8,36 @@ ```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 -UsersController["UsersController: receives HTTP request"] -UserService["UserService: GetUserProfileAsync(username)"] -ErrorResponse["ErrorResponse: create error response (message)"] -User["User: profile DTO / updated user"] -UpdateProfileRequest["UpdateProfileRequest: request body"] -IUserService["IUserService: UpdateProfileAsync(userId, displayName, bio, nicknameColor)"] -UserOperationResult["UserOperationResult: IsSuccess + User"] -UserError["UserError: domain error result"] -UploadLimits["UploadLimits: MaxAvatarSizeBytes"] - -UsersController -->|"GET {username}/profile"| UserService -UserService -->|"returns null"| ErrorResponse -ErrorResponse -->|"404 NotFound (User not found)"| UsersController -UserService -->|"returns profile"| User -User -->|"200 OK (profile)"| UsersController - -UsersController -->|"PUT /profile with UpdateProfileRequest"| UpdateProfileRequest -UpdateProfileRequest -->|"no userId claim"| ErrorResponse -UpdateProfileRequest -->|"has userId claim -> call UpdateProfileAsync"| IUserService -IUserService -->|"returns UserOperationResult"| UserOperationResult -UserOperationResult -->|"IsSuccess == false"| UserError -UserError -->|"MapUserError -> ErrorResponse"| ErrorResponse -UserOperationResult -->|"IsSuccess == true"| User -User -->|"200 OK (updated user)"| UsersController - -UsersController -->|"POST /avatar"| UsersController -UsersController -->|"no userId claim"| ErrorResponse -UsersController -->|"no form content or files"| ErrorResponse -UsersController -->|"file obtained from Request.Form.Files[0] -> check length"| UploadLimits -UploadLimits -->|"file length > MaxAvatarSizeBytes -> BadRequest"| ErrorResponse + start["UsersController receives HTTP request"] + route{"Route: which endpoint?"} + + start --> route + + %% GET profile flow + route -->|"GET {username}/profile"| gpCall["Call IUserService.GetUserProfileAsync(username)"] + gpCall --> profileNull{"Profile is null?"} + profileNull -- Yes --> notFound["Return 404 NotFound(ErrorResponse)"] + profileNull -- No --> okProfile["Return 200 Ok(profile)"] + + %% Update profile flow + route -->|"PUT profile"| updAuth["Extract userIdClaim from User"] + updAuth --> updAuthNull{"userIdClaim is null?"} + updAuthNull -- Yes --> updUnauthorized["Return 401 Unauthorized(ErrorResponse)"] + updAuthNull -- No --> updCall["Call IUserService.UpdateProfileAsync(Guid.Parse(userIdClaim), UpdateProfileRequest fields)"] + updCall --> updResult{"result.IsSuccess?"} + updResult -- No --> updError["Return mapped UserError response (UserError)"] + updResult -- Yes --> updOk["Return 200 Ok(result.User)"] + + %% Upload avatar flow (truncated) + route -->|"POST avatar"| uploadAuth["Extract userIdClaim from User"] + uploadAuth --> uploadAuthNull{"userIdClaim is null?"} + uploadAuthNull -- Yes --> uploadUnauthorized["Return 401 Unauthorized(ErrorResponse)"] + uploadAuthNull -- No --> formCheck{"Request.HasFormContentType && Request.Form.Files.Count > 0?"} + formCheck -- No --> noFile["Return 400 BadRequest(ErrorResponse: No file uploaded.)"] + formCheck -- Yes --> fileAssign["Select first file from Request.Form.Files (file)"] + fileAssign --> sizeCheck{"file.Length > UploadLimits.MaxAvatarSizeBytes?"} + sizeCheck -- Yes --> tooLarge["Return 400 BadRequest(ErrorResponse: File size exceeds maximum)"] + sizeCheck -- No --> continue["Proceed with avatar processing (truncated)"] ``` ```csharp @@ -49,12 +49,15 @@ public class UsersController : ControllerBase ``` -Handles HTTP endpoints rooted at /api/users for authenticated user operations such as retrieving a user's public profile, updating the caller's profile, and uploading an avatar. The controller delegates business logic to IUserService and ImageToAsciiService, applies rate limiting and upload-size checks, and returns standard DTOs like ErrorResponse and AvatarUploadResponse. +Controller that exposes the HTTP surface for user profile and account-related operations under `api/users`, including profile retrieval, profile updates and avatar upload. Reach for `UsersController` when you need to translate authenticated HTTP requests into calls to the user, storage and broadcasting services (for example, calling [`IUserService`](../../EchoHub.Core/Contracts/IUserService.cs.md) to update a profile or [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md) to convert an uploaded avatar). ## Remarks -This controller is the HTTP adapter for user-focused features: it validates requests and authorization, enforces upload and rate limits, converts uploaded images to ASCII art via ImageToAsciiService, and forwards profile and avatar changes to IUserService. It centralizes request-level concerns (model binding, auth, error translation) so the underlying services can remain framework-agnostic. +`UsersController` is an orchestration layer: it validates and normalizes incoming HTTP requests, enforces authentication and rate-limiting policies, performs lightweight validation (for example file size and image format checks), and delegates the domain work to collaborators such as [`IUserService`](../../EchoHub.Core/Contracts/IUserService.cs.md), [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md), [`FileStorageService`](../Services/FileStorageService.cs.md), [`PresenceTracker`](../Services/PresenceTracker.cs.md) and the collection of [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implementations. The controller centralizes common web concerns (claim extraction via `User.FindFirstValue`, mapping service results to HTTP responses with `MapUserError`, and producing [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md)/[`AvatarUploadResponse`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) payloads) so the underlying services can remain focused on business logic. The `DeletedUserName` constant is a reserved tombstone username: [`UserService`](../Services/UserService.cs.md) will refuse to register it, and it is used when re-attributing messages after account deletion (messages re-attributed to `DeletedUserName`). Note also that exported account data includes stored messages but that any end-to-end encrypted room content remains ciphertext on the server (the controller preserves what the server stores, it does not decrypt client-side E2E content). ## Notes -- UploadAvatar requires a multipart/form POST (Request.HasFormContentType) and will reject requests with no files or files exceeding the configured UploadLimits.MaxAvatarSizeBytes. -- The controller reads the caller's user id from ClaimTypes.NameIdentifier and uses Guid.Parse; if the claim is present but malformed the parse will throw. The implementation assumes authenticated tokens supply a well-formed GUID. -- Take care when extending or changing image validation: FileValidationHelper.IsValidImage is called on the uploaded stream before ImageToAsciiService.ConvertToAscii is invoked. If validation reads the stream to its end without rewinding, the conversion will receive an empty stream — ensure the validation either rewinds the stream or operates on a buffered/copy of the data. \ No newline at end of file +- The controller is annotated with `Authorize`, so every action requires authentication by default. An action must be decorated with `AllowAnonymous` to be reachable without credentials. +- `UploadAvatar` expects a multipart/form-data request and will return `BadRequest` when `Request.Form.Files` is empty. It also enforces size limits using `_uploadLimits.MaxAvatarSizeBytes` and reports the limit in MB in the error text. +- `FileValidationHelper.IsValidImage` is used to allow only images (it recognizes JPEG, PNG, GIF and WebP). Because both validation and ASCII conversion operate on the same `Stream` (`file.OpenReadStream()`), ensure the validation method does not consume the stream or that the stream position is reset before calling `ImageToAsciiService.ConvertToAscii` — otherwise the conversion may see an empty stream. +- The controller extracts the caller identity using `User.FindFirstValue(ClaimTypes.NameIdentifier)` and then calls `Guid.Parse(...)`. If the claim is present but not a valid GUID this will throw; callers should ensure the claim is a GUID or the parsing should be hardened (for example with `Guid.TryParse`). +- Upload endpoints have a more specific rate limit: the controller-level `[EnableRateLimiting("general")]` applies broadly while `UploadAvatar` additionally uses `[EnableRateLimiting("upload")]`, so be aware of which policy will throttle a client. +- Many methods rely on `MapUserError` to convert domain errors into HTTP responses; consumers of these endpoints should expect standardized [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) payloads for error cases. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md index 7507766..1f43485 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md @@ -8,4 +8,74 @@ public class EchoHubDbContext : DbContext ``` -EchoHubDbContext serves as the EF Core persistence gateway for EchoHub's domain model. It exposes `DbSet`, `DbSet`, `DbSet`, `DbSet`, `DbSet`, `DbSet`, `DbSet`, and `DbSet`, enabling queries and updates against the underlying SQLite store. When not configured by the application host, it configures a file-based SQLite database named echohub.db located under the application base directory, providing a simple local data store for development and testing. In OnModelCreating it enforces domain rules through keys, indices, field length constraints, and relationship mappings (for example, a Channel has many Messages; a Message has many Attachments; ChannelMembership uses a composite key of UserId and ChannelId), ensuring data integrity and cascade behaviors across related entities. \ No newline at end of file +EchoHubDbContext is the EF Core `DbContext` that exposes the EchoHub data model to the database. It provides `DbSet` properties for core aggregates such as [`User`](../../EchoHub.Core/Models/User.cs.md), [`Channel`](../../EchoHub.Core/Models/Channel.cs.md), [`Message`](../../EchoHub.Core/Models/Message.cs.md), [`Attachment`](../../EchoHub.Core/Models/Attachment.cs.md), `RefreshToken`, [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md), [`InviteCode`](../../EchoHub.Core/Models/InviteCode.cs.md), and [`ServerStatsReport`](../../EchoHub.Core/Models/ServerStatsReport.cs.md), enabling typed queries and persistence throughout the application. When the context is not configured by the host, `OnConfiguring` automatically wires up a SQLite database file named `echohub.db` in the application's base directory via `AppContext.BaseDirectory` and the `UseSqlite` provider. + +## Remarks +`EchoHubDbContext` centralizes data access for the domain, acting as the bridge between in-memory entities and their persisted representations. The `OnModelCreating` configuration defines keys, unique constraints, indices, and relationships that enforce data integrity and shape the underlying schema: [`User`](../../EchoHub.Core/Models/User.cs.md) enforces a unique `Username` with length limits; [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) and [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md) establish channel scopes and membership rules; [`Message`](../../EchoHub.Core/Models/Message.cs.md) and [`Attachment`](../../EchoHub.Core/Models/Attachment.cs.md) model the document and media relationships with cascade deletes to maintain referential integrity; and [`ServerStatsReport`](../../EchoHub.Core/Models/ServerStatsReport.cs.md) records runtime metrics. This design keeps persistence concerns isolated from business logic while ensuring consistent, queryable access to all EchoHub data. + +## Dependencies +- `DbContext` +- [`User`](../../EchoHub.Core/Models/User.cs.md) +- [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) +- [`Message`](../../EchoHub.Core/Models/Message.cs.md) +- [`Attachment`](../../EchoHub.Core/Models/Attachment.cs.md) +- `RefreshToken` +- [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md) +- [`InviteCode`](../../EchoHub.Core/Models/InviteCode.cs.md) +- [`ServerStatsReport`](../../EchoHub.Core/Models/ServerStatsReport.cs.md) + +## Dependency APIs +- property [`User`](../../EchoHub.Core/Models/User.cs.md) (`src/EchoHub.Core/Models/RefreshToken.cs`) +- class [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) (`src/EchoHub.Core/Models/Channel.cs`) + - property `Guid Id` + - property `string Name` + - property `string? Topic` + - property `bool IsPublic` + - property `bool IsSystem` + - property `string? PasswordHash` + - property `string? EncryptionSalt` + - property `string? WrappedRoomKey` + - property `DateTimeOffset CreatedAt` + - property `Guid CreatedByUserId` + - property `List Messages` +- property [`Message`](../../EchoHub.Core/Models/Message.cs.md) (`src/EchoHub.Core/Models/Attachment.cs`) +- class [`Attachment`](../../EchoHub.Core/Models/Attachment.cs.md) (`src/EchoHub.Core/Models/Attachment.cs`) + - property `Guid Id` + - property `Guid MessageId` + - property `Message? Message` + - property `AttachmentKind Kind` + - property `string Url` + - property `string FileName` + - property `long FileSize` + - property `string? AsciiPreview` +- property `RefreshToken` (`src/EchoHub.Client/Config/ClientConfig.cs`) +- class [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md) (`src/EchoHub.Core/Models/ChannelMembership.cs`) + - property `Guid UserId` + - property `Guid ChannelId` + - property `DateTimeOffset JoinedAt` +- class [`InviteCode`](../../EchoHub.Core/Models/InviteCode.cs.md) (`src/EchoHub.Core/Models/InviteCode.cs`) + - property `Guid Id` + - property `string Code` + - property `Guid CreatedByUserId` + - property `string CreatedByUsername` + - property `DateTimeOffset CreatedAt` + - property `DateTimeOffset? ExpiresAt` + - property `int MaxUses` + - property `int UseCount` +- class [`ServerStatsReport`](../../EchoHub.Core/Models/ServerStatsReport.cs.md) (`src/EchoHub.Core/Models/ServerStatsReport.cs`) + - property `Guid Id` + - property `DateTimeOffset GeneratedAt` + - property `DateTimeOffset PeriodStart` + - property `DateTimeOffset PeriodEnd` + - property `double WindowHours` + - property `int MessagesSent` + - property `int FilesUploaded` + - property `long BytesUploaded` + - property `int NewMembers` + - property `int ActiveMembers` + - property `int Connections` + - property `int Disconnections` + - …and 5 more member(s) not shown + +## Notes +- The `EchoHubDbContext` relies on SQLite as the backing store when not configured externally; ensure the application process has write access to the base directory where `echohub.db` is created. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Hubs/ChatHub.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Hubs/ChatHub.cs.md index e4ea4cb..4afecea 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Hubs/ChatHub.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Hubs/ChatHub.cs.md @@ -8,38 +8,34 @@ ```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 -User["User connects or calls JoinChannel"] -CH_OnConnected["ChatHub: OnConnectedAsync()"] -IChatServiceConn["IChatService: UserConnectedAsync(Context.ConnectionId, CurrentUserId, CurrentUsername)"] -BaseOnConnected["Call base.OnConnectedAsync()"] -OnConnectedCatch["On exception: Log error and rethrow"] - -CH_Join["ChatHub: JoinChannel(channelName, password)"] -IChatServiceJoin["IChatService: JoinChannelAsync(Context.ConnectionId, CurrentUserId, CurrentUsername, channelName, password)"] -CheckError{"error is not null?"} -ReturnFail["Return JoinChannelResult(false, [], error, passwordRequired)"] -AddGroup["Add connection to SignalR group 'channelName.ToLowerInvariant().Trim()'"] -IChannelServiceNode["IChannelService: GetChannelKeyEnvelopeAsync(channelName)"] -ReturnSuccess["Return JoinChannelResult(true, history, EncryptionSalt, WrappedRoomKey)"] -JoinCatch["On exception: Log error"] -ReturnJoinCatch["Return JoinChannelResult(false, [], 'Failed to join channel: ex.Message')"] - -User --> CH_OnConnected -CH_OnConnected --> IChatServiceConn -IChatServiceConn --> BaseOnConnected -CH_OnConnected -->|"exception"| OnConnectedCatch - -User --> CH_Join -CH_Join --> IChatServiceJoin -IChatServiceJoin --> CheckError -CheckError -->|"yes"| ReturnFail -CheckError -->|"no"| AddGroup -AddGroup --> IChannelServiceNode -IChannelServiceNode --> ReturnSuccess - -CH_Join -->|"exception"| JoinCatch -IChatServiceJoin -->|"exception"| JoinCatch -JoinCatch --> ReturnJoinCatch +start["Incoming Hub action to ChatHub"] + +start --> onConn["OnConnectedAsync"] +onConn --> callUserConnected["Call IChatService.UserConnectedAsync(Context.ConnectionId, CurrentUserId, CurrentUsername)"] +callUserConnected --> baseOnConn["Call base.OnConnectedAsync()"] +callUserConnected -->|"exception"| onConnLog["Log error via ILogger and rethrow"] +baseOnConn --> onConnEnd["OnConnectedAsync returns"] + +start --> onDisc["OnDisconnectedAsync"] +onDisc --> callUserDisconnected["Call IChatService.UserDisconnectedAsync(Context.ConnectionId)"] +callUserDisconnected --> baseOnDisc["Call base.OnDisconnectedAsync(exception)"] +callUserDisconnected -->|"exception"| onDiscLog["Log error via ILogger and rethrow"] +baseOnDisc --> onDiscEnd["OnDisconnectedAsync returns"] + +start --> join["JoinChannel(channelName, password?)"] +join --> callJoinService["Call IChatService.JoinChannelAsync(Context.ConnectionId, CurrentUserId, CurrentUsername, channelName, password)"] +callJoinService --> joinDecision{"error is not null?"} +joinDecision -->|"yes"| joinReturnError["Return JoinChannelResult(false, [], error, passwordRequired)"] +joinDecision -->|"no"| addGroup["Call Groups.AddToGroupAsync(Context.ConnectionId, channelName.ToLowerInvariant().Trim())"] +addGroup --> getEnvelope["Call IChannelService.GetChannelKeyEnvelopeAsync(channelName)"] +getEnvelope --> joinReturnSuccess["Return JoinChannelResult(true, history, EncryptionSalt, WrappedRoomKey)"] +callJoinService -->|"exception"| joinExceptionLog["Log error via ILogger; Return JoinChannelResult(false, [], Failed to join channel: ex.Message)"] + +start --> leave["LeaveChannel(channelName)"] +leave --> normalize["Normalize channelName to lowerInvariant and trim"] +normalize --> callLeaveService["Call IChatService.LeaveChannelAsync(Context.ConnectionId, CurrentUsername, channelName)"] +callLeaveService --> removeFromGroup["Call Groups.RemoveFromGroupAsync(Context.ConnectionId, channelName)"] +callLeaveService -->|"exception"| leaveExceptionLog["Log error via ILogger"] ``` ```csharp @@ -48,13 +44,12 @@ public class ChatHub : Hub ``` -A SignalR Hub that exposes real-time chat operations to authenticated clients. ChatHub mediates between connected clients and the server-side chat logic (IChatService) and channel management (IChannelService), handling user connection lifecycle, channel join/leave actions, message sending, and delivery of channel encryption envelopes when applicable. +A SignalR hub that exposes real-time chat operations (connect/disconnect, join/leave channel, send messages) and bridges authenticated SignalR connections with the domain services that manage presence, channels and message delivery. Reach for `ChatHub` when you need a server-side, authenticated entry point that coordinates [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) and [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md), manages SignalR groups, and forwards notifications to clients via the [`IEchoHubClient`](../../EchoHub.Core/Contracts/IEchoHubClient.cs.md) callbacks. ## Remarks -ChatHub is an authorization-guarded entrypoint for real-time chat behavior: it uses the caller's claims to identify the user, registers and deregisters connection state with IChatService on connect/disconnect, and forwards channel and message operations to the underlying domain services. It centralizes error logging and converts service-level outcomes into client-facing responses (for example, returning a JoinChannelResult or invoking Error on the caller). The hub also normalizes group names (lowercasing and trimming) and returns channel key envelopes from IChannelService for encrypted rooms so clients can unwrap room keys locally. +`ChatHub` is a thin application-layer adapter: it enforces authentication (the class is decorated with `Authorize`), resolves the current user from the SignalR `Context` claims via `CurrentUserId` and `CurrentUsername`, and delegates core logic to [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) and [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md). It is responsible for SignalR group membership using `Groups.AddToGroupAsync` / `Groups.RemoveFromGroupAsync`, for logging errors via `ILogger`, and for returning protocol-shaped results such as [`JoinChannelResult`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) and client-facing error messages through the `IEchoHubClient.Error` callback. The hub intentionally does not perform domain operations itself — it translates connection-level events and client requests into calls to the underlying services and shapes the responses for connected clients. ## Notes -- The hub requires authentication claims: CurrentUserId and CurrentUsername read from the connection's ClaimsPrincipal and will throw a HubException if the expected claims are missing. Ensure clients authenticate and include these claims. -- Channel names are normalized via ToLowerInvariant().Trim() before being added to or removed from SignalR groups; callers should expect case-insensitive channel membership. -- When joining encrypted channels the hub obtains an encryption salt and a wrapped room key from IChannelService and returns them to the client so the client can decrypt room keys locally — the server does not hold the raw room key. -- Connection lifecycle failures in OnConnectedAsync/OnDisconnectedAsync are logged and rethrown, while most per-operation failures return structured results or invoke Clients.Caller.Error so callers receive a clear error message without server-side leaks. \ No newline at end of file +- `CurrentUserId` and `CurrentUsername` throw a `HubException` if the expected claims are absent; the `Authorize` attribute reduces this risk, but any token must contain `ClaimTypes.NameIdentifier` and a `"username"` claim for the hub to function. +- Channel names are normalized with `channelName.ToLowerInvariant().Trim()` before being used with SignalR groups; callers and services must use the same normalization to avoid mismatches in group membership. +- When `JoinChannel` succeeds for an encrypted channel the hub returns the channel key envelope obtained from `IChannelService.GetChannelKeyEnvelopeAsync` (the server comment notes members unwrap the room key client-side); do not expect the server to decrypt room content for clients. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Program.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Program.cs.md index 578e5a7..96ed00b 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Program.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Program.cs.md @@ -4,12 +4,4 @@ > **Kind:** file -Bootstraps and hosts the EchoHub.Server application as a resilient, self-hosting startup routine. It is the entrypoint that wires configuration, logging, data access, authentication, and service registrations, then starts the ASP.NET Core web host inside a self-healing loop that restarts on startup failures. - -## Remarks -Program serves as the composition root of EchoHub.Server. It orchestrates essential cross-cutting concerns—initial configuration via FirstRunSetup, bootstrap logging, server-logs integration with Serilog, data access via EF Core SQLite, and authentication via JWT—by registering the relevant services and options early in the host lifecycle. The self-restarting loop provides resilience during startup and deploy-time hiccups, ensuring the server recovers automatically while debugging and monitoring can observe repeated failures. It ties together the server's operational concerns and acts as the single entry point that other components rely upon to start the web application. - -## Notes -- The startup loop restarts the host on failures, enabling resilience but potentially causing rapid churn if issues persist; external monitoring is recommended to detect persistent problems. -- Jwt:Secret must be configured; missing configuration leads to startup failure (an InvalidOperationException is thrown during startup). -- The app uses SQLite by default (echohub.db) located in AppContext.BaseDirectory; ensure filesystem permissions and migrations are properly managed in deployment. \ No newline at end of file +Program.cs is the entry point for the EchoHub.Server application. It bootstraps the server by performing the one-time setup via `FirstRunSetup.EnsureAppSettings()`, configuring the bootstrap logger, and then starting the ASP.NET Core host via `WebApplication.CreateBuilder(args)` inside an auto-restart loop. It wires core infrastructure such as [`EchoHubDbContext`](Data/EchoHubDbContext.cs.md) for EF Core and authentication, and server features like [`ServerLogsOptions`](Config/ServerLogsOptions.cs.md)/[`ServerLogsService`](Services/ServerLogs/ServerLogsService.cs.md), enabling the app to recover from startup failures by rebuilding and running the web host repeatedly. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ChannelService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ChannelService.cs.md index cfd6f1e..ff776c9 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ChannelService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ChannelService.cs.md @@ -13,15 +13,15 @@ - [EnsureSystemChannelAsync](#ensuresystemchannelasync) - [GetChannelByNameAsync](#getchannelbynameasync) - [GetChannelCryptoAsync](#getchannelcryptoasync) + - [GetChannelKeyEnvelopeAsync](#getchannelkeyenvelopeasync) - [GetChannelListAsync](#getchannellistasync) - [GetChannelMetaAsync](#getchannelmetaasync) + - [GetChannelTopicAsync](#getchanneltopicasync) - [GetChannelsAsync](#getchannelsasync) - [RekeyChannelAsync](#rekeychannelasync) - [SetChannelPasswordAsync](#setchannelpasswordasync) - [UpdateTopicAsync](#updatetopicasync) - [ValidateChannelPassword](#validatechannelpassword) -- [GetChannelKeyEnvelopeAsync](#getchannelkeyenvelopeasync) -- [GetChannelTopicAsync](#getchanneltopicasync) --- @@ -34,16 +34,15 @@ public class ChannelService : IChannelService ``` -Manages server-side channel (room) operations: creation, deletion, listing and metadata, membership enforcement, password gating, and end-to-end encryption key envelopes. Reach for ChannelService when you need authoritative server logic that enforces channel rules and persists channel state (including password and E2E envelope handling), rather than making client-side assumptions or manipulating storage directly. +A high-level service that implements [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) and centralizes channel lifecycle and membership operations for the server: listing and paging channels (`GetChannelsAsync`), creating/updating/deleting channels (`CreateChannelAsync`, `UpdateTopicAsync`, `DeleteChannelAsync`), password and encryption envelope management (`SetChannelPasswordAsync`, `RekeyChannelAsync`, `GetChannelKeyEnvelopeAsync`), and retrieving channel metadata/crypto details (`GetChannelMetaAsync`, `GetChannelCryptoAsync`). Use `ChannelService` when you need the server-side orchestration for channel policies, membership checks and the authoritative source of channel metadata and cryptographic envelopes rather than calling lower-level storage or presence primitives directly. ## Remarks -ChannelService is the central server implementation of IChannelService and enforces policy around channels: who may see or join rooms, how passwords and encryption envelopes are handled, and how the system "log" room is treated differently from ordinary channels. It coordinates presence tracking, spam-throttling (via SpamGuard), and server logging to ensure operations such as channel creation, rekeying, and membership checks are performed consistently and safely. The service preserves the distinction between password-gated channels and end-to-end (E2E) encrypted channels by exposing separate operations for setting/clearing passwords and for rekeying the wrapped room key. +`ChannelService` acts as the application-level coordinator for channel-related concerns. It composes smaller services such as [`PresenceTracker`](PresenceTracker.cs.md), [`SpamGuard`](SpamGuard.cs.md), and [`ServerLogsService`](ServerLogs/ServerLogsService.cs.md), and enforces business rules (creator/admin permissions, role-gated system channels, creation throttling) so callers do not need to reimplement policy logic. The class is responsible for keeping cryptographic envelope state (`EncryptionSalt` / `WrappedRoomKey`) separate from message content keys and for exposing those envelopes through `GetChannelKeyEnvelopeAsync` and `GetChannelCryptoAsync` while preserving server-side metadata like sender identity counts and storage footprint. ## Notes -- SetChannelPasswordAsync is not applicable to end-to-end encrypted channels; encrypted rooms change access by rekeying via RekeyChannelAsync so the room key envelope remains consistent. Clearing a password is performed by passing null as the password parameter. -- RekeyChannelAsync is restricted to the channel creator: administrators who do not know the current passphrase cannot rekey a channel on the creator's behalf. -- The system "live log" room is role-gated and its name is reserved even when the feature is disabled; this prevents user-owned channels from accidentally becoming the stream target if the feature is enabled later. -- Channel creation is subject to spam-throttling; moderators and higher roles are exempt from the throttle enforced by SpamGuard. +- The system "live log" channel has a reserved name and is role-gated: it is visible only to configured roles regardless of membership; the name remains reserved even if the feature is disabled. Be careful when creating channels with that name. +- End-to-end encrypted channels use a different flow: `SetChannelPasswordAsync` is not available for E2E channels; to change a passphrase the service uses `RekeyChannelAsync`, which swaps the join-gate hash and the wrapped room key but does not rotate the room content key (so history remains readable to clients that can re-wrap the key). +- Channel creation is subject to throttling via [`SpamGuard`](SpamGuard.cs.md) (moderators and above are exempt) and creators are automatically added as members; callers should handle [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) responses (success/failure and error messages) rather than assuming the operation always succeeds. --- @@ -71,15 +70,10 @@ public ChannelService( | `logger` | `ILogger` | — | -Initializes ChannelService by wiring its required collaborators into private fields for later use. The constructor accepts a scope factory, a presence tracker, a spam guard, a server logs service, and a logger, and stores them for use by the instance. In typical applications, the dependency injection container supplies these services, so ChannelService can create short-lived scopes when needed, track user presence, guard against spam, record server-side events, and emit contextual logs. +Constructs a `ChannelService` by taking its required collaborators from the dependency injection container and caching them in private fields for later use. This constructor is invoked by the DI framework when creating a `ChannelService` instance, so consumers typically rely on DI rather than invoking it directly. ## Remarks -By taking dependencies through constructor injection, ChannelService remains loosely coupled and highly testable, since test doubles can be supplied in place of real implementations. This composition root clarifies the service's responsibilities—managing channel state with awareness of presence, applying spam protection, and observability through logs. - -## Notes -- If ChannelService is registered as a singleton, ensure that the injected services are thread-safe or have appropriate lifetimes; otherwise adjust registrations to avoid unsafe sharing. -- If the class creates scopes via the IServiceScopeFactory, dispose them promptly to avoid memory leaks or disposed-service access. -- Verify the DI container can resolve all dependencies at startup; a misconfiguration will surface as a runtime resolution failure. +This constructor wires together a set of collaborators required by `ChannelService`: `IServiceScopeFactory` for creating scoped services, [`PresenceTracker`](PresenceTracker.cs.md) for tracking user presence, [`SpamGuard`](SpamGuard.cs.md) for abuse protection, [`ServerLogsService`](ServerLogs/ServerLogsService.cs.md) for server-side logging, and `ILogger` for structured logging. By storing these dependencies in private fields, the class remains focused on channel-related behavior while delegating infrastructure concerns to dedicated services. This separation also improves testability by allowing mocks or fakes to replace the collaborators during unit tests. --- @@ -90,7 +84,7 @@ By taking dependencies through constructor injection, ChannelService remains loo ```csharp public async Task CreateChannelAsync( Guid creatorUserId, string name, string? topic, bool isPublic, - string? [REDACTED:CONNECTION_STRING_PASSWORD] string? encryptionSalt = null, string? wrappedRoomKey = null) + string? password = null, string? encryptionSalt = null, string? wrappedRoomKey = null) ``` **Parameters:** @@ -101,21 +95,22 @@ public async Task CreateChannelAsync( | `name` | `string` | — | | `topic` | `string?` | — | | `isPublic` | `bool` | — | -| `encryptionSalt` | `string? [REDACTED:CONNECTION_STRING_PASSWORD] string?` | `null` | +| `password` | `string?` | `null` | +| `encryptionSalt` | `string?` | `null` | | `wrappedRoomKey` | `string?` | `null` | **Returns:** `Task` -Creates a new chat channel using the provided parameters, validating the name, enforcing reserved names, optionally handling a password (hashed) and an end-to-end encryption envelope, and persisting the channel with the creator as a member. Use this when you need to create a channel with consistent validation, security, and membership semantics. +Creates a new channel with the given `creatorUserId`, `name`, optional `topic`, visibility via `isPublic`, and optional security settings (`password`, `encryptionSalt`, `wrappedRoomKey`). It validates the input (name presence, name pattern via `ValidationConstants.ChannelNameRegex()`, and reserved names against `_serverLogs.Options.NormalizedRoomName`), ensures the channel name is unique, optionally hashes a password with BCrypt, and stores envelope data only when both `encryptionSalt` and `wrappedRoomKey` are supplied. If a password or envelope is provided, the corresponding fields are populated accordingly; otherwise they remain null. The creator automatically becomes a member, and the operation is throttled by a spam guard for non-exempt users. The method persists changes and returns a successful [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) containing a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), or a failure with a [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) and message in cases of validation failure, duplication, or other policy violations. -## Remarks -This method centralizes all channel-creation logic, applying business rules such as name normalization (lowercasing and trimming), reserved-name protection for the log room, password requirements for encrypted channels, and spam throttling before persisting data. It leverages a scoped database context to create the channel and automatically adds the creator as a member, ensuring the creator has immediate access. The reserved log room name is enforced regardless of feature toggles, preventing accidental conflicts with system channels. +**Remarks** +This method centralizes channel creation concerns, including input validation, security policy, and persistence, so callers don’t need to implement these cross-cutting concerns separately. It coordinates between domain entities ([`Channel`](../../EchoHub.Core/Models/Channel.cs.md), [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md)) and their DTOs, while enforcing organizational policies (e.g., reserved names, password requirements for encrypted channels, and anti-spam). The return shape guarantees a consistent success path with a populated [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) or a clear failure path via `ChannelOperationResult.Fail`. -## Notes -- The channel name is normalized to lowercase and trimmed, which makes channel uniqueness effectively case-insensitive. -- If an end-to-end envelope is supplied (encryptionSalt and wrappedRoomKey), a password must also be provided; otherwise creation fails with a validation error. -- A race on channel name creation is possible in highly concurrent scenarios; the code checks for existence prior to insert and relies on the database to enforce final uniqueness if necessary. +**Notes** +- Normalization and validation: the stored channel name is the lowercased, trimmed form and must pass `ValidationConstants.ChannelNameRegex()`; attempting to create a channel with a name that already exists yields `ChannelError.AlreadyExists`. +- Security coupling: if an envelope is provided, a non-empty `password` is required, and the password (if any) is hashed with BCrypt; envelope data is only stored when both `encryptionSalt` and `wrappedRoomKey` are present. +- Anti-spam policy: channel creation is guarded by `_spamGuard` (non-exempt users may be blocked for rapid creation), reinforcing rate-limiting behavior at the data access boundary. --- @@ -138,14 +133,7 @@ public async Task DeleteChannelAsync(Guid callerUserId, **Returns:** `Task` -Deletes a channel by name for a given caller, enforcing that only the channel creator or an administrator can perform the deletion and that protected/default channels cannot be removed. It normalizes the channel name, validates existence and non-system status, removes the channel from the database, saves changes, and returns a ChannelOperationResult containing a ChannelDto with the channel’s identity and metadata; on failure it maps to a corresponding ChannelError with a descriptive message. - -## Remarks -This method encapsulates the channel-deletion policy in a single place, ensuring consistent authorization checks and error signaling across call sites. It delegates data access to EchoHubDbContext via a scoped DI container and returns a ChannelDto representing the deleted channel’s identity and basic attributes, which can be used by clients to refresh UI state or logs. - -## Notes -- The ChannelDto is constructed after the channel row is removed and SaveChangesAsync completes, so the returned DTO serves as a confirmation of what was deleted rather than a live snapshot of a remaining entity. - +Deletes a channel by name, enforcing that only the channel's creator or an administrator can perform the deletion while protecting the default and system channels. The input channel name is normalized to lower-case and trimmed, a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) is used to locate the channel, and the operation returns a [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) with a specific [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) if the channel does not exist or cannot be deleted. If authorized, the channel is removed from the `db.Channels`, changes are persisted, and a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) describing the deleted channel is returned inside a successful [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md). --- @@ -155,7 +143,7 @@ This method encapsulates the channel-deletion policy in a single place, ensuring ```csharp public async Task<(bool Success, string? Error, bool PasswordRequired)> EnsureChannelMembershipAsync( - Guid userId, string channelName, string? [REDACTED:CONNECTION_STRING_PASSWORD] + Guid userId, string channelName, string? password = null) ``` **Parameters:** @@ -167,15 +155,20 @@ public async Task<(bool Success, string? Error, bool PasswordRequired)> EnsureCh | `PasswordRequired` | `bool` | — | -Ensures that a user is granted membership to a named channel, creating or restoring the channel when appropriate, and enforcing access rules including password protection. Call this when a user attempts to join or access a channel so the system can validate eligibility, auto-provision special channels, and persist the membership relationship in the database. The method returns a tuple (Success, Error, PasswordRequired) to indicate whether entry was granted, an error message if any, and whether the caller should prompt for a password. +Ensures that a user identified by `Guid userId` becomes a member of the channel named `channelName`, creating or restoring the channel as needed, enforcing gating rules, and returning a structured result that indicates success, a possible error message, and whether a password is required for first-time joins. -## Remarks -Centralizes channel-join semantics within ChannelService, encapsulating rules around default channels, system/log channels, and password gates. It coordinates with the database context, server configuration, and validation utilities to decide whether entry should be granted, a channel recreated, or a password prompt issued. By funneling join logic through a single path, it reduces duplication and ensures consistent behavior across different join entry points (TUI, REST, IRC). +The method normalizes the channel name using `ToLowerInvariant()` and `Trim()`, then validates it with `ValidationConstants.ChannelNameRegex()`. If the name is invalid, it returns a failed result along with an error message describing the required channel name constraints. It then opens a scope and obtains an [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to inspect and modify data related to users, channels, and memberships. + +If the target channel is a live-logs channel (as determined by `_serverLogs.IsLogsChannel`), the caller’s ability to view that channel is verified via `_serverLogs.CanView` against the user’s role; otherwise membership is denied. + +If the channel does not exist, the method may auto-recreate it: the default channel (as defined by `HubConstants.DefaultChannel`) is recreated with safe defaults, or a logs channel is recreated with a non-public, system-owned flag and a predefined room topic. If neither special case applies, the method reports that the channel does not exist and should be created first via the channel list. + +If the channel exists but is a system channel and the request is not for a logs channel, access is blocked and the join is rejected. + +When the caller is not already a member, the method enforces password protection if the channel has a `PasswordHash`. If no password is supplied, it returns success = false with `PasswordRequired` set to true. If a password is supplied but is incorrect (verified via `BCrypt.Verify`), it returns the same shape with `PasswordRequired` = true. On successful password verification (or if no password is needed), a new [`ChannelMembership`](../../EchoHub.Core/Models/ChannelMembership.cs.md) entry is created and persisted. + +The function returns a 3-tuple: `(bool Success, string? Error, bool PasswordRequired)`. A successful join yields `(true, null, false)`; otherwise, `Error` describes the failure and `PasswordRequired` signals whether a password is needed for the join. -## Notes -- Automatic channel provisioning: If the requested channel does not exist, the method may recreate the default channel or the log channel and logs a warning. Callers should not assume a static channel list. -- Password gate: For channels with a PasswordHash, a password is required on first-time joins and validated with BCrypt. The method returns PasswordRequired = true in those cases and updates membership only after successful verification. -- Database scope and side effects: The operation creates a short-lived DI scope to access EchoHubDbContext and persists changes (new ChannelMembership, and possibly a newly created Channel). Callers should be mindful of potential race conditions if multiple concurrent joins occur for the same channel. --- @@ -196,14 +189,13 @@ private static async Task EnsureDefaultChannelAsync(EchoHubDbContext db) **Returns:** `Task` -Ensures the application has a canonical default channel in the EchoHub database by checking the Channels collection for a channel named HubConstants.DefaultChannel and seeding one if it does not exist. This bootstrapping helper is intended to be invoked during initialization to guarantee a general discussion channel is present without duplicating the initialization logic elsewhere. +Ensures that the default channel exists in the database by checking for a channel named `HubConstants.DefaultChannel`. If none exists, it creates a new [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) with a generated `Id` (`Guid.NewGuid()`), the default name, a `Topic` of `General discussion`, and a system `CreatedByUserId` of `Guid.Empty`, then saves changes with `SaveChangesAsync`. ## Remarks -By centralizing the default-channel bootstrapping in EnsureDefaultChannelAsync, callers avoid duplicating the existence check and channel-creation code across startup paths. It ties together the Channel entity, the HubConstants default channel name, and the database context, so changes to the default channel semantics propagate from this single place. The method is private and static, reinforcing that it is an internal bootstrap concern rather than a reusable operation for callers. +Centralizes the provisioning of the default channel, letting startup and runtime logic rely on a known channel name without duplicating initialization checks. By using `HubConstants.DefaultChannel` and `Guid.Empty` as the creator, it signals that the record is system-generated and intended as a baseline rather than user-created. ## Notes -- Potential race condition under concurrent invocations: the existence check followed by insertion is not atomic, which could raise a constraint violation if two callers run at the same time. -- CreatedByUserId = Guid.Empty marks system-generated creation; auditing considerations may require handling. +- Potential race condition if this method is invoked concurrently during initialization; ensure it runs once or enforce a database constraint on `Channels.Name` to prevent duplicates. --- @@ -225,15 +217,10 @@ public async Task EnsureSystemChannelAsync(string channelName, strin **Returns:** `Task` -Ensures there is a system-owned channel with the specified name by normalizing the name and either creating a new system channel or converting an existing non-system channel into a system channel. It then returns a ChannelDto describing the channel’s identity, topic, visibility, and system status. +Ensures that a system channel with the specified name exists in the database by normalizing the name and looking it up. If none is found, it creates a new system channel (not public) with CreatedByUserId set to an empty GUID and logs its creation. If a non-system channel already exists with that name, it is claimed as a system channel by updating its IsSystem and IsPublic flags and clearing the PasswordHash, logging a warning. It returns a ChannelDto describing the channel's identity and status. ## Remarks -Guarantees a canonical system channel identity for internal communications and server content streaming. It encapsulates the create-or-claim logic behind a single API and logs whether a channel was created or claimed. If the target channel already exists and is already marked as system, the method is effectively a no-op and simply returns its ChannelDto. - -## Notes -- There is a potential race condition when two concurrent invocations try to create the same system channel; relying on database constraints or proper isolation is recommended to avoid duplicates. -- If a non-system channel exists with the same name, the code will convert it to a system channel by setting IsSystem = true, IsPublic = false, and clearing PasswordHash; CreatedAt remains the original timestamp. -- The channel name is lower-cased and trimmed before the lookup, so callers should not rely on case-sensitive or whitespace-sensitive channel naming. +This method centralizes the architectural concept of system channels by guaranteeing a canonical system channel for a given name, creating or reclaiming it as needed and thereby preventing user-owned channels from shadowing system channels with reserved identifiers. --- @@ -254,15 +241,14 @@ public async Task GetChannelByNameAsync(string channelName) **Returns:** `Task` -GetChannelByNameAsync fetches a channel by its name after normalizing the input to lowercase and trimming whitespace. It creates a new DI scope to obtain EchoHubDbContext, queries the Channels set for a channel whose Name matches the normalized input, and, if found, counts the number of Messages belonging to that channel. If no matching channel exists, it returns null. The returned ChannelDto includes the channel’s Id, Name, Topic, visibility (IsPublic), the total MessageCount, CreatedAt timestamp, and two boolean flags indicating whether a password hash exists and whether a WrappedRoomKey is present, plus whether the channel is a system channel. This method centralizes the data-shaping of channel metadata for consumers (e.g., channel listings or details) and hides direct EF queries behind a concise API. +Fetches a channel by name in a case-insensitive manner and returns a compact [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) that includes the channel’s identity, metadata, and the current message count. It normalizes the input, creates a short-lived DI scope to obtain the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), resolves the channel by its lowercased name, counts its related [`Message`](../../EchoHub.Core/Models/Message.cs.md)s, and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) populated with the channel’s id, name, topic, visibility, created timestamp, and flags indicating whether a password or a wrapped room key exists, plus whether it is a system channel. If no channel matches, it returns `null`. ## Remarks -This abstraction centralizes channel metadata retrieval for UI and API surfaces, ensuring consistent ChannelDto shaping and hiding data-access details behind a single, strongly-typed API. It also clarifies that a null return indicates a non-existent channel. +By encapsulating the read path behind `GetChannelByNameAsync`, callers avoid dealing with EF queries or DI lifetimes directly. It centralizes how channel metadata is retrieved and projected into a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), which helps maintain consistent data contracts across the application. The per-call scope ensures proper disposal of the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and aligns with typical request-scoped lifetimes. ## Notes -- Returns null when no channel matches the provided name; callers should handle the nullable result. -- Performs two database queries (FirstOrDefaultAsync for the channel, then CountAsync for its messages) when a channel exists; this is straightforward but has a potential perf cost. -- Relies on input normalization to lowercase; if stored channel names are not stored in a comparable form, the lookup could miss matches. +- Potential ambiguity if multiple channels share the same normalized name; `FirstOrDefaultAsync` may return any one of them. +- Two database round-trips per invocation: one to fetch the channel and another to count its messages; consider combining into a single query if profiling shows this as a bottleneck. --- @@ -283,7 +269,42 @@ public async Task GetChannelCryptoAsync(string channelName) **Returns:** `Task` -Retrieves the ChannelCryptoDto describing the cryptographic state of a channel. The method normalizes the input channel name to lowercase and trims whitespace, then queries the EchoHubDbContext for a Channel with the matching name. If no channel is found, it returns null. If a channel exists, it returns a ChannelCryptoDto where the first value indicates whether a WrappedRoomKey is present (WrappedRoomKey != null) and includes the channel's EncryptionSalt. Data access occurs within a short-lived DI scope created from _scopeFactory, resolving EchoHubDbContext for the lookup. +GetChannelCryptoAsync retrieves the cryptographic metadata for a named channel. It normalizes the input channel name by lowercasing and trimming, opens a short-lived scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), and queries the `Channels` set for a channel whose `Name` matches the normalized value. If the channel is found, it returns a [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) whose first component indicates whether a `WrappedRoomKey` is present and whose second component carries the channel's `EncryptionSalt`; if no channel matches, it returns null. + +## Remarks +By encapsulating this logic in a dedicated method, callers avoid duplicating the database query and the cryptographic-state interpretation across the codebase. It centralizes encryption-metadata access behind a simple, asynchronous call and uses a scoped DbContext to minimize lifetime and concurrency issues. + +## Notes +- Returns null when the channel does not exist. +- The first component of [`ChannelCryptoDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) indicates the presence of a `WrappedRoomKey`; the `EncryptionSalt` may be null depending on data, so callers should handle null salts. + +--- + +### GetChannelKeyEnvelopeAsync +> **File:** `src/EchoHub.Server/Services/ChannelService.cs` +> **Kind:** method + +```csharp +public async Task<(string? EncryptionSalt, string? WrappedRoomKey)> GetChannelKeyEnvelopeAsync(string channelName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `EncryptionSalt` | `string?` | — | +| `WrappedRoomKey` | `string?` | — | + + +Gets the encryption envelope for a channel by name. It normalizes the input with `ToLowerInvariant()` and `Trim()`, opens a short-lived DI scope to resolve [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), and queries the `Channels` set for a channel whose `Name` matches. It returns a tuple of the channel's `EncryptionSalt` and `WrappedRoomKey` (as `string?`); if no matching channel exists, both values are `null`. + +## Remarks +This method centralizes access to channel encryption metadata and hides the details of DI-scoped DbContext usage from callers. It provides a single, easy-to-consume envelope for encryption-related data, which is useful when preparing to decrypt or unwrap channel-specific material. By returning `(string? EncryptionSalt, string? WrappedRoomKey)` as nullable values instead of throwing when a channel is absent, callers must handle the absence gracefully. + +## Notes +- Returns `(null, null)` when the channel cannot be found. +- Each invocation creates a new DI scope, which is appropriate for isolated data access but may have perf implications in hot paths; consider scope management or caching at a higher level if this method is called frequently. + --- @@ -298,7 +319,13 @@ public async Task> GetChannelListAsync() **Returns:** `Task>` -Fetches and returns a list of channel summaries. The method creates a scoped DI container, reads the EchoHubDbContext, loads all channels ordered by name, and maps each channel to a ChannelListItem that includes the channel's name, topic, the number of online users in that channel (via the presence tracker), whether the channel is public, and whether a password is set. This is typically used to populate a channel directory or lobby UI with up-to-date channel metadata and presence information. +GetChannelListAsync asynchronously loads all channels from the database, orders them by `Name`, and projects each channel into a [`ChannelListItem`](../../EchoHub.Core/Contracts/IChannelService.cs.md) that includes the channel's `Name`, `Topic`, the current online user count from `_presenceTracker.GetOnlineUsersInChannel(c.Name).Count`, the public status (`c.IsPublic`), and whether a password is configured (`c.PasswordHash != null`). The method returns a `List` suitable for rendering a channel catalog in a UI or API response. + +## Remarks +GetChannelListAsync acts as an orchestrator between the persistent store ([`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md)) and the in‑memory presence tracker (`_presenceTracker`). It centralizes channel-list assembly so callers don't need to know how presence counts are computed or how channels are stored. By resolving [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) within a short‑lived scope via `_scopeFactory.CreateScope()`, it ensures proper disposal of the database context per invocation and keeps DI concerns isolated from consumer code. + +## Notes +- Presence counts are computed per channel; listing many channels may impact response time. If the channel catalog grows large, consider caching or batching presence data to improve responsiveness. --- @@ -319,7 +346,40 @@ public async Task GetChannelMetaAsync(string channelName) **Returns:** `Task` -Fetches channel-level metadata for a given channel name without returning the messages themselves. It normalizes the input by lowercasing and trimming, resolves the channel via a scoped DI context, and if the channel exists returns a ChannelMetaDto containing the channel's Id, normalized Name, Topic, flags indicating whether a WrappedRoomKey or PasswordHash exists, the total MessageCount, the distinct count of Senders, an estimated storage footprint for the channel (attachments plus text), and the channel's CreatedAt timestamp. If no channel matches the provided name, the method returns null. The operation executes within a scoped DI context to ensure proper disposal of the database context. +GetChannelMetaAsync retrieves the metadata for a channel by its name and returns a [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) (or null if the channel cannot be found). It normalizes the input with `ToLowerInvariant()` and `Trim()`, opens a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) via `_scopeFactory.CreateScope()`, and looks up the channel in `db.Channels` by `Name`. When found, it computes the total `messageCount` from `db.Messages.CountAsync(...)`, the number of distinct `SenderUserId`s, and the estimated on-disk footprint from attachments and message text, then returns a new [`ChannelMetaDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) containing `c.Id`, `c.Name`, `c.Topic`, booleans for `c.WrappedRoomKey != null` and `c.PasswordHash != null`, the counts, the total footprint, and `c.CreatedAt`. + +## Remarks +This method uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to perform multiple read-only queries and aggregates data from `db.Channels` and `db.Messages`. The returned booleans reflect whether `c.WrappedRoomKey` or `c.PasswordHash` are non-null, indicating encryption and access protection. For encrypted channels, the footprint uses ciphertext sizes to reflect on-disk cost, and sender identities are treated as metadata preserved by the server even when messages are encrypted. + +## Notes +- Callers must handle the possibility that the return value is `null` when no channel matches the given `channelName`. + +--- + +### GetChannelTopicAsync +> **File:** `src/EchoHub.Server/Services/ChannelService.cs` +> **Kind:** method + +```csharp +public async Task<(string? Topic, bool Exists)> GetChannelTopicAsync(string channelName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `Topic` | `string?` | — | +| `Exists` | `bool` | — | + + +GetChannelTopicAsync retrieves the topic for a channel identified by `channelName`. It normalizes the input by calling `ToLowerInvariant()` and `Trim()`, opens a short-lived DI scope to resolve [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), queries the `Channels` set for a channel whose `Name` equals the normalized value using `FirstOrDefaultAsync`, and returns the `(Topic, Exists)` tuple; if no channel exists, it returns `(null, false)`. + +## Remarks +Encapsulates a small piece of data access behind a scoped context, avoiding long-lived DbContext usage and centralizing the normalization logic for channel lookups. The API communicates existence via the `Exists` flag, while the `Topic` can still be `null` if a channel exists but has no topic set. + +## Notes +- The lookup uses `FirstOrDefaultAsync` on `db.Channels`; if more than one channel shares the same normalized `Name`, the returned topic is non-deterministic; enforce unique `Name` values to avoid surprises. +- A per-call DI scope is created to obtain [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md); callers should not rely on an ambient scope for this operation. --- @@ -342,15 +402,10 @@ public async Task> GetChannelsAsync(Guid userId, i **Returns:** `Task>` -Fetches a paginated list of channels visible to the specified user, ensuring a default channel exists and applying system-channel visibility rules. It builds a Page of ChannelDto items by filtering channels based on whether they are system channels (only visible if the caller has the appropriate server role) or non-system channels (visible if public or if the user is a member). The method returns a PaginatedResponse containing the channels and the total count, ordered with system channels first and then by name. Per-channel metadata includes the number of messages, creation time, and security flags such as whether a password is set or a wrapped room key is present. +GetChannelsAsync returns a paginated list of channels visible to the user identified by `userId`. It first ensures a default channel exists, then determines if the caller can view system channels via `_serverLogs.CanView(caller?.Role ?? ServerRole.Member)`, and finally queries `db.Channels` to surface system channels only when permitted or non-system channels that are public or where the user is a member (via `ChannelMemberships`). The results are ordered with system channels first, then by `Name`, and projected into [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) objects containing each channel’s `Id`, `Name`, `Topic`, `IsPublic`, `Messages.Count`, `CreatedAt`, and flags for `PasswordHash != null` and `WrappedRoomKey != null`, plus `IsSystem`. The method returns a [`PaginatedResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) with the current page of channels and the total count. ## Remarks -This method centralizes channel discovery and visibility logic used by API surfaces and the UI. By enforcing system-channel visibility through server-side role checks and by materializing concise per-channel data into ChannelDto, callers receive a consistent, paged view of channels while preserving the default channel guarantee. The use of a scoped DbContext and a two-phase query (total count, then page fetch) encapsulates the data-access concerns behind a single, well-defined operation. - -## Notes -- EnsureDefaultChannelAsync(db) may create the default channel if it is missing; this side effect occurs on every call. callers should be aware of potential writes on read-like operations. -- The total and page fetch are executed as separate queries; data may change between these calls, affecting the reported total and the returned page. -- The channel's Messages.Count is computed in the projection, yielding a per-channel count without loading full message collections. +GetChannelsAsync centralizes the channel visibility policy: system channels (the live log room) are exposed only to users whose role allows viewing server logs, while non-system channels are visible if they are public or the user is a member, as determined by `ChannelMemberships`. The results are produced from a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and are ordered to surface system channels first, then alphabetically by name, and are projected into lightweight [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) records to drive UI lists without leaking unnecessary data. This encapsulation ensures consistent, permission-aware channel listing across the application. --- @@ -378,14 +433,11 @@ public async Task RekeyChannelAsync(Guid callerUserId, s **Returns:** `Task` -RekeyChannelAsync rotates the passphrase for an end-to-end encrypted channel by swapping the join-gate hash and the wrapped room key, while the actual content key remains unchanged so the history stays readable. The client then re-wraps the content key under the new passphrase-derived key. This operation is restricted to the channel creator; administrators who do not know the current passphrase cannot perform a rekey. +RekeyChannelAsync rekeys an end-to-end encrypted channel by swapping the `join-gate` hash and the `WrappedRoomKey`, re-wrapping the channel's content key under the new passphrase-derived key while leaving the content key itself unchanged so historical messages remain decryptable. The operation is restricted to the channel creator; admins cannot rekey a room unless they know the current passphrase. -## Remarks -This method encapsulates a security-sensitive transition that updates credential material without discarding encrypted content. By validating the new passphrase (via ValidateChannelPassword) and requiring non-empty new salt and wrapped key before touching the database, it preserves both confidentiality and integrity. The operation executes in a scoped data context to ensure the channel state is read and persisted atomically, reflecting the latest creator-approved configuration while keeping history intact. +Passphrase changes are validated, and the operation returns a [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) that is either a success containing a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) or a failure with a [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) and message (e.g. `NotFound`, `ValidationFailed`, or `Forbidden`). Internally, the method uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to locate the channel by name, ensure the channel is end-to-end encrypted, verify the caller is the creator, check the old password, and persist updates to `PasswordHash`, `EncryptionSalt`, and `WrappedRoomKey`. Upon success, it computes the current message count and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) reflecting the updated credentials. -## Notes -- New password, salt, and wrapped key are validated before any changes are persisted; if validation fails, the operation aborts with a ValidationFailed result. -- Rekeying is restricted to the channel creator; the method enforces this by verifying the caller's user ID and the correctness of the current passphrase before applying changes. +This operation centralizes the sensitive rekey workflow and ensures the channel state remains consistent and auditable within a single database transaction. --- @@ -409,18 +461,15 @@ public async Task SetChannelPasswordAsync(Guid callerUse **Returns:** `Task` -Sets, changes, or clears (null) a channel's join password. This operation is allowed only for the channel's creator or an administrator. End-to-end encrypted channels do not accept password changes here; such channels must use RekeyChannelAsync to rotate the passphrase, preserving the room key envelope. +Sets, changes, or clears (null) a channel's join password. Creator or admin only. Not available on end-to-end encrypted channels — those change passphrase via RekeyChannelAsync so the room key envelope stays consistent. The method normalizes the channel name to lowercase and trims, validates the password via `ValidateChannelPassword`, and then uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to locate the channel and enforce authorization. If the channel doesn't exist, is a system channel, or is end-to-end encrypted, it returns an appropriate [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) failure. If the caller is the channel creator or an admin, it updates the channel's `PasswordHash` (hashing a non-null password with `BCrypt.Net.BCrypt.HashPassword` or clearing it when `password` is null), persists the changes, counts the channel's messages, and returns a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) describing the channel along with flags for password protection and encryption. ## Remarks - -By centralizing password management in this method, the system enforces consistent authorization, validation, and persistence rules for channel passphrases. It guards against modifying system channels and avoids altering encryption state for end-to-end encrypted channels at this layer, delegating that concern to RekeyChannelAsync when appropriate. The method returns a ChannelDto describing the updated channel, including whether a password is set and whether the channel remains end-to-end encrypted. +Centralizes channel password management behind a single operation that enforces ownership and role-based access. It interacts with the EF Core context to fetch and persist channel state and to surface up-to-date metadata via [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) (including whether a password is active and whether the channel is end-to-end encrypted). The method explicitly avoids modifying end-to-end encrypted channels here, directing such changes to `RekeyChannelAsync` to preserve the room key envelope. ## Notes +- Clearing the password (passing `null`) removes the join password, which may affect who can join depending on the channel's other visibility settings. +- Only the channel creator or an admin can perform password changes; otherwise the call returns `ChannelError.Forbidden`. -- Passwords are stored as BCrypt hashes; if a null password is provided, the password is cleared (PasswordHash becomes null). -- The channel name is normalized to lowercase and trimmed before lookup to ensure stable, case-insensitive matching. -- If the channel does not exist, is a system channel, or the caller lacks sufficient privileges (not the creator or an admin), the operation fails with NotFound, Protected, or Forbidden respectively. -- After a successful change, the returned ChannelDto includes the current message count and flags indicating HasPassword and HasWrappedKey, reflecting the channel's encryption state. --- @@ -444,15 +493,16 @@ public async Task UpdateTopicAsync( **Returns:** `Task` -Updates the topic of a channel, performing authorization, validation, and persistence in one operation. Given the caller's user ID and the channel name, it normalizes the name, enforces topic length (when provided), ensures only the channel creator can update, persists the topic change, and returns a ChannelOperationResult containing a ChannelDto with the channel's identity, current topic, visibility, message count, creation time, and indicators for password protection and wrapped room key. +Updates the topic of a channel by name, but only if the caller is the channel's creator. It trims and validates a non-null `topic` against `ValidationConstants.MaxChannelTopicLength` (a null `topic` clears the topic), normalizes the channel name to lower-case, persists the change via EF Core, and returns a `ChannelOperationResult.Success` with a [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) containing the updated channel data plus a live `MessageCount`. If the channel is missing, or the caller isn't the creator, or the topic is too long, the method returns a corresponding failure via `ChannelOperationResult.Fail` with an appropriate [`ChannelError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md). ## Remarks -This method centralizes the domain logic for updating a channel topic behind a service boundary. It enforces the business rule that only the channel creator may modify the topic, and it uses a scoped DbContext to apply the change, ensuring consistency with the data-access layer. The returned ChannelDto exposes a compact snapshot of the channel, including whether the channel is password-protected and whether a wrapped room key exists, which informs UI decisions without leaking internal state. + +Only the channel creator can update the topic, enforced by comparing `dbChannel.CreatedByUserId` to `callerUserId`. The method uses a short-lived DI scope to fetch [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), performs a read of the channel by name, applies the update, saves changes, and then counts the channel's `Messages` to populate the `MessageCount` in the returned [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md). The [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) also exposes security-related flags derived from `PasswordHash` and `WrappedRoomKey` to help clients adjust their UI and access logic. ## Notes -- Topic can be null to clear the current topic (the code stores topic?.Trim()). -- Channel name normalization is applied so lookups are case-insensitive and consistent. -- The operation yields concrete failure codes (NotFound, Forbidden, ValidationFailed) to guide callers in handling user feedback. + +- The `MessageCount` is retrieved via `db.Messages.CountAsync(m => m.ChannelId == dbChannel.Id)` after applying the update; for very active channels this can add latency. +- Passing a `null` `topic` clears the topic; callers should handle potential null values in the UI. --- @@ -473,83 +523,15 @@ private static string? ValidateChannelPassword(ref string? password) **Returns:** `string?` -Normalizes and validates a channel password. If the input password is null or consists only of whitespace, it is treated as no password (the value is effectively normalized to null) and no error is produced. For non-empty input, the method enforces length constraints defined by ValidationConstants and returns an error message when the password is too short or too long; otherwise, it returns null to indicate a valid password. The password is passed by reference, allowing the caller to observe and adopt the normalized value in place. +Normalizes a provided channel password by treating whitespace-only input as the absence of a password (`null`) and then enforces length constraints from [`ValidationConstants`](../../EchoHub.Core/Constants/ValidationConstants.cs.md) (minimum via `MinChannelPasswordLength`, maximum via `MaxPasswordLength`). It returns an error message when the password is too short or too long, or `null` when the value is valid. ## Remarks -Centralizes the channel password policy so all call sites apply the same minimum and maximum length rules and the same interpretation of an empty password. The implementation defers to ValidationConstants for policy values, ensuring changes to password requirements propagate consistently. The use of a ref parameter enables in-place normalization, so the normalized password (or its absence) is visible to the caller without requiring a separate assignment. - -## Notes - -- The caller must pass a mutable variable by ref; passing a constant or read-only expression will not compile. -- The method returns null when the password is valid (or when treated as no password), or a non-null string containing the user-facing validation message when invalid. - ---- - -## GetChannelKeyEnvelopeAsync -> **File:** `src/EchoHub.Server/Services/ChannelService.cs` -> **Kind:** method - -```csharp -public async Task<(string? EncryptionSalt, string? WrappedRoomKey)> GetChannelKeyEnvelopeAsync(string channelName) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `EncryptionSalt` | `string?` | — | -| `WrappedRoomKey` | `string?` | — | - - -Gets the encryption envelope for a given channel by name by querying the Channels table via a scoped EchoHubDbContext; it returns the channel's EncryptionSalt and WrappedRoomKey as a tuple, or (null, null) if the channel cannot be found. This method is intended for scenarios where callers need to access per-channel cryptographic parameters to decrypt or initialize channel data, without surfacing the data-access details to higher layers. - -## Remarks -Encapsulates a small, cohesive data-access operation and hides EF Core/DI plumbing from callers. By creating a scoped scope and resolving EchoHubDbContext per call, it avoids leaking a long-lived DbContext into consumer code and makes the envelope retrieval occur in a single boundary. It relies on the Channels table's Name field to identify a channel and returns two optional values, allowing callers to decide how to handle missing encryption data. This placement fits ChannelService as a dedicated place to retrieve channel-related metadata used by encryption/decryption flows. +By using a `ref` parameter for `password`, the input variable may be mutated to `null` by the callee to reflect the decision that no password is set. This centralizes channel password rules in one place, ensuring consistent behavior across channel creation and update flows. ## Notes -- Caller must handle possible nulls in both EncryptionSalt and WrappedRoomKey; if the channel isn't found, both will be null. -- Since the input channelName is lowercased before querying, ensure channel.Name storage is consistent (lowercase) to guarantee matches; otherwise, the lookup could miss existing channels. - ---- - -## GetChannelTopicAsync -> **File:** `src/EchoHub.Server/Services/ChannelService.cs` -> **Kind:** method - -```csharp -public async Task<(string? Topic, bool Exists)> GetChannelTopicAsync(string channelName) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `Topic` | `string?` | — | -| `Exists` | `bool` | — | - -Retrieves the topic for a named channel from the EchoHub database. The method normalizes the input (lowercases and trims), opens a scoped DI context to resolve EchoHubDbContext, and queries the Channels set for a channel with the matching name. If no channel is found, it returns (null, false); otherwise it returns the channel's Topic along with true, indicating the channel exists. The operation is asynchronous, allowing callers to await the database query without blocking. - -## Remarks -This method encapsulates a small, focused data-access concern: turning a channel name into its topic, while also signaling whether the channel exists. Returning a value tuple (Topic, Exists) makes it straightforward for call sites to branch logic without null checks against the channel entity. The DI-scoped DbContext use ensures clean disposal per call and aligns with standard EF Core usage in a DI-driven application. - -## Example -```csharp -var (topic, exists) = await GetChannelTopicAsync("general"); -if (exists) -{ - Console.WriteLine(topic); -} -else -{ - Console.WriteLine("Channel not found."); -} -``` - -## Notes -- The lookup lowercases the channel name; ensure stored channel names are normalized the same way to guarantee matches. -- Topic can still be null even when Exists is true; callers should handle null topics gracefully. -- If multiple channels share the same name (data integrity issue), FirstOrDefaultAsync returns the first match. +- Because the parameter is `ref`, the caller should re-read the original variable after the call because its value may have been changed to `null`. +- A return value of `null` indicates a valid or absent password; non-null strings are error messages describing the violation. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ChatService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ChatService.cs.md index ac6db24..a06e486 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ChatService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ChatService.cs.md @@ -10,20 +10,20 @@ - [BroadcastChannelUpdatedAsync](#broadcastchannelupdatedasync) - [BroadcastMessageAsync](#broadcastmessageasync) - [BroadcastToAllAsync](#broadcasttoallasync) + - [BuildLogBacklog](#buildlogbacklog) - [BuildReplyRef](#buildreplyref) - [FileIdFromUrl](#fileidfromurl) - [GetChannelHistoryAsync](#getchannelhistoryasync) - [GetChannelHistoryInternalAsync](#getchannelhistoryinternalasync) - [GetChannelsForUserAsync](#getchannelsforuserasync) - [GetOnlineUsersAsync](#getonlineusersasync) + - [JoinChannelAsync](#joinchannelasync) - [LeaveChannelAsync](#leavechannelasync) + - [SanitizeNewlines](#sanitizenewlines) - [SendMessageAsync](#sendmessageasync) - [UpdateStatusAsync](#updatestatusasync) - [UserConnectedAsync](#userconnectedasync) - [UserDisconnectedAsync](#userdisconnectedasync) -- [BuildLogBacklog](#buildlogbacklog) -- [JoinChannelAsync](#joinchannelasync) -- [SanitizeNewlines](#sanitizenewlines) --- @@ -36,15 +36,16 @@ public class ChatService : IChatService ``` -Coordinates the server-side chat workflow: presence tracking, channel membership and validation, message handling (decryption, sanitization, spam checks, reply validation, optional link embeds), persistence, and broadcasting to configured IChatBroadcaster implementations. Reach for ChatService when you need the complete, policy-enforced chat behavior used by the hub (connect/disconnect, join/leave, send message, history, status and broadcasts) rather than calling lower-level pieces like the channel store, encryption, or broadcasters individually. +Coordinates chat-related operations for the server-side hub: connection lifecycle, channel joins/leaves, message sending, history retrieval and broadcasting. Reach for `ChatService` when you need a single, authoritative orchestrator that applies presence tracking, channel validation, encryption/decryption, spam/mute rules, link-embed enrichment, storage and multi-backend broadcasting rather than implementing those concerns in a hub or duplicating them across callers. ## Remarks -ChatService is an orchestration façade that centralizes chat policies and cross-cutting concerns so the rest of the system sees a single, consistent chat surface. It delegates channel validation and membership (including password checks) to the channel service, uses PresenceTracker for online lists, defers spam decisions to the SpamGuard, asks LinkEmbedService for embeds, and relies on IMessageEncryptionService for decrypt/encrypt logic. It also records runtime telemetry and logs through ServerStatsCollector and ServerLogsService, and persists or reads backlog data via FileStorageService for special channels (for example, the rolling log room). Finally, it shields broadcasters from internal policies by converting and routing messages appropriately (e.g., encrypted payloads for some clients, plaintext for legacy/IRC broadcasters). +`ChatService` centralizes cross-cutting chat logic so transport implementations (for example SignalR or an IRC bridge) can remain thin. It delegates channel membership and validation to [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md), relies on [`PresenceTracker`](PresenceTracker.cs.md) for presence state, uses [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) and [`LinkEmbedService`](LinkEmbedService.cs.md) to handle encrypted payloads and link previews, enforces anti-abuse via [`SpamGuard`](SpamGuard.cs.md), persists attachments via [`FileStorageService`](FileStorageService.cs.md), and emits audit/operational data to [`ServerLogsService`](ServerLogs/ServerLogsService.cs.md) and [`ServerStatsCollector`](Stats/ServerStatsCollector.cs.md). Outgoing delivery is performed by the configured [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implementations so the same message lifecycle (validation, enrichment, storage) can be broadcast to multiple transports consistently. ## Notes -- Join throttle: only first-time joins (where the user is not already a member) count toward the join throttle to avoid falsely flagging reconnect/auto-join bursts. -- Live log room is treated as read-only; attempts to write to it are rejected early and its backlog is sourced from a rolling log file — only the first history page returns a backlog. -- SpamGuard operates on the stored content (ciphertext when end-to-end encryption is used) and never requires or performs decryption; muting and escalation are handled through the service's moderation workflow. +- Join throttling only counts first-time joins (no existing membership) to avoid tripping during normal auto-join bursts on reconnect; clients that re-join known channels should not trigger the throttle. +- The special "log"/live-log room is treated as read-only and its backlog comes from rolling log files rather than DB messages; paging behaves differently for that channel. +- Spam checks operate on the stored content (which may be ciphertext for end-to-end encrypted rooms) — the guard does not require plaintext to function and escalation results in timed mutes issued by the server. +- Message handling includes decryption (clients may send encrypted payloads while other protocols supply plaintext), stripping of any explicit encryption prefix typed by users to prevent spoofing, and plaintext sanitization (for example collapsing excessive newlines) before optional embed fetching and storage. --- @@ -84,14 +85,7 @@ public ChatService( | `logger` | `ILogger` | — | -Constructs a ChatService by injecting its required collaborators and wiring them to private fields. This constructor is invoked by the dependency injection container when a ChatService is created, supplying services for scope management, presence tracking, message broadcasting, content embedding, encryption, channel operations, file storage, spam protection, server logging, and statistics collection. The use of `IEnumerable` indicates that multiple broadcasters can participate in delivering messages and events, allowing pluggable delivery strategies without changing ChatService code. - -## Remarks -ChatService acts as an orchestration hub for chat functionality. By depending on interfaces rather than concrete implementations, it remains highly testable and extensible: you can substitute mocks or fakes for broadcasters, presence tracking, or encryption in tests or different environments. The broadcaster collection enables evolving notification strategies by simply registering new IChatBroadcaster implementations, aligning with the open/closed principle. - -## Notes -- No null-checks are performed in the constructor; rely on the DI container to provide non-null dependencies. If ChatService might be created outside the DI pipeline, consider adding guards. -- When using `IEnumerable`, all registered broadcasters will be resolved and invoked; behavior depends on the concrete broadcaster implementations. +Initializes a new `ChatService` instance by capturing its required collaborators through dependency injection and storing them in private fields for later use. The constructor takes services for scope management (`IServiceScopeFactory`), presence tracking ([`PresenceTracker`](PresenceTracker.cs.md)), a collection of broadcasters ([`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md)), link embedding ([`LinkEmbedService`](LinkEmbedService.cs.md)), message encryption ([`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)), channel operations ([`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md)), file storage ([`FileStorageService`](FileStorageService.cs.md)), spam protection ([`SpamGuard`](SpamGuard.cs.md)), server-side logging ([`ServerLogsService`](ServerLogs/ServerLogsService.cs.md)), statistics collection ([`ServerStatsCollector`](Stats/ServerStatsCollector.cs.md)), and a logger (`ILogger`), wiring them to internal fields like `_scopeFactory`, `_presenceTracker`, `_broadcasters`, `_embedService`, `_encryption`, `_channelService`, `_fileStorage`, `_spamGuard`, `_serverLogs`, `_statsCollector`, and `_logger` so the service can perform broadcasting, embedding, encryption, channel management, persistence, spam guarding, logging, and metrics collection. --- @@ -113,14 +107,10 @@ public Task BroadcastChannelDeletedAsync(string channelName) **Returns:** `Task` -BroadcastChannelDeletedAsync publishes a channel-deletion event to all connected clients by delegating to the shared broadcasting pipeline. It forwards the channelName to each subscriber through SendChannelDeletedAsync, coordinated by BroadcastToAllAsync to ensure every participant receives the notification. +BroadcastChannelDeletedAsync asynchronously broadcasts a channel-deleted event to all connected clients by invoking `SendChannelDeletedAsync` on each subscriber, via the central `BroadcastToAllAsync` mechanism using the lambda `b => b.SendChannelDeletedAsync(channelName)`. It accepts a `string channelName` and returns a `Task` representing the asynchronous broadcast operation. ## Remarks -Provides a domain-friendly API that hides the broadcasting details behind a simple, expressive method name. By delegating to BroadcastToAllAsync, it centralizes how channel-deletion notifications are distributed, reducing duplication and ensuring consistent behavior across all subscribers. - -## Notes -- No input validation is performed on channelName; callers should ensure the value is non-null and meaningful before invocation. -- Exceptions raised during per-subscriber delivery will propagate via the returned Task; callers should decide whether to await and handle failures. +This method is a thin facade over the generic broadcasting path. It delegates to `BroadcastToAllAsync` to deliver the `SendChannelDeletedAsync` call to every connected client, isolating the channel-deletion notification from the underlying broadcast implementation and ensuring consistent semantics across different events. --- @@ -143,14 +133,10 @@ public Task BroadcastChannelUpdatedAsync(ChannelDto channel, string? channelName **Returns:** `Task` -BroadcastChannelUpdatedAsync is a thin wrapper that notifies all connected clients that the specified channel has been updated. It accepts the ChannelDto describing the channel and an optional new channelName. The method delegates to the shared broadcast mechanism (BroadcastToAllAsync) by applying a function that calls SendChannelUpdatedAsync on each client with the provided payload. Use this when you want real-time client UIs to reflect changes to a channel, such as a rename or updated metadata, without having to push updates individually to each client. +BroadcastChannelUpdatedAsync forwards the given [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) and an optional `string? channelName` to all connected clients by routing through the shared broadcast pipeline: it calls `BroadcastToAllAsync` with a lambda that invokes each client's `SendChannelUpdatedAsync`. ## Remarks -BroadcastChannelUpdatedAsync centralizes the channel-update notification path in ChatService. By encapsulating the broadcast call behind this single method, callers don't need to know about how clients are iterated or how the payload is delivered; tests can mock this entry point, and future changes to the broadcasting strategy stay confined here. - -## Notes -- This method only notifies clients; it does not modify the channel data in storage. -- If channelName is non-null, it will be included as part of the payload and may be used by clients to display the new name. +Thin wrapper around the existing broadcast mechanism for the 'channel updated' event. It centralizes the notification path so UI clients stay in sync when a channel changes, and it decouples `ChatService` from the concrete hub method used to push updates. If you add additional update events later, similar wrappers can be introduced to keep the surface area small and consistent. --- @@ -173,14 +159,11 @@ public Task BroadcastMessageAsync(string channelName, MessageDto message) **Returns:** `Task` -BroadcastMessageAsync asynchronously broadcasts the provided MessageDto to all clients subscribed to the specified channel by delegating to BroadcastToAllAsync. This method serves as a focused helper for channel-scoped messages, insulating callers from the details of iterating over recipients and invoking SendMessageToChannelAsync on each. +BroadcastMessageAsync is a thin asynchronous wrapper that broadcasts a [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) to a named channel by delegating to the shared broadcast pipeline `BroadcastToAllAsync`. It forwards the `channelName` and `message` to every recipient by invoking the lambda `b => b.SendMessageToChannelAsync(channelName, message)`. ## Remarks -This wrapper consolidates the channel-based dispatch pattern into a single, discoverable API on the chat service. It centralizes the broadcasting contract so callers do not need to know how broadcasting is implemented (per-subscriber dispatch vs. transport specifics), and it supports testing and mocking of channel messages by providing a stable entry point. +This method acts as a channel-scoped entry point for the generic broadcast mechanism, decoupling channel-specific semantics from the underlying broadcasting orchestration. By composing with the `BroadcastToAllAsync` pipeline, it ensures consistent delivery behavior across recipients while allowing the underlying strategy to evolve without changing the public API. The wrapper also simplifies testing by isolating the channel-binding logic from the broadcast traversal. -## Notes -- The visible code does not show input validation; consider validating channelName and message upstream to avoid potential ArgumentNullException during broadcasting. -- The method returns a Task; await it to observe completion and to surface any exceptions from the underlying broadcast pipeline (e.g., failures in SendMessageToChannelAsync). --- @@ -201,15 +184,35 @@ private async Task BroadcastToAllAsync(Func action) **Returns:** `Task` -BroadcastToAllAsync is a private helper that sequentially applies an asynchronous action to every broadcaster in the _broadcasters collection. By awaiting the provided `Func` for each broadcaster, it ensures ordered, per-broadcaster execution. If an individual broadcaster throws, the exception is caught and logged with the broadcaster’s runtime type name, allowing the remaining broadcasters to continue without interrupting the overall broadcast flow. +BroadcastToAllAsync iterates over the collection of `_broadcasters` and applies the provided `Func` to each broadcaster, awaiting the resulting task before moving to the next. If an invocation throws, the exception is caught and logged via `_logger.LogError`, including the broadcaster's type name from `broadcaster.GetType().Name`, and processing continues with the remaining broadcasters. Use this helper when you need to perform a common asynchronous operation across all configured broadcasters while tolerating individual failures. + +--- + +### BuildLogBacklog +> **File:** `src/EchoHub.Server/Services/ChatService.cs` +> **Kind:** method + +```csharp +private List BuildLogBacklog(string channelName) +``` + +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `channelName` | `string` | — | + +**Returns:** `List` + + +Turns the log backlog into transport-encrypted [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md)s so past log lines render identically to streamed messages on the client. It reads backlog entries from `_serverLogs.ReadBacklog()`, encrypts each entry’s content with `_encryption.Encrypt(entry.Content)`, assigns a fresh `Guid` via `Guid.NewGuid()`, uses `ServerLogsService.SenderName` as the sender, attaches the provided `channelName`, and preserves each backlog entry’s `Timestamp`. This method never touches the database. ## Remarks -This helper centralizes the common pattern of broadcasting to multiple chat broadcasters while isolating failures. It provides a small orchestration layer that coordinates across the _broadcasters collection and ensures one faulty broadcaster does not derail the entire operation. Because it is private, this logic remains an implementation detail of the class rather than part of its public API. +Acts as an adapter that repackages backlog entries into the identical [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) format used for real-time messages, enabling a seamless, consistent rendering experience for historical logs. It centralizes encryption and ID generation at the point of backlog materialization, reducing divergence between what clients see in history and what they see in live streams. ## Notes -- The method is sequential; broadcasting happens one broadcaster after another, not in parallel. If you need parallel broadcasting, use a different approach. -- Exceptions from action are swallowed per-broadcaster; if you need different error handling, handle it inside the action or upstream. -- Logging uses broadcaster.GetType().Name to identify failures; if multiple broadcasters share a type, the log may not distinguish instances. +- The `Id` of each [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) is generated per invocation with `Guid.NewGuid()`, so IDs are not stable across reloads. +- This method is private; it is an internal helper that shapes backlog data specifically for the client-render path and is not directly callable from outside. --- @@ -230,15 +233,14 @@ private ReplyRefDto BuildReplyRef(Message target) **Returns:** [`ReplyRefDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) -BuildReplyRef constructs the wire reference for a reply target by decrypting the target’s content to obtain a plaintext snippet, conditionally truncating it, and then re-encrypting the result into a ReplyRefDto that carries the target’s ID, sender, and the encrypted snippet. Specifically, it decrypts target.Content; if the decrypted text is not recognized as E2E room ciphertext and exceeds 120 characters, it truncates to 120 characters and appends an ellipsis; finally, it encrypts the possibly shortened plaintext and returns a ReplyRefDto. +Builds a wire reference for a reply target by returning a [`ReplyRefDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) that contains the target’s ID, the sender’s username, and an encrypted surface plaintext. It decrypts the target’s `Content` to obtain plaintext; if the decrypted text is not a room ciphertext (i.e. `!RoomCrypto.IsRoomCiphertext(plain)`) and longer than 120 characters, it truncates to 120 characters and appends an ellipsis. The (potentially truncated) plaintext is then encrypted again with `_encryption.Encrypt` before being stored in the [`ReplyRefDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) alongside the target’s `Id` and `SenderUsername`. ## Remarks -Encapsulates the logic for producing a secure, compact teaser of the original message for a reply. It ensures that non-E2E content is truncated to a sane length while guaranteeing that E2E content remains intact (and thus decryptable) by avoiding truncation. It delegates the actual encryption to the central _encryption service and uses RoomCrypto.IsRoomCiphertext to decide when truncation is safe. +This method centralizes the policy for constructing reply references: End-to-End room ciphertext is preserved as ciphertext and not truncated in this pass, while non-End-to-End plaintext is surfaced only as a concise preview. It coordinates with `_encryption` and [`RoomCrypto`](../../EchoHub.Core/Security/RoomCrypto.cs.md) to decide truncation and to produce a transport-ready reference that the client can render without exposing raw plaintext. ## Notes -- Truncation only occurs when the decrypted content does not look like room ciphertext; this prevents corrupting E2E data. -- The returned snippet is encrypted before being included in ReplyRefDto. -- The 120-character limit is a fixed server-side threshold governing the preview length. +- Truncation uses an ellipsis character `…` and a hard limit of 120 characters for non-E2E plaintext. + --- @@ -259,14 +261,14 @@ private static string FileIdFromUrl(string url) => url.Split('/')[^1] **Returns:** `string` -Extracts the storage file id from an attachment URL by taking the last path segment after the final '/'. It is intended for URLs that look like '/api/files/{id}' and is used in scenarios where the code needs to derive the identifier from a URL without performing a full URL parser. +Extracts the storage file id from an attachment URL by taking the last path segment (for example '/api/files/{id}'). This private helper is used when only the id is needed from a known URL rather than maintaining the id separately. It relies on splitting the URL on '/' and selecting the final segment with `[^1]`, without performing further validation. ## Remarks -Because this helper relies on a simple string.Split and the C# index-from-end operator [^1], it assumes the input is a plain path that ends with the id and does not end with a trailing slash. If the URL ends with '/', the result will be an empty string. It also does not guard against null inputs, which would raise an exception at runtime. In practice this method is a tiny, in-class utility that centralizes the id extraction so callers don't duplicate the split logic. +By centralizing the assumption that the file id is the last path segment, this helper reduces duplication and keeps callers focused on higher-level logic. It relies on a simple split-and-select approach and does not validate edge cases such as trailing slashes or query parameters. ## Notes -- Trailing slash in the URL yields an empty id; normalize the URL or trim the trailing slash before calling. -- Null or empty input is not handled; ensure a non-null, non-empty URL is passed. +- Trailing slash or query string edge cases may yield an empty result or a value containing extraneous parts. +- No input validation: passing `null` or clearly malformed URLs will throw at runtime. --- @@ -289,13 +291,14 @@ public async Task> GetChannelHistoryAsync(string channelName, i **Returns:** `Task>` -GetChannelHistoryAsync retrieves a paged history of messages for a given channel. It normalizes the channel name to lowercase and trims it, clamps the requested count to the range [1, ValidationConstants.MaxHistoryCount], and ensures offset is non-negative. If the channel is a logs channel (no DB messages), the backlog is read from the rolling log file: the first page is populated via BuildLogBacklog, while older pages are empty (files are archives). For regular channels, a short-lived DI scope is created to resolve EchoHubDbContext, and the method delegates to GetChannelHistoryInternalAsync to fetch the requested slice from the database. The call is asynchronous and returns a `List`. +Gets a paginated history of messages for a specified channel. The method normalizes `channelName` to lowercase and trims it, clamps `count` to `ValidationConstants.MaxHistoryCount`, and ensures `offset` is non-negative. If the channel is a log-backed channel (no database messages), it returns the backlog on the first page via `BuildLogBacklog` and an empty list for subsequent pages. Otherwise, it creates a DI scope to obtain an [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and delegates to `GetChannelHistoryInternalAsync` to fetch messages from the database. ## Remarks -Serves as a unified history retrieval entry point that abstracts away the storage details behind a paging API. It centralizes channel-history concerns so callers don't need to know whether messages come from the rolling log or the database, while preserving the expected paging semantics across both sources. +Log-backed channels are served from the rolling log backlog, while database-backed channels fetch history from the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md). The method unifies access to channel history by hiding the data source, but preserves the backlog-first paging contract for log channels (as documented in the inline comment). ## Notes -- Be aware that for log channels, only the first page contains backlog data; requesting subsequent pages returns an empty list. +- For log-backed channels, requesting an `offset` > 0 returns an empty list; only the first page can include backlog items. +- Channel name normalization is performed before retrieval; callers may pass mixed-case or whitespace around the channel name. --- @@ -319,16 +322,8 @@ private async Task> GetChannelHistoryInternalAsync(EchoHubDbCon **Returns:** `Task>` -Retrieves a page of messages for a named channel, enriching each entry with sender metadata, attachment data, and embed information, so the client can render a historical view of the chat. Call this when you need to assemble a channel's message history in a transport-ready form, with tombstoned accounts preserved and content decrypted for display. +Fetches a batch of messages for a named channel and returns them as `List` after assembling sender metadata, decryption, and attachment/embed preparation. It first resolves the channel by name via `EchoHubDbContext.Channels`; if the channel cannot be found, it returns an empty list. To preserve tombstoned messages (where a sender account has been deleted), it performs a left join with `Users` so messages can still appear with null `NicknameColor` and `DisplayName`, then applies pagination with `offset` and `count` and finally reverses the results to chronological order. The method also gathers reply targets for quotes, groups attachments by message, validates attachments against the current file store via `_fileStorage.GetStoredFileIds()`, decrypts message content using `_encryption.Decrypt`, and decrypts/deserializes embeds from `EmbedJson` using `JsonSerializer`. Attachments are pruned if their underlying files are missing; if a message has no live attachments and no plaintext content, it is pruned from the result. Attachments and previews are re-encrypted for transport, and embedded metadata (when valid JSON) is deserialized into [`EmbedDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) instances. This function coordinates with its dependencies to ensure only live content is delivered and that the client receives an encryption-safe, transport-ready history payload. -## Remarks -Conceptually, this method centralizes history construction: it loads messages for a channel, preserves messages from deleted accounts by left-joining with users, collects reply targets for quote rendering, and hydrates attachments while pruning entries that no longer have valid files. It decrypts message content and any embed metadata, reconstructs attachments for transport (including per-attachment previews that are re-encrypted), and translates raw data into the client-facing DTOs (MessageDto, AttachmentDto, EmbedDto). This batching approach minimizes round-trips by prefetching related data (replies, attachments, embeds) in a single operation. - -## Notes -- If a channel is not found, the method returns an empty list rather than throwing. Callers should handle an empty history gracefully. -- Messages associated with deleted users are preserved in history, but their display name and nickname color may be null; UI code should account for missing metadata. -- Attachments are shown only if their underlying files still exist on disk; messages with only vanished attachments and no plaintext content are pruned from the result. -- The method decrypts content and embed JSON, and it re-encrypts payloads for transport; the exact transport-encryption details are handled deeper in the pipeline and may depend on the caller's context. --- @@ -350,17 +345,10 @@ public Task> GetChannelsForUserAsync(string username) **Returns:** `Task>` -Retrieves the list of channel names that a specific user participates in, exposed as an asynchronous method. It delegates to the presence tracker via _presenceTracker.GetChannelsForUser(username) and wraps the result in Task.FromResult, which means the call completes synchronously and simply presents an async surface to callers. Use this when you require an async API surface (e.g., to be consistent with other async members) even though the underlying operation is synchronous. +This method is a thin wrapper around `_presenceTracker.GetChannelsForUser` that returns the channels for a given `username` as a `Task>`. It preserves the asynchronous API surface while delegating the actual lookup to the presence tracker. ## Remarks - -It provides an asynchronous API surface for retrieving a user's channel list by delegating to the presence tracker. This keeps ChatService methods consistent in an async context and avoids exposing a synchronous API directly to callers that expect Task-returning methods. The actual retrieval is synchronous, so this wrapper does not introduce true asynchrony. - -## Notes - -- Completes synchronously; no actual I/O is awaited here. -- Any exception from _presenceTracker.GetChannelsForUser will be thrown at call time (not surfaced as a faulted Task). -- If you anticipate long blocking work, prefer an actual asynchronous implementation or an asynchronous presence tracker. +By delegating to `_presenceTracker`, this symbol keeps the `ChatService` decoupled from the concrete presence-tracking implementation. This makes it easier to test `GetChannelsForUserAsync` in isolation and to swap the presence logic without changing callers, while still offering a stable public surface via `GetChannelsForUserAsync`. --- @@ -381,294 +369,195 @@ public async Task> GetOnlineUsersAsync(string channelName) **Returns:** `Task>` -Retrieves the list of online users for a given chat channel by joining the in-memory presence tracker with the database-stored user records. It normalizes the channel name to lowercase and trims whitespace, fetches the set of online usernames from the tracker, queries EchoHubDbContext.Users for those usernames that are not Invisible, and then maps each user to a UserPresenceDto that includes the in-memory IRC-only flag. This method is typically used when you need to present the current participants of a channel, excluding hidden users, with their display metadata. +GetOnlineUsersAsync returns the current online users for a given channel as a list of [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) records. The channel name is normalized with `ToLowerInvariant()` and trimmed, then the in-memory `_presenceTracker` is consulted to obtain the set of online usernames for that channel. A scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) is then used to query `db.Users` for those usernames that are not `UserStatus.Invisible`, materializing the result with `ToListAsync()`. Finally, the code maps each [`User`](../../EchoHub.Core/Models/User.cs.md) to a [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md), including the `IsIrcOnly` flag via `_presenceTracker.IsIrcOnly(u.Username)`, and returns the list. ## Remarks -Acts as a bridge between transient presence state and the persistent user store, ensuring that the live list respects visibility rules while enriching with profile data. The conversion to UserPresenceDto happens after the EF query to allow the IRC-only flag to be derived from the presence tracker, not stored in the database. The use of a scoped DbContext keeps the data access isolated and safe for concurrent calls. - -## Example -```csharp -// Example usage -var online = await chatService.GetOnlineUsersAsync("general"); -Console.WriteLine($"Online in #general: {online.Count}"); -``` +The method bridges in-memory presence information with the persisted user data, encapsulating the lookup so callers need only know a channel name to obtain current participants. It also enforces visibility rules by excluding users with `UserStatus.Invisible` and by deriving the `IsIrcOnly` state from the live tracker rather than from the database. The scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) usage respects dependency injection lifetimes and limits the DbContext to the operation's boundary. ## Notes -- If a username is online according to the tracker but missing from the database, it will be ignored. -- Channel name normalization means calls with different casing or surrounding whitespace map to the same channel. -- The IsIrcOnly flag is determined by the presence tracker and is included in each UserPresenceDto. +- Be aware that the query loads a list of [`User`](../../EchoHub.Core/Models/User.cs.md) records for all online usernames; channels with large online counts could have performance implications, and paging or batching may be warranted in high-traffic scenarios. --- -### LeaveChannelAsync +### JoinChannelAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task LeaveChannelAsync(string connectionId, string username, string channelName) +public async Task<(List History, string? Error, bool PasswordRequired)> JoinChannelAsync( + string connectionId, Guid userId, string username, string channelName, string? password = null) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `connectionId` | `string` | — | -| `username` | `string` | — | -| `channelName` | `string` | — | - -**Returns:** `Task` - - -LeaveChannelAsync handles the workflow for when a user leaves a chat channel. It normalizes the channel name to lowercase, updates the presence tracker to reflect that the user has left, broadcasts a user-left notification to all connected clients in that channel, and logs the action at the debug level. - -Developers call this when a user intentionally exits a channel; the method encapsulates the coordinated state change, notification, and observability so callers don't have to orchestrate these steps separately. - -## Remarks - -It centralizes the leave workflow into a single, reusable operation that updates presence, notifies clients, and records the event for debugging. Normalizing the channel name here prevents case-sensitivity inconsistencies when tracking presence or delivering notifications. Notification is performed asynchronously by the broadcasting layer, which preserves responsiveness and allows the caller to await completion. - -## Example - -```csharp -// Most common usage: user "alice" leaves the "General" channel -await chatService.LeaveChannelAsync("conn-123", "alice", "General"); -``` +| `History` | `List` | — | +| `Error` | `string?` | — | +| `PasswordRequired` | `bool` | — | -## Notes -- ToLowerInvariant is called on channelName without a null-check; passing null will throw. Ensure channelName is non-null before calling, or upstream validation. -- The connectionId parameter is unused in this implementation; it may be present for correlation or future use. -- Exceptions from _presenceTracker.LeaveChannel or BroadcastToAllAsync propagate to the caller; no internal retry is performed. +Joins a user to a chat channel by orchestrating normalization, anti-spam checks, membership validation, presence setup, and history retrieval in a single, centralized workflow. When a client requests to join a channel, `JoinChannelAsync` lowercases and trims the channel name, enforces a first-time-join throttle via `_spamGuard`, delegates membership and potential password gating to `_channelService.EnsureChannelMembershipAsync`, registers the user in `_presenceTracker`, broadcasts the join to other clients (excluding invisible users via `UserStatus.Invisible`), and finally returns the channel history via `GetChannelHistoryAsync` along with any error and a `PasswordRequired` flag for future joins. --- -### SendMessageAsync +### LeaveChannelAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task SendMessageAsync(Guid userId, string username, string channelName, string content, string? originConnectionId = null, Guid? replyToMessageId = null) +public async Task LeaveChannelAsync(string connectionId, string username, string channelName) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `userId` | `Guid` | — | +| `connectionId` | `string` | — | | `username` | `string` | — | | `channelName` | `string` | — | -| `content` | `string` | — | -| `originConnectionId` | `string?` | `null` | -| `replyToMessageId` | `Guid?` | `null` | -**Returns:** `Task` +**Returns:** `Task` -SendMessageAsync coordinates the end-to-end process of posting a chat message to a named channel. It normalizes the channel name, validates it against allowed patterns, blocks writes to read-only channels (including the logs room and system channels), decrypts incoming content, strips a literal encryption prefix if present to prevent spoofing, and enforces non-empty content and a maximum length. It then looks up the target channel and the sender from the database, enforces mute state (including automatic unmute when a mute has expired), runs a spam guard that can auto-mute or reject messages, validates an optional reply target, and resolves link embeds before persisting the message. The method returns a user-facing string on error or when action is blocked, and returns null on a successful send; side effects include database updates, saving changes, and a moderation log entry when auto-muting occurs. +Normalizes the channel name to a canonical form using `ToLowerInvariant()` and `Trim()`, then updates presence via `_presenceTracker.LeaveChannel(username, channelName)`, broadcasts a user-left notification to all connected clients through `BroadcastToAllAsync`, and logs a debug entry with the user and channel via `_logger.LogDebug("{User} left channel '{Channel}'", username, channelName)`. ## Remarks -The method centralizes chat message submission, ensuring consistent enforcement of security, moderation, and content rules across all channels. It encapsulates cross-cutting concerns (validation, decryption, sanitization, moderation, and embed resolution) behind a single entry point, reducing duplication and potential inconsistencies in callers. By using a scoped DbContext and explicit read-only checks, it mitigates the risk of unintended writes and keeps transactional boundaries clear. The combination of encryption-aware processing, a programmable spam guard, and read-only channel protection reveals a deliberate design to balance user privacy, abuse prevention, and system integrity. +By encapsulating normalization, presence update, and broadcast in a single method, this symbol provides a consistent, reusable leave operation for the chat service. It ensures that all participants are informed of departures and that the server's presence state stays in sync across callers. The normalization step guarantees that channel identity is consistent, preventing duplicate or missed leaves due to casing. ## Notes -- The method mutates and persists mute state (sender.IsMuted/MutedUntil) in response to spam protection or mute expiry. -- Returning strings for error/status means callers must handle UI messaging; on success it returns null. -- Be aware of early returns for read-only channels and non-existent channels; ensure the consumer handles user feedback. +- Channel identity is normalized to lowercase; avoid relying on mixed-case channel names. +- This method is asynchronous; callers should `await` it to ensure the left-notification is delivered before proceeding. --- -### UpdateStatusAsync +### SanitizeNewlines > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task UpdateStatusAsync(Guid userId, string username, UserStatus status, string? statusMessage) +private static string SanitizeNewlines(string content) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `userId` | `Guid` | — | -| `username` | `string` | — | -| `status` | [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md) | — | -| `statusMessage` | `string?` | — | +| `content` | `string` | — | -**Returns:** `Task` +**Returns:** `string` -Updates a user's presence status for the specified userId and username, performing validation, persisting changes to the database, and broadcasting the new presence to connected clients. When the input is invalid or the user cannot be found, it returns a user-facing error string; on success it returns null. +SanitizeNewlines is a private helper that cleans up a string by normalizing newline endings and trimming excessive blank lines to prevent newline spam in messages. It converts all CRLF/CR endings to LF, collapses runs of whitespace-only lines to at most `HubConstants.MaxConsecutiveNewlines` in a row, and caps the total line count to `HubConstants.MaxMessageNewlines` before returning the result. ## Remarks -The method creates a short-lived DI scope to obtain EchoHubDbContext, updates the user entity (Status, StatusMessage trimmed, and LastSeenAt set to UTC now), and saves changes. It then builds a UserPresenceDto and uses the presence tracker to determine the target channels, broadcasting the updated presence to all relevant clients via BroadcastToAllAsync. The return value encodes success (null) or failure (a user-facing string) without throwing exceptions. +SanitizeNewlines encapsulates formatting hygiene, centralizing newline handling behind a single, configurable policy. By relying on [`HubConstants`](../../EchoHub.Core/Constants/HubConstants.cs.md), the behavior can be tuned without changing call sites, and its private scope keeps the class’s public surface area focused on higher-level responsibilities for chat content processing. ## Notes -- If a value outside the defined UserStatus enum is supplied, the method immediately returns "Invalid status. Use online, away, dnd, or invisible." due to the enum validation check. -- The status message is length-validated against ValidationConstants.MaxStatusMessageLength and is trimmed before storage; overly long messages produce a descriptive error. - +- If `HubConstants.MaxMessageNewlines` is configured to 0 or negative, the method may return an empty string, effectively dropping content. +- The method treats any line consisting only of whitespace as a blank line, so lines that look empty but contain spaces or tabs contribute to the consecutive-blank budget and may be collapsed accordingly. --- -### UserConnectedAsync +### SendMessageAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task UserConnectedAsync(string connectionId, Guid userId, string username) +public async Task SendMessageAsync(Guid userId, string username, string channelName, string content, string? originConnectionId = null, Guid? replyToMessageId = null) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `connectionId` | `string` | — | | `userId` | `Guid` | — | | `username` | `string` | — | +| `channelName` | `string` | — | +| `content` | `string` | — | +| `originConnectionId` | `string?` | `null` | +| `replyToMessageId` | `Guid?` | `null` | -**Returns:** `Task` - - -Upon a client connection, this method coordinates in-memory presence tracking, connection-count telemetry, and optional persistence of the user's online state. It updates the in-memory presence tracker and stats collector with the new connection, then resolves a scoped EchoHubDbContext to locate the user by userId. If the user exists, it updates LastSeenAt to the current UTC time and sets Status to Online, persisting the change via SaveChangesAsync. A debug-level log records the connection event for troubleshooting. The inline comment notes that churn aggregation is performed in the periodic stats report rather than in this hot path. - -## Remarks - -This method glues together presence, persistence, and telemetry for a user connection. It relies on a scoped DbContext to keep database changes isolated per connection, avoiding long-lived contexts and potential contention. By updating both the in-memory trackers and the persisted user state when a user connects, it helps ensure a consistent view of online users across in-memory data and storage, while gracefully handling the case where a user record may be absent. - -## Notes +**Returns:** `Task` -- If the user record cannot be found in EchoHubDbContext.Users, no database write occurs; the method still updates presence and stats. -- The LastSeenAt timestamp uses DateTimeOffset.UtcNow to avoid timezone inconsistencies across servers. +Sends a message from a user to a channel by validating the channel name, enforcing read-only constraints, decrypting and sanitizing the content, and applying user-state checks and anti-spam rules before proceeding with the submission pipeline. If any validation fails, the method returns a descriptive error string (for example, "Invalid channel name." or "Channel '{channelName}' does not exist."). The operation normalizes the channel name with the regex from `ValidationConstants.ChannelNameRegex()` and enforces the maximum length via `HubConstants.MaxMessageLength`. It rejects writes to log/system channels (`_serverLogs.IsLogsChannel(...)`) and decrypts the incoming content with `_encryption.Decrypt`, removing any literal `'$ENC$'` prefix before validation. After sanitizing newlines, it opens a DI scope to obtain an [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), resolves the target [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) (ensuring it exists and is not system-only), and loads the [`User`](../../EchoHub.Core/Models/User.cs.md) to check mute status (including auto-unmuting if the mute has expired). A spam guard (`_spamGuard.CheckMessage`) may auto-mute or reject the message depending on the verdict (`SpamVerdictKind.AutoMute` or `SpamVerdictKind.Rejected`). If a `replyToMessageId` is provided, the method validates the target message exists within the same channel. It also attempts to fetch URL embeds in a guarded block via `_embedServic` to enrich the message without destabilizing the submission flow. All persistence and side-effects occur within the scoped context, and the method returns a user-facing string on fail or proceeds with the normal submission path on success. The orchestration relies on several collaborators, including [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), [`EmbedDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md), [`ValidationConstants`](../../EchoHub.Core/Constants/ValidationConstants.cs.md), and [`HubConstants`](../../EchoHub.Core/Constants/HubConstants.cs.md), to enforce channel hygiene, user state, and content enrichment. --- -### UserDisconnectedAsync +### UpdateStatusAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task UserDisconnectedAsync(string connectionId) +public async Task UpdateStatusAsync(Guid userId, string username, UserStatus status, string? statusMessage) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `connectionId` | `string` | — | +| `userId` | `Guid` | — | +| `username` | `string` | — | +| `status` | [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md) | — | +| `statusMessage` | `string?` | — | **Returns:** `Task` -Handles the disconnection lifecycle for a user in the chat service. Given a connectionId, it resolves the associated username, collects the channels the user was in, updates presence statistics, and if the user is no longer online, persists LastSeenAt and marks the user as Invisible in the database. It also constructs a UserPresenceDto and broadcasts a status-change to the user's previously tracked channels. The method returns the username that disconnected (or null if no user could be resolved from the connectionId). - -## Remarks - -By isolating persistence and presence updates behind a scoped database context, this method coordinates ephemeral connection state with durable user data. It serves as the boundary between connection lifecycle management and user presence broadcasting, ensuring that changes are persisted and that clients are notified consistently. The pattern of resolving the user from the connection, updating LastSeenAt and Visibility, and broadcasting a UserPresenceDto helps keep the client UIs in sync with accurate user status. - -## Notes - -- If the connectionId cannot be mapped to a username, the method still records the disconnection count via the stats collector, but skips the database update and user-broadcast. -- LastSeenAt is updated to DateTimeOffset.UtcNow and Status is set to Invisible only when a valid username is found and the user is no longer online. -- A scoped EchoHubDbContext is used to persist changes; the DbContext instance is disposed as part of the scope lifecycle. The broadcast is sent to the channels the user was connected to before disconnect; if there were no such channels, there is no targeted broadcast. - ---- - -## BuildLogBacklog -> **File:** `src/EchoHub.Server/Services/ChatService.cs` -> **Kind:** method - -```csharp -private List BuildLogBacklog(string channelName) -``` - -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `channelName` | `string` | — | - -**Returns:** `List` +Updates a user\`s `Status` and optional `StatusMessage`, persists the change to the database, and broadcasts the new presence to all subscribed channels. It validates that the `status` is a defined enum value (guarding against undefined bindings via `Enum.IsDefined`), and enforces the maximum length for `statusMessage` using `ValidationConstants.MaxStatusMessageLength`; on failure it returns a string error, otherwise it returns `null` after a successful update. - -Turns the log-file backlog into transport-encrypted MessageDto objects so clients render past log lines exactly like streamed ones. It reads the backlog via the server logs store, encrypts each backlog entry’s content with the encryption service, and wraps it in a new MessageDto using a freshly generated GUID, the SenderName from ServerLogsService, the provided channelName, and the backlog entry’s timestamp. This method never touches the database and serves solely as a transform to replay historical log lines in the same MessageDto format as live messages. +It uses a scoped DI container to resolve [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), loads the user by `userId`, updates `Status`, `StatusMessage` (trimmed), and `LastSeenAt` to `DateTimeOffset.UtcNow`, saves changes, builds a [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) for broadcasting, determines the channels with `_presenceTracker.GetChannelsForUser(username)`, and notifies clients via `BroadcastToAllAsync` calling `SendUserStatusChangedAsync` with the presence payload. ## Remarks -Acts as a translator between persisted log backlog and the live message stream. By centralizing encryption and formatting, it ensures backlog replay matches live streams and isolates storage concerns from presentation. The use of a new GUID per backlog item also avoids depending on database identifiers for UI rendering. +By performing the work inside a scoped container, the method keeps the Entity Framework context life cycle local to the operation, avoiding leaks across requests. It constructs a [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) containing the user's identity and presence details, which is then broadcast to all relevant channels via `BroadcastToAllAsync` and `SendUserStatusChangedAsync`. It also records whether the user is IRC-only using `_presenceTracker.IsIrcOnly(user.Username)` as part of the presence payload, ensuring clients receive a faithful representation of user state. ## Notes -- The MessageDto payload sent for backlog entries is encrypted; clients must decrypt to display the original content. -- IDs for backlog items are generated per call (Guid.NewGuid) and are not tied to persisted database IDs. +- The `statusMessage` is trimmed before persistence; a null value yields a null field in storage. + --- -## JoinChannelAsync +### UserConnectedAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -public async Task<(List History, string? Error, bool PasswordRequired)> JoinChannelAsync( - string connectionId, Guid userId, string username, string channelName, string? [REDACTED:CONNECTION_STRING_PASSWORD] +public async Task UserConnectedAsync(string connectionId, Guid userId, string username) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `History` | `List` | — | -| `Error` | `string?` | — | -| `PasswordRequired` | `bool` | — | - - -Joins a user to a chat channel, performing gating, membership validation, presence tracking, and history retrieval. It first enforces a join throttle via a spam guard, then delegates the channel membership check (including any password gate) to ChannelService. If the gate fails, it returns an empty history with the reason. On a successful gate, it records the join in the presence tracker, fetches a lightweight presence snapshot for broadcasting, broadcasts the join to all connected clients (except when the user is invisible), and finally returns the channel history along with an indication that no error occurred and that no password is required. +| `connectionId` | `string` | — | +| `userId` | `Guid` | — | +| `username` | `string` | — | -## Remarks -This method encapsulates the end-to-end join workflow in a single, reusable operation, ensuring consistent enforcement of anti-spam, permission, and presence semantics across the chat surface. By obtaining presence information in a scoped, guarded manner, it keeps side effects localized to the join flow while enabling clients to incrementally update their views. The implementation gracefully handles failures when fetching presence data (logging at debug level) without interrupting the primary join path, and it respects user visibility by avoiding broadcasts for invisible users. +**Returns:** `Task` -## Example -```csharp -// Example usage: a user joining a publicly accessible channel without a password -var (history, error, passwordRequired) = await chatService.JoinChannelAsync( - connectionId: "conn-123", - userId: userId, - username: "Alice", - channelName: "general", - password: null); -``` -## Notes -- If the spam guard rejects the join, the method returns immediately with an empty history and a non-null error reason; no membership or presence side effects occur. -- Invisible users will not trigger a broadcast of the join to other clients, though their history is still returned to them. -- Presence data is a best-effort fetch; failures are logged at debug level and do not prevent the join from completing or the history from being returned. +UserConnectedAsync handles a user establishing a real-time connection by recording the connection with the in-memory presence tracker (`_presenceTracker`), updating the live online user count via the stats collector (`_statsCollector`), and, within a short-lived scope, persisting the user's state in the database ([`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md)). If a [`User`](../../EchoHub.Core/Models/User.cs.md) exists for the provided `userId`, it updates `LastSeenAt` to `DateTimeOffset.UtcNow` and sets `Status` to `UserStatus.Online`, then saves changes with `SaveChangesAsync`. Finally, it emits a debug log with `username` and `connectionId` via `_logger.LogDebug`. --- -## SanitizeNewlines +### UserDisconnectedAsync > **File:** `src/EchoHub.Server/Services/ChatService.cs` > **Kind:** method ```csharp -private static string SanitizeNewlines(string content) +public async Task UserDisconnectedAsync(string connectionId) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `content` | `string` | — | - -**Returns:** `string` - - -SanitizeNewlines normalizes line endings to a single newline form, collapses consecutive blank lines to at most HubConstants.MaxConsecutiveNewlines, and truncates the total line count to HubConstants.MaxMessageNewlines. This private helper should be invoked when preparing user-provided content for transmission so that messages stay readable and within size limits, rather than letting users push uncontrolled newline spam through the chat pipeline. - -## Remarks - -SanitizeNewlines centralizes newline handling to ensure consistent formatting across the chat pipeline. It is driven by HubConstants thresholds, avoiding hard-coded limits and enabling consistent behavior wherever message sanitization occurs. As a pure transformation of the input with no external state, it has no side effects beyond returning a sanitized string. +| `connectionId` | `string` | — | -## Notes +**Returns:** `Task` -- Collapses consecutive whitespace-only lines, which can alter intended spacing in user messages. -- Truncates lines beyond MaxMessageNewlines, so content beyond the limit is dropped from the end. -- The function is private and intended for internal use within the ChatService; external callers cannot rely on it. +Handles a user disconnection by resolving the provided `connectionId` to a `username` via `_presenceTracker`, capturing the channels the user was in, and then marking the user as disconnected. If a `username` exists and the user is no longer online, it creates a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), updates the corresponding [`User`](../../EchoHub.Core/Models/User.cs.md)'s `LastSeenAt` to `DateTimeOffset.UtcNow` and `Status` to `UserStatus.Invisible`, saves changes, constructs a [`UserPresenceDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) with the updated presence, and broadcasts the status change to the previously observed channels via `BroadcastToAllAsync` with `SendUserStatusChangedAsync`. Finally, it logs the disconnect with `_logger` and returns the `username` (which may be null). --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md index 8b4cf14..d1c54f8 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md @@ -18,40 +18,17 @@ public sealed class DirectoryClaimStore ``` -Persists the directory claim token (an opaque secret issued on first registration) together with the server's stable ServerId, and exposes a short-lived RegistrationStatus used by operator-facing endpoints. Use this type when you need a simple on-disk, atomic store for the initial claim token and ServerId and also want to surface the most recent registration outcome (success or failure) for diagnostics or UI. +Persists an opaque directory claim token and the row's stable `ServerId` to disk and exposes that data plus an ephemeral `RegistrationStatus` for operator-facing endpoints. Reach for `DirectoryClaimStore` when the process needs to remember the one-time claim token issued at first registration and to report current registration status; it handles atomic on-disk writes and concurrent access within the process so callers can read `ClaimToken`, `ServerId`, and `Status` without taking locks. ## Remarks -This class centralises two responsibilities: durable storage of the claim token + ServerId and an in-memory, ephemeral view of registration status. The file write uses an atomic temporary-write-then-rename strategy (so partial writes are avoided) and callers should treat the stored contents as a secret. Concurrency is handled with a SemaphoreSlim for writes and Volatile reads/writes for the in-memory references: SaveClaimAsync and UpdateServerIdAsync serialize on-disk updates while ClaimToken, ServerId and Status are safe to read without taking the write lock. - -## Example -```csharp -// resolve IConfiguration and ILogger from your DI container -var store = new DirectoryClaimStore(configuration, logger); - -// Persist the one-time claim token and server id (called once when first claimed) -await store.SaveClaimAsync(claimToken, serverId); - -// Read back the persisted values later -var token = store.ClaimToken; // may be null until saved -var id = store.ServerId; // may be null until saved - -// Update only the ServerId when re-registering with the same token -await store.UpdateServerIdAsync(newServerId); - -// Report ephemeral registration outcomes for operator UI -store.SetSuccess(serverId); -// or on failure: -store.SetFailure("ConflictError", new[] { "host-a", "host-b" }); - -// Inspect the last registration status -var status = store.Status; -``` +`DirectoryClaimStore` separates durable state (the `PersistedClaim` containing `ClaimToken` and `ServerId`) from ephemeral state (`RegistrationStatus`). Durable state is loaded once in the constructor (via configuration-resolved `FilePath`) and updated by `SaveClaimAsync` and `UpdateServerIdAsync` using an atomic write strategy (tmp file + rename). Ephemeral `Status` is updated in-memory by `SetSuccess` and `SetFailure` for operator/UI endpoints and is intentionally not written to disk. Thread-safety is achieved by using `Volatile.Read`/`Volatile.Write` for lock-free readers and a private `SemaphoreSlim` (`_writeLock`) to serialize writers; writers also perform the atomic file swap. ## Notes -- The on-disk file is treated as a secret; protect filesystem permissions and backups accordingly. -- Status is ephemeral and kept only in memory; SetSuccess/SetFailure do not persist to disk. -- SaveClaimAsync is intended to be called once per row's lifetime (first claim). UpdateServerIdAsync is a no-op when the ServerId is unchanged. -- ClaimToken and ServerId properties may be null until a persisted value is loaded or saved. +- The on-disk file is treated as a secret; callers and operators should protect the `FilePath` and its contents (it contains the `ClaimToken`). +- `SaveClaimAsync` is intended to be called only once per row's lifetime (on first claim). `UpdateServerIdAsync` is used when re-registering with an existing token and is a no-op when the `ServerId` is unchanged. +- `SetSuccess` / `SetFailure` mutate only the in-memory `Status` and do not persist anything; process restarts will lose these ephemeral fields (durable `PersistedClaim` is preserved). +- Writes use an atomic tmp+rename strategy to avoid partial files, but this class does not coordinate cross-process access beyond the atomic replace; if multiple processes may write the same file concurrently, external synchronization is required to avoid races. +- I/O errors from loading or writing the backing file (e.g. permissions, disk full) will surface to callers of the write methods or during construction; callers should handle or surface those exceptions as appropriate. --- @@ -79,12 +56,13 @@ public sealed record RegistrationStatus( | `ConflictingHosts` | `string[]?` | — | -RegistrationStatus is a small, immutable data container that captures the outcome of attempting to register a directory claim in the EchoHub server. It indicates whether the registration succeeded and optionally conveys the server identity, timestamp, error details, and any conflicting hosts so higher-level logic can react accordingly. +Represents the outcome of attempting to register a server with the directory claim store. This `record` is an immutable value type that carries the essential pieces of registration state: whether the entity is registered (`IsRegistered`), the assigned `ServerId` if one exists, the time of the last registration attempt (`LastRegisteredAt`, a `DateTimeOffset?`), an optional `LastError` describing the failure, and any `ConflictingHosts` that prevented registration. Consumers typically construct or propagate this value from the registration workflow and use it to inform callers, UI logic, or logging code rather than broadcasting multiple primitive values. ## Remarks -RegistrationStatus models a single, transportable result from a registration process. As a record, it benefits from value-based equality, making it easy to compare results across layers or to cache and reuse them. The nullable fields reflect real-world outcomes: a registration attempt may not yield a ServerId or LastRegisteredAt, and LastError plus ConflictingHosts carry additional context when registration fails or is disputed. This abstraction isolates the surface area of registration outcomes from the rest of the directory claim store, enabling consistent handling without sprinkling primitive flags throughout the codebase. +As a `sealed` `record`, `RegistrationStatus` provides value-based equality and immutability, making it a safe, portable summary of a registration outcome across components. The nullable members reflect that some details may be unavailable depending on the failure mode (for example, no `ServerId` if registration hasn't completed). The `ConflictingHosts` array communicates all hosts involved in a conflict, enabling callers to present a remediation path. ## Notes -- Nullable fields indicate optional context; always guard before accessing ServerId, LastRegisteredAt, LastError, and ConflictingHosts to avoid NullReferenceException. +- The `string[]?` `ConflictingHosts` is an array, which is mutable. If you publish this instance or cache its value, clone the array to prevent external mutation from changing the documented status. +- Nullability semantics: `ServerId`, `LastRegisteredAt`, `LastError`, and `ConflictingHosts` being `null` means the data is not available in the current outcome; interpret accordingly and avoid conflating a genuine value with absence. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/FileCleanupService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/FileCleanupService.cs.md index 5bf09fb..85530c7 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/FileCleanupService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/FileCleanupService.cs.md @@ -8,11 +8,12 @@ public sealed class FileCleanupService : BackgroundService ``` -FileCleanupService is a hosted background service that periodically deletes files in a configured storage directory that are older than a configured retention period. It reads settings from configuration, chooses the target path, and logs progress while running until cancellation. +FileCleanupService is a hosted background worker that periodically deletes files older than a configured retention window from a storage directory determined by configuration. It reads its interval and retention settings from configuration, selects a path (configured path or a sensible default under the application base directory), and logs its activity while reliably continuing after errors. ## Remarks -The cleanup logic is encapsulated in a dedicated BackgroundService to centralize disk-space hygiene and keep it decoupled from request-driven code. It relies on dependency-injected IConfiguration and ILogger to determine interval, retention, and storage path, and to report status and errors. Cleanup runs in a cancellation-friendly loop and uses UTC timestamps to compare age, making behavior predictable across servers. +FileCleanupService encapsulates cleanup policy behind a dedicated background service, so cleanup logic is not sprinkled across the app. It uses dependency-injected `IConfiguration` and `ILogger` to stay configurable and observable, and it handles exceptions without bringing down the service. The cleanup operation is intentionally conservative: files are deleted only if their creation time UTC is older than the computed cutoff, and per-file errors are logged and do not stop processing of the rest. ## Notes -- It only considers files directly within storagePath; subdirectories are not scanned. If you need recursive cleanup, switch to Directory.GetFiles(storagePath, "*", SearchOption.AllDirectories) and adjust the cutoff logic accordingly. -- File age is determined by GetCreationTimeUtc; if your deployment uses different semantics (e.g., files moved or uploaded), consider using GetLastWriteTimeUtc or metadata-based age checks. \ No newline at end of file +- If the storage path does not exist or is not configured, the cleanup is skipped gracefully. +- Defaults are applied when configuration values are missing or invalid: `Storage:CleanupIntervalHours` defaults to 1, `Storage:RetentionDays` defaults to 30. +- If files are in use or cannot be deleted due to permissions, the service logs a warning and continues with the remaining files. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/FileStorageService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/FileStorageService.cs.md index 93f32b9..c69d0a4 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/FileStorageService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/FileStorageService.cs.md @@ -8,14 +8,12 @@ public class FileStorageService ``` -FileStorageService persists uploaded data to a local disk storage location, creating the directory if it does not exist and selecting the path from configuration (Storage:Path) or defaulting to an uploads folder beside the application. Each saved file is assigned a GUID-based fileId and stored with its original extension; the API supports SaveFileAsync, GetFilePath, GetStoredFileIds, and DeleteFile for common lifecycle operations. +FileStorageService is a lightweight on-disk storage helper for uploaded files. It derives its storage location from configuration under `Storage:Path` (defaulting to an `uploads` directory next to the application's base directory), ensures the directory exists, and provides basic operations to save, locate, enumerate, and delete files. ## Remarks - -By centralizing disk interactions, this service hides filesystem details from callers and provides a single, testable abstraction for storing attachments. It guarantees the storage directory exists and maps between a stable fileId and the corresponding on-disk file (preserving the extension). The GetStoredFileIds method performs a single directory scan to facilitate bulk checks across many files without per-file I/O. +FileStorageService centers on a GUID-based identity for each stored file and writes files to a single storage directory as `"{fileId}{extension}"`. Retrieval by id uses a wildcard extension, so callers do not need to know the original file name or extension at lookup time. The `GetStoredFileIds` method performs one directory scan to produce the set of ids (filenames without extensions), enabling bulk checks of attachments without issuing a separate filesystem glob per id. The design hides actual file names from callers while preserving the original extension on disk to help downstream consumers infer content type. The constructor's path resolution and directory creation ensure a usable store is available up front, reducing boilerplate for calling code. ## Notes - -- The storage path is captured at construction time; changes to configuration after construction won't affect this instance. -- No validation of the incoming stream’s content type or size is performed here; enforce validation at call sites if needed. -- DeleteFile uses GetFilePath to locate the file before deleting and becomes a no-op if the file does not exist. \ No newline at end of file +- Initialization may throw if the configured storage path is invalid or cannot be created due to permissions. +- `DeleteFile` is safe to call for non-existent files; it simply becomes a no-op. +- `GetFilePath` relies on a pattern `"{fileId}.*"`; if multiple matches exist (e.g., due to external tampering), the first match is returned, which should be rare given the GUID-based ids. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md index f8b3125..af8e3fa 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md @@ -8,86 +8,13 @@ ```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"] -Extract["ExtractUrls(content) -> urls"] -CheckUrls["urls.Count == 0?"] -ReturnNullNoUrls["Return null (no URLs found)"] -InitEmbeds["Create empty List#60;EmbedDto#62; embeds"] -ForEach["For each url in urls"] -CallFetch["Call FetchEmbedForUrlAsync(url)"] -ReturnNullFromFetch["Returned null -> continue"] -AddEmbed["Add EmbedDto to embeds"] -CatchLog["Catch Exception -> LogDebug and continue"] -AfterLoop["All URLs processed"] -ReturnDecision["embeds.Count > 0?"] -ReturnEmbeds["Return embeds"] -ReturnNullAll["Return null (no successful embeds)"] +LinkEmbedService["TryGetEmbedsAsync: ExtractUrls content; if no URLs -> return null. For each URL: call FetchEmbedForUrlAsync -> validate absolute URI, allow http or https, skip private hosts; create CancellationTokenSource using HubConstants, send GET with HttpClient 'OgFetch' and HttpCompletionOption.ResponseHeadersRead; if non-success status -> skip; ensure Content-Type starts with text/html; read limited HTML; parse OG tags; determine title with og:title fallback to ; if no title -> skip; else build EmbedDto and add to results. Catch exceptions and LogDebug. Return embeds list or null"] +HubConstants["HubConstants: EmbedFetchTimeoutSeconds, EmbedMaxHtmlBytes, EmbedMaxDescription"] +EmbedDto["EmbedDto: represents successful OG embed data"] -subgraph FetchEmbedForUrlAsync - F1["Try Uri.TryCreate(url, Absolute)"] - F1_no["Return null (invalid uri)"] - F2["Check scheme is http or https"] - F2_no["Return null (unsupported scheme)"] - F3["IsPrivateHost(uri)?"] - F3_no["Return null (private host)"] - F4["Create CTS with HubConstants.EmbedFetchTimeoutSeconds"] - F5["Create HTTP client 'OgFetch'"] - F6["Send GET request, get response"] - F7["response.IsSuccessStatusCode?"] - F7_no["Return null (unsuccessful status)"] - F8["Content-Type starts with #quot;text/html#quot;?"] - F8_no["Return null (non-html content)"] - F9["Read limited HTML (HubConstants.EmbedMaxHtmlBytes)"] - F9_empty["Return null (empty or whitespace html)"] - F10["Parse OG tags, get title or fall back to #60;title#62;"] - F10_no["Return null (no title)"] - F11["Build EmbedDto and return"] -end - -Start --> Extract -Extract --> CheckUrls -CheckUrls -->|"yes"| ReturnNullNoUrls -CheckUrls -->|"no"| InitEmbeds -InitEmbeds --> ForEach -ForEach --> CallFetch -CallFetch -->|"throws"| CatchLog -CallFetch -->|"null"| ReturnNullFromFetch -CallFetch -->|"EmbedDto"| AddEmbed -ReturnNullFromFetch --> ForEach -AddEmbed --> ForEach -CatchLog --> ForEach -ForEach -->|"done"| AfterLoop -AfterLoop --> ReturnDecision -ReturnDecision -->|"yes"| ReturnEmbeds -ReturnDecision -->|"no"| ReturnNullAll - -CallFetch --> F1 -F1 -->|"no"| F1_no -F1 -->|"yes"| F2 -F2 -->|"no"| F2_no -F2 -->|"yes"| F3 -F3 -->|"true"| F3_no -F3 -->|"false"| F4 -F4 --> F5 -F5 --> F6 -F6 --> F7 -F7 -->|"no"| F7_no -F7 -->|"yes"| F8 -F8 -->|"no"| F8_no -F8 -->|"yes"| F9 -F9 -->|"empty"| F9_empty -F9 -->|"has html"| F10 -F10 -->|"no"| F10_no -F10 -->|"yes"| F11 - -F1_no --> ReturnNullFromFetch -F2_no --> ReturnNullFromFetch -F3_no --> ReturnNullFromFetch -F7_no --> ReturnNullFromFetch -F8_no --> ReturnNullFromFetch -F9_empty --> ReturnNullFromFetch -F10_no --> ReturnNullFromFetch -F11 --> AddEmbed +LinkEmbedService -->|"reads timeouts and limits"| HubConstants +LinkEmbedService -->|"creates and adds successful EmbedDto"| EmbedDto +LinkEmbedService -->|"foreach URL (loop)"| LinkEmbedService ``` ```csharp @@ -95,30 +22,13 @@ public partial class LinkEmbedService ``` -Scans a piece of message text for URLs and attempts to produce lightweight link preview data (EmbedDto) by fetching and parsing Open Graph and common HTML metadata. Use TryGetEmbedsAsync when you need server-side link previews for chat messages and want a defensive, timeout- and size-limited fetch that never throws (it logs failures and returns null when no usable embeds are found). +Detects and fetches Open Graph-style embed metadata for any URLs found in a piece of message `content`. Use `LinkEmbedService` (via its `TryGetEmbedsAsync` method) when you want a best-effort, non-throwing attempt to produce [`EmbedDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) objects for links inside user messages — for example, to show link previews — and you want network, size and privacy protections applied automatically. ## Remarks -LinkEmbedService centralizes the logic for discovering URLs in a message and converting remote HTML metadata into EmbedDto instances suitable for display. It is intentionally defensive: only absolute http/https URLs are considered, private hosts are skipped, fetches are limited by a cancellation timeout and a maximum HTML byte count (HubConstants), and only text/html responses are parsed. Errors during individual fetches are caught and logged at debug level so the caller observes either a list of successful embeds or null (no useful embeds). - -## Example -```csharp -// Given an instance of LinkEmbedService (typically from DI): -var embeds = await linkEmbedService.TryGetEmbedsAsync(messageContent); -if (embeds is null) -{ - // No embeds found or all fetch attempts failed. -} -else -{ - Console.WriteLine($"Found {embeds.Count} embeds"); - foreach (var embed in embeds) - { - // render embed in UI or pass to presentation layer - } -} -``` +`LinkEmbedService` centralizes link-preview logic so callers do not have to implement URL extraction, host-safety checks, HTTP fetching, HTML-size limits, or Open Graph parsing themselves. The public `TryGetEmbedsAsync` method returns `null` when no useful embed data is available (either because no URLs were found or all fetch attempts failed) and never throws; individual fetch failures are caught and logged at debug level. Internally it calls the private `FetchEmbedForUrlAsync` for each URL which enforces absolute `http`/`https` URIs, rejects private hosts via `IsPrivateHost`, uses an `IHttpClientFactory`-created client named `"OgFetch"`, applies a `CancellationTokenSource` timeout (`HubConstants.EmbedFetchTimeoutSeconds`), requires a `text/html` response, bounds the HTML read size (`HubConstants.EmbedMaxHtmlBytes`), extracts Open Graph tags (falling back to the `<title>` tag), decodes HTML entities with `WebUtility.HtmlDecode`, and truncates long descriptions to `HubConstants.EmbedMaxDescriptionLength`. ## Notes -- TryGetEmbedsAsync returns null when no URLs are present or when all fetches fail; it does not return an empty list in those cases—check for null before iterating. -- The service expects an IHttpClientFactory and creates a client with the name "OgFetch"; ensure your HttpClient configuration (handlers, DNS/timeout policies) is appropriate for remote HTML fetches. -- HTML metadata extraction is heuristic: it uses Open Graph tags, falls back to a <title> regex, reads only the first N bytes of HTML, and truncates long descriptions per HubConstants. Consumers should treat returned fields as untrusted display content and apply any necessary sanitization in the UI layer. \ No newline at end of file +- The service expects an `IHttpClientFactory` client named `"OgFetch"` to be configured; network policy (proxies, handlers) should be applied on that named client rather than relying on this class to set HTTP options. +- Fetching is constrained by time and size: a cancellation timeout (`HubConstants.EmbedFetchTimeoutSeconds`) and a maximum number of HTML bytes (`HubConstants.EmbedMaxHtmlBytes`) are enforced; pages that exceed these limits may yield no embed. +- Only absolute `http`/`https` URLs are considered and private/internal hosts are explicitly ignored by `IsPrivateHost`; the method will return `null` instead of an [`EmbedDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) for such URLs. +- Failures during individual URL fetches are swallowed (logged at debug) so `TryGetEmbedsAsync` remains non-throwing for callers — check logs when embeds are unexpectedly missing. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/MessageEncryptionService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/MessageEncryptionService.cs.md index 04f392e..f8cc0ec 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/MessageEncryptionService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/MessageEncryptionService.cs.md @@ -8,12 +8,12 @@ public class MessageEncryptionService : IMessageEncryptionService ``` -MessageEncryptionService provides AES-GCM-based encryption and decryption for strings using a 256-bit key loaded from configuration, returning ciphertexts in a standardized prefixed, base64-encoded format. Callers use it when they need authenticated encryption with a consistent storage format and null-safety helpers. +MessageEncryptionService is a server-side component that encrypts and decrypts text using AES-GCM with a 256-bit key sourced from configuration. It implements [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) and exposes `Encrypt`, `Decrypt`, `EncryptNullable`, and `DecryptNullable`. Use it when you need to store or transmit sensitive strings (for example, in a database) without exposing plaintext. Each encrypted value is prefixed with the configured `CiphertextPrefix` and serialized as a base64-encoded nonce followed by a base64-encoded payload containing the ciphertext and authentication tag, enabling safe storage and later decryption with the same key. If a value supplied to `Decrypt` does not begin with the encryption prefix, the service treats it as legacy plaintext and returns it unchanged. When decryption fails for any reason, the service logs the issue and returns the placeholder string `[encrypted message — decryption failed]` to avoid leaking cryptographic details. ## Remarks -By centralizing the encryption logic, this class ensures all encrypted messages share the same nonce handling, tag size, and output format, which simplifies storage and auditing across clients and servers. It also enforces key validation upfront and uses dependency-injected logging to surface decryption problems and protect the caller from exceptions. The EncryptNullable/DecryptNullable helpers make it convenient to encode optional values without duplicating boilerplate. +MessageEncryptionService centralizes cryptographic logic to isolate security concerns from business code. It provides a single, testable path for encryption and decryption and ensures consistent storage format for encrypted data, which simplifies auditing and data integrity checks. The class reads a 256-bit key at startup from `Encryption:Key` (as Base64) and validates its length, making key management explicit and failure-revealing at boot time; the `EncryptDatabaseEnabled` flag controls whether database encryption should be active, enabling or disabling encryption behavior without code changes. ## Notes -- Key retrieval and validation: the constructor reads Encryption:Key from configuration as a Base64 string and requires exactly 32 bytes; otherwise it throws InvalidOperationException. -- Decryption safety and error handling: if content doesn't start with the CiphertextPrefix, it is treated as legacy plaintext; malformed payloads log a warning and yield "[encrypted message — decryption failed]"; any exception results in a logged error and the same sentinel output. -- Null handling convenience: EncryptNullable and DecryptNullable gracefully handle null inputs without throwing. \ No newline at end of file +- Do not rotate the encryption key at runtime; the key is loaded once during construction and would render previously encrypted data unreadable. +- The class is thread-safe for concurrent use since it creates a new `AesGcm` instance per operation and does not share mutable state. +- Non-prefixed content is treated as legacy plaintext, ensuring backward compatibility with data that predates server-side encryption. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/MuteExpirationService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/MuteExpirationService.cs.md index 19984b0..d606786 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/MuteExpirationService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/MuteExpirationService.cs.md @@ -8,14 +8,11 @@ public sealed class MuteExpirationService : BackgroundService ``` -Automatically unmutes users when their timed mute period has expired. - -This is a background hosted service that periodically scans for users who are currently muted and whose MutedUntil timestamp has passed, then clears the mute state and logs the action. It uses a scoped DbContext instance per iteration (via IServiceScopeFactory) to perform a safe, isolated database update, and it runs on a fixed cadence (15 seconds) until the host is stopped. The service catches non-cancellation exceptions to avoid leaking the loop and continues monitoring uninterrupted. +Periodic background task that checks for users with an active timed mute and lifts the mute once the expiration time has passed. Implemented as a `BackgroundService`, it creates a short-lived scope to obtain an [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), queries `Users` for those where `IsMuted` is true and `MutedUntil` has a value that is in the past, clears `IsMuted` and `MutedUntil`, and saves the changes. It logs each auto-unmute and continues running until the host is canceled; the check runs every 15 seconds to balance timely unmute with database load. ## Remarks -The MuteExpirationService centralizes the expiry-based state transition for user mutes, decoupling this concern from user actions or other services. By resolving EchoHubDbContext within a scope for each cycle, it ensures proper disposal of the context and its resources while keeping the background loop lightweight. This pattern keeps mute state consistent across the system and reduces the chance of missed expirations if a user’s timed mute expires while the application is running. +This symbol centralizes the timed-mute expiration lifecycle, decoupling unmute logic from controllers or scheduled jobs. It ensures mutes expire even if no user action occurs and uses a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) to avoid long-lived contexts and to work with fresh data on each cycle. Updates are batched per cycle, with a per-user log entry (e.g., "Auto-unmuted user ... (timed mute expired)") to aid observability and troubleshooting. ## Notes -- The query materializes expired mutes with ToListAsync before processing; for environments with a very large backlog of expirations, consider batching to reduce memory usage. -- The cadence (CheckInterval) is 15 seconds; adjust if you need tighter or looser alignment with mute expiration semantics. -- Time comparisons use UTC (DateTimeOffset.UtcNow) to avoid timezone-related drift; ensure MutedUntil is stored as a UTC timestamp to preserve correctness. \ No newline at end of file +- The polling interval is fixed by `CheckInterval` (15 seconds); lowering or raising this value trades immediacy against database load. Adjust with awareness of your project’s performance characteristics. +- Only mutes with a non-null `MutedUntil` are expired by this service. If a user’s `MutedUntil` is null, that mute will not be auto-expanded by this path and will require manual intervention or a different expiration rule. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/PresenceTracker.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/PresenceTracker.cs.md index 28ed761..74ba66f 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/PresenceTracker.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/PresenceTracker.cs.md @@ -8,32 +8,33 @@ public class PresenceTracker ``` -Tracks active connections and per-user channel membership for a server-side hub/service. Use this when you need to know which usernames are currently connected (counting a user with multiple connections only once), map individual connection IDs to their user, and query which users are in which chat channels. +Maintains an in-memory registry of active connections, per-user connection sets, and per-user channel memberships. Use `PresenceTracker` when a hub or real-time service needs a centralized, process-local view of who is online (distinct users are counted once even if they have multiple connections), which channels each user has joined, and to obtain connection IDs for broadcasting to members of channels. ## Remarks -PresenceTracker centralizes presence state in three concurrent dictionaries: a connectionId → (userId, username) map, a username → set-of-connectionIds map, and a username → set-of-channelNames map. It uses a single private lock to make operations on the HashSet values atomic because ConcurrentDictionary only protects access to individual slots, not the mutable collections stored as values. The UserCountChanged event is raised only when the distinct online user count changes (for example, when a user's first connection is added or their last connection is removed); the implementation invokes the event outside the lock to avoid holding the lock while user code runs. +`PresenceTracker` centralises presence state so hubs or services can avoid scattering connection and channel bookkeeping across call sites. It stores a mapping of connection id → `(userId, username)` in `_connections`, a mapping of `username` → connection id set in `_userConnections`, and a mapping of `username` → channel name set in `_userChannels`. The class deduplicates users with multiple connections (a user with N connections is one online user) and raises `UserCountChanged` only when the distinct online user count changes. Internally it uses a private lock (`_lock`) around operations that mutate the `HashSet` values because `ConcurrentDictionary` protects its buckets but not the mutability of objects stored inside them; this ensures consistency for the `TryGetValue` → modify sequences and for multi-step cleanup when the last connection for a user is removed. ## Example ```csharp var tracker = new PresenceTracker(); tracker.UserCountChanged += count => Console.WriteLine($"Online users: {count}"); -// user connects from two clients (only the first connection should trigger the count change) -tracker.UserConnected("conn-1", Guid.NewGuid(), "alice"); // triggers UserCountChanged -> 1 -tracker.UserConnected("conn-2", Guid.NewGuid(), "alice"); // no count change +// A client connects with two transports (two connection IDs) for the same logical user +var aliceId = Guid.NewGuid(); +tracker.UserConnected("conn-1", aliceId, "alice"); +tracker.UserConnected("conn-2", aliceId, "alice"); -// join a channel and query who is in it -tracker.JoinChannel("alice", "general"); -var usersInGeneral = tracker.GetOnlineUsersInChannel("general"); // contains "alice" +// Join a channel; returns true only if this `username` was not already in the channel +var firstJoin = tracker.JoinChannel("alice", "general"); -// disconnect one connection; user still online because another connection remains -tracker.UserDisconnected("conn-1"); // returns "alice"; no UserCountChanged +// Read who is in a channel (snapshot list) +var usersInGeneral = tracker.GetOnlineUsersInChannel("general"); -// final disconnect removes the user and triggers UserCountChanged -tracker.UserDisconnected("conn-2"); // returns "alice"; triggers UserCountChanged -> 0 +// When a connection goes away +tracker.UserDisconnected("conn-1"); +// When the last connection is removed, `UserCountChanged` will fire and channel membership for that user is cleaned up ``` ## Notes -- The class treats usernames as dictionary keys using the string's default equality (case-sensitive by default). Normalize or use a consistent casing strategy before calling if your application expects case-insensitive behavior. -- The lock protects the HashSet instances stored in the dictionaries; callers do not need to synchronize when calling the public methods, but should avoid long-running work inside UserCountChanged handlers because the event is invoked from the presence-tracking flow (the implementation intentionally invokes the event outside the lock, but handlers that re-enter tracker methods could still affect ordering). -- The provided source appears truncated / contains a small syntax issue near GetChannelsForUser and GetConnectionsInChannels; verify the final implementation of those methods before relying on their exact return behavior. \ No newline at end of file +- `UserCountChanged` is invoked synchronously on the calling thread after the internal lock is released; subscribers are called inline and should avoid long-running work to prevent blocking the caller. +- `JoinChannel` creates or updates the per-`username` channel set in `_userChannels` even if that `username` currently has no active connections; channel membership is tracked separately from `_connections`. +- The implementation uses a single private lock (`_lock`) to protect mutations of the `HashSet` values stored in the `ConcurrentDictionary` instances; this simplifies correctness but can be a contention point at very large scale. Consider sharding presence state if you expect thousands of concurrent mutations per second. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md index 79d7dfe..dfdf0b0 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md @@ -6,8 +6,8 @@ - [ServerDirectoryService](#serverdirectoryservice) - [InfiniteRetryPolicy](#infiniteretrypolicy) + - [ServerDirectoryService (constructor)](#serverdirectoryservice-constructor) - [BuildConnection](#buildconnection) - - [ConnectWithRetryAsync](#connectwithretryasync) - [DisposeConnectionAsync](#disposeconnectionasync) - [ExecuteAsync](#executeasync) - [ExtractConflictingHosts](#extractconflictinghosts) @@ -21,16 +21,16 @@ - [RunConnectionLoopAsync](#runconnectionloopasync) - [StopAsync](#stopasync) - [DirectoryHubUrl](#directoryhuburl) - - [ReconnectBaseDelay](#reconnectbasedelay) - [ReconnectMaxDelay](#reconnectmaxdelay) + - [UserCountMinInterval](#usercountmininterval) - [DirectoryProtocol](#directoryprotocol) - [DirectoryRegistrationErrors](#directoryregistrationerrors) - [ErrorDetail](#errordetail) - [RegisterServerDto](#registerserverdto) - [RegisterServerResult](#registerserverresult) - [Response](#response) -- [ServerDirectoryService (constructor)](#serverdirectoryservice-constructor) -- [UserCountMinInterval](#usercountmininterval) +- [ConnectWithRetryAsync](#connectwithretryasync) +- [ReconnectBaseDelay](#reconnectbasedelay) --- @@ -43,16 +43,18 @@ public sealed class ServerDirectoryService : BackgroundService ``` -Maintains a durable, resilient registration of this process in the central server directory and continuously reports presence (user counts) to that directory. Run as a hosted BackgroundService, it opens and manages a SignalR HubConnection to the directory, performs server registration/claiming, publishes metadata (name, description, hosts, version, tags) and incremental presence updates, and automatically reconnects with backoff when the connection drops. +Maintains a long-lived SignalR connection to the central server directory and keeps this process advertised and up-to-date. `ServerDirectoryService` runs as a hosted background worker that connects to the directory hub at `DirectoryHubUrl`, attempts to register/claim the server identity (persisting a claim token via [`DirectoryClaimStore`](DirectoryClaimStore.cs.md)), and pushes aggregated presence (user count) updates derived from [`PresenceTracker`](PresenceTracker.cs.md) to the directory. Use this service when the application should automatically announce itself and maintain presence information in the shared directory rather than performing manual/one-off registration calls. ## Remarks -This service sits between the local PresenceTracker, a persistent DirectoryClaimStore (which holds claim tokens and server IDs), and the remote directory hub. It coalesces frequent presence changes into a single "latest wins" update using a single-slot bounded channel to avoid flooding the directory, and applies an exponential backoff on reconnect attempts to avoid tight retry loops. Certain registration failures (for example: host already claimed, invalid token, or host conflict) are treated as permanent for the lifetime of the process — once that permanent-failure state is observed the service stops attempting to register on that connection and any subsequent reconnects, leaving operator intervention required to correct configuration and restart. +`ServerDirectoryService` is the glue between local presence tracking and the remote directory. It encapsulates the connection lifecycle (built by `BuildConnection` and managed by `ConnectWithRetryAsync` and `RunConnectionLoopAsync`), registration/claim semantics (`RegisterAsync` and `HandleRegistrationResponseAsync`), and presence propagation (`OnUserCountChanged` and `ProcessUserCountUpdatesAsync`). To avoid noisy updates the service coalesces bursts of presence changes using a single-slot [`Channel<int>`](../../EchoHub.Core/Models/Channel.cs.md) (`_userCountUpdates`) so that the most recent count wins, and it enforces a minimum send interval controlled by `UserCountMinInterval`. The service also implements an increasing reconnect backoff bounded by `ReconnectBaseDelay` and `ReconnectMaxDelay` via `GetBackoffDelay`. If the registration receives a fatal error (examples noted in comments: `HostAlreadyClaimed`, `InvalidToken`, `HostConflict`) the service sets `_registrationPermanentlyFailed` and stops attempting further register attempts for this connection — the operator must fix configuration and restart the process. ## Notes -- Permanent registration failures stop further register attempts even across reconnections; the operator must fix configuration and restart the service to recover. -- Presence updates are coalesced and throttled: the single-slot channel drops intermediate values (latest wins) and sends no more often than the configured UserCountMinInterval, so short-lived fluctuations may be suppressed. -- Reconnect attempts use a backoff between ReconnectBaseDelay and ReconnectMaxDelay; expect progressively longer wait times on repeated failures. -- The service relies on configuration and on DirectoryClaimStore to persist claim tokens; ensure those dependencies are available and correctly configured or registration will fail. +- `OnUserCountChanged` feeds a single-slot channel so intermediate counts can be dropped; the directory will see only the latest value sent after throttling, not every intermediate change. This is by design to reduce churn. +- Presence updates are throttled by `UserCountMinInterval`; rapid updates will be coalesced and delayed to respect that interval. +- If `_registrationPermanentlyFailed` becomes true (due to registration error codes like `HostAlreadyClaimed`/`InvalidToken`/`HostConflict`), the service stops retrying registration on the current connection and on subsequent reconnects — fixing the configuration and restarting the service is required to recover. +- The implementation persists a freshly-issued claim token via [`DirectoryClaimStore`](DirectoryClaimStore.cs.md) early in the registration flow to provide a durability guarantee for first-time claims; this ordering is intentional to avoid losing a claim token on process crash. +- The startup logic yields briefly before attempting its initial connect so the host can finish starting; this affects the timing of the first registration attempt. + --- @@ -65,69 +67,68 @@ private sealed class InfiniteRetryPolicy : IRetryPolicy ``` -Retrieves indefinitely with exponential backoff capped at a maximum delay for reconnect attempts. This private sealed class implements IRetryPolicy and provides a retry strategy that increases the wait time between attempts rather than failing fast, enabling resilient reconnection to the server directory service. - -NextRetryDelay yields delays based on an exponential progression: the first retry occurs after 1 second, followed by 2 seconds, 4 seconds, 8 seconds, and 16 seconds. After that, the delay is capped by ReconnectMaxDelay (30 seconds), so all subsequent retries use that maximum delay. This approach balances persistence in the face of transient failures with a bound on retry timing to avoid excessive load. +`InfiniteRetryPolicy` is a private sealed class that implements `IRetryPolicy` to provide a retry strategy. Its `NextRetryDelay` computes the next wait as 2^min(`retryContext.PreviousRetryCount`, 10) seconds and returns it, capped by `ReconnectMaxDelay`, enabling indefinite retries while bounding the maximum wait. ## Remarks -This policy encapsulates a specific retry strategy behind the IRetryPolicy interface, isolating timing logic from the rest of the reconnection code. Marked private and sealed, it signals an internal, non-extendable implementation used solely by the server directory service's retry mechanism. The cap on delay helps prevent runaway retry intervals while still ensuring the system makes progress toward recovery. +This abstraction centralizes the exponential backoff so the rest of the server's reconnection logic shares a consistent, testable delay policy. By being private and sealed, it remains an internal implementation detail, reducing surface area for change and misuse outside its containing class. ## Notes -- The first retry delay is 1 second, not immediate. -- Delays progress as 1s, 2s, 4s, 8s, 16s, and then 30s for all subsequent retries due to the cap. -- If many clients share the same cap, consider introducing jitter at the call site to avoid synchronized retries (this policy does not include jitter by default). - +- The backoff growth saturates after the 10th retry; `NextRetryDelay` uses `Math.Min(retryContext.PreviousRetryCount, 10)` to compute the exponent, so delays cannot grow beyond `ReconnectMaxDelay`. --- -### BuildConnection +### ServerDirectoryService (constructor) > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` -> **Kind:** method +> **Kind:** constructor ```csharp -private HubConnection BuildConnection() +public ServerDirectoryService( + IConfiguration configuration, + PresenceTracker presenceTracker, + DirectoryClaimStore claimStore, + ILogger<ServerDirectoryService> logger) ``` -**Returns:** `HubConnection` +**Parameters:** + +| Parameter | Type | Default | +|-----------|------|---------| +| `configuration` | `IConfiguration` | — | +| `presenceTracker` | [`PresenceTracker`](PresenceTracker.cs.md) | — | +| `claimStore` | [`DirectoryClaimStore`](DirectoryClaimStore.cs.md) | — | +| `logger` | `ILogger<ServerDirectoryService>` | — | -BuildConnection constructs and returns a HubConnection configured to connect to the directory hub. It encapsulates the boilerplate of wiring the hub URL and an infinite automatic-reconnect policy, so callers can obtain a ready-to-configure connection without duplicating setup code. +Constructs a `ServerDirectoryService` by binding its essential collaborators: `IConfiguration`, [`PresenceTracker`](PresenceTracker.cs.md), [`DirectoryClaimStore`](DirectoryClaimStore.cs.md), and `ILogger<ServerDirectoryService>`. Typically invoked by the dependency injection container, it assigns these dependencies to the private fields `_configuration`, `_presenceTracker`, `_claimStore`, and `_logger` so the service can access configuration, track presence, manage directory claims, and emit logs. ## Remarks -BuildConnection centralizes the creation of the SignalR client used by the directory service, ensuring a consistent URL and reconnect policy across all call sites. By wrapping the builder steps, it reduces boilerplate and makes it easy to adjust the underlying connection strategy in one place. Note that the returned HubConnection is configured but not started; callers should invoke StartAsync (and manage its lifecycle) when ready. The attached InfiniteRetryPolicy governs how the client attempts to reconnect after a disconnect, providing resilience against transient network issues. +By design, this constructor is a straightforward DI-only initializer with no business logic. It simply wires the four collaborators into private fields so the rest of the service can coordinate configuration data, presence state, claim storage, and logging. ## Notes -- The connection is not started by BuildConnection; you must call StartAsync and later dispose of the connection to avoid leaks. Ensure DirectoryHubUrl is properly configured before using this method. +- This constructor does not perform argument null checks; rely on the DI container to provide valid instances. If you instantiate `ServerDirectoryService` manually, consider adding guards. +- Ensure the DI container is configured to register [`PresenceTracker`](PresenceTracker.cs.md), [`DirectoryClaimStore`](DirectoryClaimStore.cs.md), and `ILogger<ServerDirectoryService>` so resolution succeeds at startup. --- -### ConnectWithRetryAsync +### BuildConnection > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` > **Kind:** method ```csharp -private async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken ct) +private HubConnection BuildConnection() ``` -**Parameters:** - -| Parameter | Type | Default | -|-----------|------|---------| -| `connection` | `HubConnection` | — | -| `ct` | `CancellationToken` | — | - -**Returns:** `Task<bool>` +**Returns:** `HubConnection` -ConnectWithRetryAsync establishes a SignalR hub connection by repeatedly invoking StartAsync on the supplied HubConnection until the operation succeeds or the provided CancellationToken is triggered. When StartAsync completes successfully, the method returns true. If an exception occurs, the method increments its retry counter, computes a backoff delay via GetBackoffDelay(attempt), logs a warning including the delay, and awaits Task.Delay(delay, ct) before retrying. If the CancellationToken is canceled before a successful connection, the loop exits and the method returns false. This encapsulates transient-connection retry logic so callers do not have to implement their own retry loop. +BuildConnection creates and returns a new `HubConnection` configured to communicate with the directory hub. It wires the hub URL from `DirectoryHubUrl`, enables automatic reconnection using an `InfiniteRetryPolicy`, and returns the built instance for the caller to start and use. ## Remarks -Centralizes retry/backoff semantics for establishing the directory connection, so callers don't implement their own loop. It respects the cancellation token to avoid hanging and uses logging to surface transient failures for operators. +Encapsulating this setup here ensures consistent behavior across call sites that need a connection to the directory hub. The `InfiniteRetryPolicy` drives unbounded reconnect attempts, with the delay determined by `NextRetryDelay` on the `RetryContext`; callers should consider lifecycle management and potential long-running retries. ## Notes -- All exceptions from StartAsync are treated as retryable; there is no distinction between transient and permanent errors. -- CancellationToken is observed during both StartAsync and the subsequent Task.Delay, so cancellation is respected promptly. -- The backoff duration is determined by GetBackoffDelay(attempt); ensure this aligns with the desired backoff strategy to avoid excessively long waits or too-aggressive retries. +- The returned `HubConnection` is not started automatically; you must call `StartAsync()` before use. +- Each invocation yields a new `HubConnection`; reuse the instance if a single long-lived connection is required. --- @@ -148,20 +149,15 @@ private static async Task DisposeConnectionAsync(HubConnection connection) **Returns:** `Task` -Disposes the given HubConnection asynchronously with a hard 3-second timeout and suppresses any errors, ensuring shutdown proceeds without being blocked by a slow disposal. Use this during teardown when you want to promptly release the connection without surfacing disposal failures. +Disposes a `HubConnection` asynchronously with a bounded timeout by awaiting `DisposeAsync()` converted to a `Task` via `AsTask()` for up to 3 seconds. If the operation exceeds the timeout or throws, the exception is caught and ignored to prevent shutdown from blocking. This private helper ensures resources are released promptly during server shutdown without risking a hang. ## Remarks -This pattern encapsulates a best-effort disposal strategy: it waits up to three seconds for disposal to complete, then continues regardless of the outcome. By catching all exceptions, callers cannot rely on successful disposal being reported; if you need visibility into disposal failures, handle the disposal outside this helper. It is intended for shutdown scenarios where the connection must be released promptly and further operations on the connection are no longer needed. - -## Example -```csharp -// Example usage within the same class -await DisposeConnectionAsync(connection); -``` +This method isolates the disposal of a `HubConnection` from the rest of shutdown logic, providing a deterministic, non-blocking path when terminating the server. By swallowing disposal failures, it avoids a slow or faulty dispose from delaying process termination, though it hides potential cleanup issues that may warrant later diagnostics. As a private static helper, it signals that disposing a given `HubConnection` is a concern tied to the server's lifecycle rather than a general-purpose cleanup utility. ## Notes -- Empty catch hides disposal failures; only use this when shutdown must not be delayed by disposal issues. -- The 3-second timeout is hard-coded; adjust if your application's shutdown window requires a different bound. +- This method swallows all exceptions from `DisposeAsync` and the timeout; consider adding logging if you need visibility into disposal problems. +- The 3-second timeout is hard-coded and may not suit every environment; make it configurable if needed. +- Caller must ensure the `connection` parameter is non-null; passing null will throw before entering the try block. --- @@ -182,7 +178,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) **Returns:** `async Task` -Executes the host’s startup lifecycle for registering the server with the EchoHubSpace directory when configured to be public. It yields to the host to finish starting, validates the configuration, collects hosts and metadata from configuration, resolves the server version, and then delegates to the connection loop that maintains the directory registration. It also subscribes to user-count changes so presence updates can be propagated while the registration is active, and guarantees cleanup of the event subscription when the operation completes or fails. +Coordinates the public-directory registration lifecycle for the server. It first yields to the host to finish initialization, then decides whether to register by reading `Server:PublicServer` from configuration; when enabled, it gathers metadata from `Server:PublicHosts`, `Server:Name`, `Server:Description`, `Server:Tags`, and the computed `version`, logs its intent, subscribes to user-count changes via `_presenceTracker.UserCountChanged`, and starts the connection loop with `RunConnectionLoopAsync` using `stoppingToken`. If registration is disabled or required config is missing, it logs and exits gracefully. + +## Remarks +This symbol serves to encapsulate the startup flow for a publicly visible server: it centralizes the decision, metadata collection, and lifecycle management needed to register with the directory. The initial `await Task.Yield()` gives the host a chance to continue its startup sequence before any logging or network activity. The subscription to `_presenceTracker.UserCountChanged` is paired with a `finally` to guarantee cleanup and avoid leaks, even if the connection loop fails or is cancelled. + +## Notes +- The code unsubscribes from `_presenceTracker.UserCountChanged` in `finally` to avoid memory leaks and stray callbacks after the connection loop ends. --- @@ -203,17 +205,23 @@ private static string[]? ExtractConflictingHosts(ErrorDetail? error) **Returns:** `string[]?` -ExtractConflictingHosts reads a JSON payload from an ErrorDetail's Data field and returns the list of host names described under the ConflictingHosts property. It tolerates both PascalCase (ConflictingHosts) and camelCase (conflictingHosts) keys because SignalR's wire casing depends on the hub's serializer configuration, and the field is typed object?. - -It returns null when the payload is missing or not a JSON object, when neither property is present, when the property isn't an array, or when the array contains no string entries. Otherwise, it returns a string[] of the host names extracted from the array. +ExtractConflictingHosts pulls the `ConflictingHosts` from an error's loosely-typed `Data` payload and returns it as a `string[]` when present. It tolerates both PascalCase and camelCase keys to accommodate different serializer configurations. If the payload is missing, not a JSON object, not an array, or contains no string values, the method returns null. ## Remarks -The helper centralizes resilient parsing of optional error metadata, shielding callers from variations in payload shape and casing. By returning null for absent or empty data, it lets higher-level error handling distinguish between "no information" and an explicit list of conflicting hosts. + +By centralizing the JSON-payload parsing in a small helper, callers do not need to know the wiring quirks of the error data or the particular casing produced by the hub's serializer. It provides a stable, strongly-typed extraction point for host names when conflicts are reported. + +## Example + +```csharp +string[]? hosts = ExtractConflictingHosts(error); +``` ## Notes -- Returns null rather than an empty array when no hosts are present or the data is malformed. -- Non-string elements inside the host list are ignored. -- The method is private and static, indicating it is an internal helper for extracting just this piece of information from a larger error payload. + +- Returns null if the input error is null, the `Data` payload is not a JSON object, the relevant property is missing, or the array contains no string values. +- Non-string items within the `ConflictingHosts` array are ignored; only string values are collected. +- The source snippet in the method contains a likely compile-time issue: `List<string> hosts = []` is invalid C#. It should be initialized as `new List<string>()` (or `var hosts = new List<string>();`). This is a potential trap to address during review. --- @@ -234,14 +242,13 @@ private static TimeSpan GetBackoffDelay(int attempt) **Returns:** `TimeSpan` -Calculates the exponential backoff delay for a reconnect attempt. Given the retry attempt index, it computes 2^min(attempt, 10) seconds, then clamps the result to ReconnectMaxDelay. The returned TimeSpan is used by the reconnect logic to wait before the next attempt, ensuring that retries are spaced out but never exceed a configured maximum delay. +`GetBackoffDelay` computes the wait duration before the next reconnect attempt. Given an `attempt`, it derives the delay as `TimeSpan.FromSeconds(Math.Pow(2, Math.Min(attempt, 10)))` and returns the value capped at `ReconnectMaxDelay` as a `TimeSpan`. ## Remarks -Isolates the backoff policy in a small helper, keeping the retry loop simple and readable. The exponential ramp-up helps avoid overwhelming the remote endpoint while still providing progressively longer waits as failures persist; the cap guarantees a bound on wait times. This method is private to the class and intended for internal use by the directory service's reconnect flow. +This symbol encapsulates the reconnect retry policy within the server directory service to ensure consistent timing across retries. It employs exponential growth with a hard cap to prevent unbounded delays while avoiding overly-aggressive backoff in early attempts. ## Notes -- Ensure 'attempt' is non-negative; negative values yield sub-second delays due to 2^negative, which may be surprising. -- The delay is clamped to ReconnectMaxDelay; even very large attempts cannot produce longer waits. +- The exponent is capped by `Math.Min(attempt, 10)`, so delays stop growing exponentially after the 10th attempt; beyond that, the final delay is determined by `ReconnectMaxDelay`. --- @@ -262,17 +269,10 @@ private Task HandleRegistrationErrorAsync(ErrorDetail[]? errors) **Returns:** `Task` -HandleRegistrationErrorAsync interprets the errors returned by the directory registration attempt, logs a code-specific message, and marks the registration as permanently failed to stop automatic retries until a restart. +HandleRegistrationErrorAsync centralizes the processing of errors reported by directory registration. It marks the registration as permanently failed, derives an error code (falling back to `UnknownError`) and the set of conflicting hosts from the first error, and then logs a targeted, code-specific message before recording the failure in `_claimStore`. ## Remarks - -This method centralizes the directory registration failure handling, consolidating how various error codes are surfaced to operators and how the failure state is recorded. It relies on ExtractConflictingHosts to surface any conflicting hosts and on the claim store to persist the failure details so that diagnostics and recovery decisions can reason about what happened. - -## Notes - -- It forces the server into a permanently failed state for registration, with log messages indicating that the server will not retry until restarted. This ensures long-running processes do not silently retry under inconsistent directory state. -- It returns a completed Task and performs its side effects synchronously (logging and state mutation) without throwing, so callers can await the result safely without handling exceptions. - +By encapsulating this logic in one place, the method decouples error interpretation from the main registration flow. It coordinates with `_claimStore` to persist a failure snapshot and with `_logger` to surface actionable diagnostics for operators, aiding remediation. The design relies on the first error and the extracted conflicts to provide a deterministic failure narrative while supporting specific guidance for each known error code from `DirectoryRegistrationErrors` (e.g. `HostAlreadyClaimed`, `InvalidToken`, `HostConflict`, `InvalidInput`). --- @@ -296,7 +296,14 @@ private async Task HandleRegistrationResponseAsync(Response<RegisterServerResult **Returns:** `Task` -Handles the directory registration response by validating the envelope, enforcing protocol compatibility, and performing the appropriate follow-up depending on success or failure. It ensures a durability guarantee by saving a fresh claim token before acknowledging success, otherwise updating the ServerId to keep internal state in sync, and it logs the outcome for observability. +Handles the asynchronous response from the directory registration workflow. It validates the envelope is non-null, asserts the protocol version against `DirectoryProtocol.Version`, and then branches on success or failure, performing durability-oriented state updates via internal stores and logging the outcome. + +## Remarks +This method centralizes response handling for directory registration: it immediately treats null envelopes, protocol mismatches, or malformed success payloads as permanent failures to avoid operating against an incompatible or corrupted directory. On success, it ensures a fresh `ClaimToken` is persisted before acknowledging the new `ServerId`, guaranteeing durability for the initial credential and consistent recovery behavior after restarts. The approach also accommodates re-registration by syncing the persisted `ServerId` when no new token is provided, keeping local state aligned with the directory. + +## Notes +- If a new `ClaimToken` is supplied, it is saved prior to completing the success path; if not, the method only updates the persisted `ServerId` to reflect the directory. + --- @@ -317,14 +324,13 @@ private void OnUserCountChanged(int newCount) **Returns:** `void` -OnUserCountChanged is an internal callback invoked whenever the server detects a change in the user count. It publishes the new value by writing to a single-slot channel via _userCountUpdates.Writer.TryWrite(newCount). The single-slot channel semantics coalesce bursts of updates so that only the most recent count is propagated downstream, reducing churn and avoiding repeated handling for rapid presence changes. +Internal event handler that forwards the new user count into the single-slot update channel. It uses `_userCountUpdates.Writer.TryWrite(newCount)` to coalesce bursts of presence changes, ensuring only the latest value is observed downstream. ## Remarks -By delegating to a dedicated Writer, this method decouples the act of detecting changes from the consumers that react to them. The single-slot channel pattern ensures downstream work is debounced; observers observe the latest value after a change cycle, which is ideal for presence-aware UI updates or telemetry. +This internal abstraction decouples the producer of presence changes from the consumer by routing updates through the `_userCountUpdates.Writer` channel on a single-slot channel. The non-blocking `TryWrite` call ensures bursts of updates don't overwhelm downstream processing, preserving only the most recent value. ## Notes -- The call does not inspect the result of TryWrite; if the channel is full or readers slow, an update may be dropped, so downstream consumers should be able to tolerate occasional missed updates. -- This method is private to its containing type; external code should not rely on direct invocation. +- The non-blocking nature of the `TryWrite` call means bursts can be coalesced and intermediate counts may be dropped; only the latest value is observed. --- @@ -347,15 +353,8 @@ private async Task ProcessUserCountUpdatesAsync(HubConnection connection, Task c **Returns:** `Task` -ProcessUserCountUpdatesAsync continuously consumes user-count updates from a buffered channel and, when appropriate, reports the latest count to the directory via a HubConnection. It throttles emissions to respect a minimum interval, draining newer values during the wait so the hub receives the most up-to-date count rather than a stale snapshot, and it exits gracefully on cancellation or connection closure; updates are only sent when the hub is connected and the local registration is valid. - -## Remarks -This method decouples producers of user-count data from the actual update path by reading from a channel and emitting to the directory hub only when ownership conditions are met. The drain-on-wait policy ensures the directory reflects the latest state under bursty updates without flooding the hub, and the preconditions (connected hub and valid registration) guard presence reporting within the lifecycle of the system. +Runs an asynchronous background loop that propagates the latest observed user count to the directory service over a `HubConnection`. It listens for updates from `_userCountUpdates.Reader` and terminates when the `connectionClosed` task completes or cancellation is requested via the `ct` token. On each update, it reads the current `count`, then throttles sends to respect `UserCountMinInterval` (draining newer values during the wait so the eventual call carries the latest count). If the count hasn’t changed since the last report, or the hub is not connected, or registration has permanently failed or is not currently registered, it skips sending. When sending is appropriate, it invokes the hub method `UpdateUserCount` with the latest `count`, updates `_lastReportedUserCount` and `lastSentAt`, and logs the outcome. This pattern ensures updates are delivered efficiently, tolerate bursts, and never crash due to transient failures. -## Notes -- Updates may be dropped under bursty traffic due to throttling and the drain loop; callers should not rely on every intermediate update being observed by the hub. -- If the hub is temporarily unavailable or registration is not established, updates are skipped until conditions are met; there is no automatic backoff beyond the local catch logging. -- Cancellation via the provided CancellationToken causes an immediate exit from the loop; ensure callers signal cancellation during shutdown to avoid lingering tasks. --- @@ -380,15 +379,7 @@ private async Task RegisterAsync(string name, string? description, string[] host **Returns:** `Task` -Registers the current server with the directory service over a live hub connection. If connected and not permanently failed, it collects the online user count, builds a RegisterServerDto containing the server's name, description, hosts, user count, version, tags, and the current claim token, and sends it to the directory via the hub's RegisterServer method. The response is processed by HandleRegistrationResponseAsync to apply the result locally. Any exceptions are caught and logged as warnings with no propagation to the caller. - -## Remarks -Encapsulates the server-registration handshake behind a private method, so callers don't have to manage DTO construction or hub invocation directly. It coordinates with _presenceTracker for live user counts and with _claimStore to carry the persisted claim token across registrations. The guard checks against the hub's connection state and a permanent-failure flag reflect the intended lifecycle: registration only runs when viable, preventing noisy or duplicate attempts. - -## Notes -- Exceptions are swallowed; the method logs a warning and does not rethrow, so callers cannot rely on exceptions for control flow and may need separate retry logic. -- On the first-ever registration, ClaimToken is null; after the first successful claim it is persisted and reused on subsequent registrations. -- If not connected or if a permanent failure has been recorded, the method returns immediately without attempting registration. +RegisterAsync asynchronously registers the current server with the directory service when the hub connection is active. It exits early if the hub is not connected or if a permanent registration failure has been recorded, avoiding unnecessary work. When proceeding, it reads the current online user count from `_presenceTracker.GetOnlineUserCount()`, builds a `RegisterServerDto` with the server's `name`, `description`, `hosts`, `userCount`, `version`, `tags`, and the persisted claim token `_claimStore.ClaimToken`, and then calls the directory via `_connection.InvokeAsync<Response<RegisterServerResult>>("RegisterServer", dto)`. The envelope is then passed to `HandleRegistrationResponseAsync` to finalize the registration flow. --- @@ -403,15 +394,14 @@ private static string ResolveVersion() **Returns:** `string` -Resolves the version string used to identify the running ServerDirectoryService assembly. It prefers AssemblyInformationalVersionAttribute.InformationalVersion, but strips any SourceLink git SHA suffix (for example '0.2.10+abc123') before returning; if that attribute isn't present, it falls back to the assembly version, and finally to '0.0.0' if neither is available. +Returns a human-friendly version string for the server assembly. It is a private static helper that reads the `AssemblyInformationalVersionAttribute.InformationalVersion` from the containing assembly (via `typeof(ServerDirectoryService).Assembly`) and, if present, strips any `+` suffix (git SHA) added by SourceLink before returning the value; if not present, it falls back to the assembly's `Version` as a string, and finally to the literal `0.0.0` if neither is available. ## Remarks -This centralizes version resolution for the ServerDirectoryService, ensuring a consistent, human-friendly version string for diagnostics, logging, and server identity without leaking VCS details. It prefers source-controlled metadata when possible, but gracefully degrades to a stable default when it's not. +This tiny helper centralizes version resolution for the server, ensuring consistent display and logging of version regardless of build configuration. By extracting the informational version when available and normalizing away VCS metadata, it prevents leaking internal identifiers while still reflecting the actual package version. The implementation relies on reflection to read the version data from the containing assembly, so the produced value depends on the built assembly's metadata at runtime. ## Notes -- Strips the SourceLink git SHA suffix by locating the '+' and returning the prefix portion only. -- If neither informational version nor assembly version is available, the method returns '0.0.0'. -- Uses a private static helper scope; ensure tests align with the private context and the assembly hosting the symbol remains ServerDirectoryService's assembly. +- If the `InformationalVersion` contains a `+` (the SourceLink suffix), only the portion before `+` is returned, keeping the string human-friendly. +- If neither the informational version nor the standard assembly version is available, the method returns the literal `0.0.0` as a safe fallback. --- @@ -443,37 +433,10 @@ private async Task RunConnectionLoopAsync( **Returns:** `Task` -## Source Code -Runs a resilient, long-running loop that manages the lifecycle of a connection to a directory service. It repeatedly builds a connection, wires in heartbeat and re-registration logic, and, when the connection is permanently closed or cancellation is requested, cleanly tears down and rebuilds the connection to maintain availability. +Runs a resilient, long-running loop that maintains a connection to the directory service by repeatedly building a connection via `BuildConnection()`, wiring up `Ping`/`Heartbeat`, `Reconnected`, and `Closed` handlers, and connecting with retry through `ConnectWithRetryAsync`. On a successful connect, it registers the server with `RegisterAsync` and streams user-count updates by calling `ProcessUserCountUpdatesAsync` until cancellation or a permanent disconnection is signaled via a `TaskCompletionSource`. When a permanent close occurs or cancellation is requested, the method disposes the connection and rebuilds after a short delay. ## Remarks -This method centralizes the connection lifecycle management, coordinating connection establishment, heartbeat handling, re-registration on reconnect, and clean disposal. It relies on a cancellation token and a TaskCompletionSource to synchronize asynchronous events across the loop, enabling robust recovery paths while preserving a consistent registration state. - -## Notes -- If ConnectWithRetryAsync(connection, stoppingToken) returns false, the method exits, causing the outer loop to terminate and the service to stop attempting a reconnect. -- The On("Ping") handler sends a heartbeat back to the directory and safely logs any heartbeat failures without crashing the loop. -- When the connection is permanently closed, the Closed event signals completion via the TaskCompletionSource and the outer loop proceeds to rebuild after a short delay, unless cancellation has been requested. - -## Dependencies -- TaskCompletionSource -- TaskCreationOptions -- Task - -## Dependency APIs -- TaskCompletionSource (non-generic) - - Constructor: TaskCompletionSource(TaskCreationOptions options) - - Property: Task Task { get; } -- TaskCreationOptions (enum) - - Member used: RunContinuationsAsynchronously -- Task (System.Threading.Tasks.Task) - - Represents an asynchronous operation; used for awaiting and coordinating async work - -## Symbol To Document -- Name: RunConnectionLoopAsync -- Kind: method -- File: src/EchoHub.Server/Services/ServerDirectoryService.cs -- Language: csharp -- ID: a28a6f2b-23c7-4de0-bb5e-3da24401feb3 +The method centralizes all aspects of directory connectivity—heartbeat, re-registration, and back-to-back disconnections—into a single loop, minimizing risk of desynchronization between the server and directory state. It uses a `TaskCompletionSource` to coordinate the 'permanent close' signal so the outer loop can rebuild cleanly after a failure, and respects `_registrationPermanentlyFailed` to avoid blind re-registration after a known permanent fault. --- @@ -494,13 +457,15 @@ public override async Task StopAsync(CancellationToken cancellationToken) **Returns:** `async Task` -Override of StopAsync performs the base shutdown logic and then clears the internal _connection to release the resource and reflect that the service is disconnected. +This `StopAsync` override extends the base stop behavior by clearing the service's internal `_connection` after the base stop completes, ensuring resources are released and the connection cannot be reused. It first awaits `base.StopAsync(cancellationToken)` to perform the standard shutdown, then sets `_connection` to `null`. ## Remarks -Ensures the derived service participates in the lifecycle by letting the base stop routine complete before releasing its own resources. Clearing _connection after the base stop prevents reuse of an active connection during shutdown and marks the service as disconnected for the rest of the system. + +Clearing `_connection` after the base stop ensures there are no lingering references to an active connection once shutdown has begun. It communicates a clear lifecycle boundary for the service's connection state to its collaborators and helps GC reclaim resources. ## Notes -- If base.StopAsync throws, _connection will not be cleared; consider wrapping the cleanup in a finally block to guarantee cleanup. + +- Be aware that `_connection` becomes `null` after `StopAsync` completes; code that accesses `_connection` during shutdown should guard against null references or only run after shutdown is finished. --- @@ -513,53 +478,42 @@ private const string DirectoryHubUrl = "https://echohub.voidcube.cloud/hubs/serv ``` -This private constant string DirectoryHubUrl holds the base URL for the servers directory hub used by ServerDirectoryService. It is initialized to https://echohub.voidcube.cloud/hubs/servers and should be referenced wherever the directory hub endpoint is needed, ensuring a single source of truth and avoiding string duplication. - -## Remarks -By centralizing the hub URL in a single private constant, the class avoids scattering the endpoint string across multiple methods. This reduces the risk of inconsistent paths and simplifies maintenance if the hub address changes; it also makes the code more testable by isolating the configuration-like value in one place. - -## Notes -- The value is baked into the assembly as a private const; it cannot be overridden at runtime. For environment-specific endpoints, consider configuration-driven access and testing hooks to swap or mock the value. +Defines the immutable base URL for the directory hub used by the `ServerDirectoryService` to reach server endpoints: `https://echohub.voidcube.cloud/hubs/servers`. As a private `const`, the value is baked into the assembly, ensuring a single source of truth for hub interactions within this service. --- -### ReconnectBaseDelay +### ReconnectMaxDelay > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` > **Kind:** field ```csharp -private static readonly TimeSpan ReconnectBaseDelay = TimeSpan.FromSeconds(2) +private static readonly TimeSpan ReconnectMaxDelay = TimeSpan.FromSeconds(30) ``` -ReconnectBaseDelay is a private static readonly TimeSpan that defines the base wait time used by ServerDirectoryService when retrying a failed connection. By centralizing this 2-second base delay, the code avoids magic numbers and provides a single point to tune the retry cadence across all reconnection attempts. +ReconnectMaxDelay defines the upper bound for the delay between reconnection attempts performed by the service. Declared as a private static readonly `TimeSpan` and initialized with `TimeSpan.FromSeconds(30)`, it provides a single, immutable cap that applies to all reconnect logic within the `ServerDirectoryService`. ## Remarks -Centralizing the base delay enforces a uniform retry cadence and simplifies tuning during incidents or tests. Being static and readonly ensures the value is shared across all instances and cannot be changed at runtime, which preserves predictable timing in concurrent reconnection scenarios. If you later introduce a more sophisticated backoff strategy (for example, exponential backoff with jitter), this base delay would typically feed that mechanism rather than replace it. - -## Notes -- Changing this value affects all reconnection retries across the service; it's a global constant for the directory service. -- Because it is private, external code or tests cannot override it directly; consider configuration or making it injectable if runtime tunability is required. +Static readonly guarantees a shared, immutable cap across all instances, ensuring the reconnect cadence remains consistent even under concurrent reconnect operations. Because the field is private, the policy cannot be adjusted from outside the class; tuning requires a code change rather than a runtime configuration. --- -### ReconnectMaxDelay +### UserCountMinInterval > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` > **Kind:** field ```csharp -private static readonly TimeSpan ReconnectMaxDelay = TimeSpan.FromSeconds(30) +private static readonly TimeSpan UserCountMinInterval = TimeSpan.FromSeconds(1) ``` -Defines the upper bound for the delay between reconnection attempts. The field is private, static, and readonly, initialized as TimeSpan.FromSeconds(30). It is used by the server’s internal reconnection logic to cap backoff durations, ensuring retry intervals remain bounded even under transient network issues. +This private static readonly `TimeSpan` defines the minimum interval between user-count operations inside the class, enforcing throttling to avoid rapid updates. It is initialized as `TimeSpan.FromSeconds(1)` and should be used wherever the class would otherwise perform frequent user-count recomputations to maintain consistent timing. ## Remarks -Centralizes reconnection policy within ServerDirectoryService to ensure consistent retry timing across all attempts. Being private and readonly, this value is not exposed to external components and cannot be modified at runtime, promoting predictable behavior and easier maintenance. The 30-second cap prevents excessively long delays during outages while avoiding overly aggressive retry loops. +This field centralizes the throttling policy for user-count computations within the class, ensuring consistent timing across internal update paths. Making it `static` and `readonly` prevents accidental drift at runtime and communicates that the value is a fixed policy rather than dynamic state. It also makes tuning straightforward: adjust this single value to influence all user-count throttling behavior without changing multiple call sites. ## Notes -- The maximum delay is fixed after class initialization; changing it requires code edits and a recompilation. -- Private scope ensures external code cannot depend on or bypass this policy. +- The value is baked into the assembly; changing it requires recompilation unless the code is refactored to read from a configuration source. --- @@ -572,14 +526,14 @@ internal static class DirectoryProtocol ``` -DirectoryProtocol is a small, internal helper that exposes the current envelope protocol version used by the server's directory communications. The Version constant holds the protocol version as a string ("1.0"), and serves as a single source of truth for compatibility checks. Bumps to this version are coordinated across both repositories to keep envelope formats aligned during client/server exchanges. +Pinned envelope protocol version is centralized in a single constant. The value is exposed as `DirectoryProtocol.Version`, so code references a single source of truth rather than duplicating version strings, ensuring coordinated upgrades across both repositories when the envelope protocol evolves. ## Remarks -By centralizing the protocol version, this type makes explicit when envelope formats may evolve and prevents drift between the two sides. It also clarifies where to pull the version for any envelope construction or validation, reducing the risk of duplicating literals across the codebase. +It acts as a minimal contract boundary by providing a stable, centralized version that downstream code can validate against. By routing all version bumps through `DirectoryProtocol.Version`, the codebase gains a predictable upgrade path and reduces drift between repositories. ## Notes -- Do not hard-code '1.0' in multiple places; reference DirectoryProtocol.Version instead. -- This class is internal; its Version member is only accessible to code within the same assembly, so cross-repo coordination relies on the shared build/packaging process. +- Because `Version` is a `const`, its value is baked into compiled assemblies; updating it requires recompiling all dependents and coordinating updates across both repositories. +- Changes to the version must be performed in sync across both repositories to prevent a mismatch in protocol expectations. --- @@ -592,13 +546,15 @@ internal static class DirectoryRegistrationErrors ``` -This internal static class DirectoryRegistrationErrors serves as a centralized collection of string constants that represent the standard error codes used during the EchoHub server's directory registration workflow. It helps avoid hard-coded literals spread across the codebase and provides a single source of truth for the messages the hub uses to classify and surface registration failures. The constants cover common causes like invalid input, invalid token, host state conflicts, as well as client-side synthetic codes used for status reporting (ProtocolVersionMismatch, MalformedResponse) which are not emitted by the hub. +DirectoryRegistrationErrors is an internal static class that defines a concise set of error-code constants used during directory registration in the EchoHub server. It provides named codes such as `InvalidInput`, `InvalidToken`, `HostAlreadyClaimed`, and `HostConflict` to represent specific failure reasons returned by the server, eliminating scattered string literals and reducing typos. It also includes client-side synthetic codes `ProtocolVersionMismatch` and `MalformedResponse`, which are generated locally for status reporting and are not emitted by the hub. ## Remarks -The class is internal to the server assembly and centralizes canonical error codes for the directory registration subsystem to promote consistent error handling across components. Keeping these strings in one place reduces typos and mismatches in error reporting and mapping. Note that ProtocolVersionMismatch and MalformedResponse are client-side synthetic codes documented here for parity; they are never emitted by the hub. +By centralizing these values, the codebase gains a single source of truth for directory-registration errors, simplifying error handling, testing, and mapping to user-visible messages. It distinguishes between server-disclosed error codes (the first four) and client-side diagnostics (the two synthetic codes) that help with local status reporting without being emitted by the hub. ## Notes -- Not accessible from outside the server assembly; if you need client-visible error codes, expose a separate contract instead. +- Changing any constant's value is a breaking change; external or internal code that relies on the exact string value may fail after the change. +- The constants are compile-time constants; ensure all referencing code is recompiled together to avoid mismatches. +- The two client-side codes (`ProtocolVersionMismatch`, `MalformedResponse`) are for client-only diagnostics and are not emitted by the hub; avoid handling them as server-facing error payloads. --- @@ -619,28 +575,14 @@ internal record ErrorDetail(string Code, string? Message, JsonElement? Data) | `Data` | `JsonElement?` | — | -ErrorDetail represents a single error entry that can appear inside a `Response<T>` as part of an API error payload. It carries an error code (Code), an optional human-readable message (Message), and an optional Data payload for error-specific details. The Data field is typed as JsonElement to keep the payload shape flexible, accommodating different errors with varying detail structures (for example, a host-related error might include a ConflictingHosts array). As a record, ErrorDetail benefits from value-based equality and immutability, which makes it a stable, serializable unit for error reporting across API boundaries. +An internal record that represents a single error entry inside a `Response<T>`. The `Code` identifies the error kind, [`Message`](../../EchoHub.Core/Models/Message.cs.md) provides an optional human-readable description, and `Data` carries an optional, loosely-typed payload as a `JsonElement` to accommodate varying error shapes (e.g. host-related errors might carry a list of conflicting hosts). ## Remarks -ErrorDetail's design separates the error signaling (via Code) from the optional payload (Message and Data), enabling clients to react to known codes while optionally surfacing human-readable context or structured details. It works alongside the surrounding `Response<T>` wrapper to assemble a consistent error surface while preserving flexibility in the Data payload. The JsonElement Data keeps the detail shape decoupled from the type system, at the cost of requiring clients to inspect Code before interpreting Data. - -## Example -```csharp -using System.Text.Json; - -var json = "{\"ConflictingHosts\":[\"host1\",\"host2\"]}"; -JsonElement data = JsonSerializer.Deserialize<JsonElement>(json); - -var error = new ErrorDetail( - Code: "ConflictingHosts", - Message: "One or more hosts conflict with existing entries.", - Data: data -); -``` +This abstraction decouples error signaling from concrete payload schemas by wrapping code, message, and data within a single value. It fits the `Response<T>` pattern by enabling diverse error details to accompany a common envelope, while allowing clients to switch on `Code` to interpret the `Data` payload. ## Notes -- Data is loosely-typed by design; clients should first inspect Code to determine how to interpret Data. -- If Data is null, the consumer should rely on Code and optional Message for context. +- JsonElement is a view into the underlying JsonDocument; if the document is disposed, the Data value becomes invalid. Ensure the originating `JsonDocument` remains alive as long as `ErrorDetail.Data` is accessed. +- If you need a durable payload, consider storing `Data.GetRawText()` or a deserialized DTO instead of keeping the `JsonElement` itself. --- @@ -672,26 +614,29 @@ internal record RegisterServerDto( | `ClaimToken` | `string?` | — | -RegisterServerDto is a compact, immutable data transfer object (record) that carries all the information required to register a server in the EchoHub server directory. It groups identity data (Name, Version), optional metadata (Description), network endpoints (Hosts), current user load (UserCount), and classification tags (Tags) into a single value object so callers supply a single payload to the directory service rather than wiring multiple fields through separate calls. The optional ClaimToken supports claim-based authorization when needed. +RegisterServerDto is an internal C# positional record that serves as the single, strongly-typed payload for registering a server with the directory service. It captures the server's identity (``Name``), optional description (``Description``), the collection of host endpoints (``Hosts``), the current user count (``UserCount``), the software version (``Version``), a set of metadata tags (``Tags``), and an optional authentication token (``ClaimToken``). Because it is immutable and passed as a single object, it keeps registration logic clean and reduces parameter clutter across layers. ## Remarks -RegisterServerDto exists to encapsulate the registration data in a single cohesive unit, decoupling the producer from the directory service and enabling consistent validation and persistence. As a record, it provides value-based equality, which helps determine duplicates or idempotent operations across registration attempts. +RegisterServerDto is internal and immutable, which helps ensure a consistent snapshot of registration data as it moves through the directory service. By bundling related fields together, it reduces coupling between components and makes validation, logging, and auditing easier. The nullable fields ``Description`` and ``ClaimToken`` reflect optional aspects of registration; consumers should handle possible nulls and token absence accordingly. ## Example ```csharp +// Example of constructing the payload for registration var dto = new RegisterServerDto( - Name: "EchoServer-01", - Description: "Primary gateway for region A", - Hosts: new[] { "https://host1.example.com", "https://host2.example.com" }, - UserCount: 128, - Version: "2.3.1", - Tags: new[] { "production", "gateway" }, - ClaimToken: null + "EchoServer-01", + "Primary gateway", + new string[] { "tcp://host1:1234", "tcp://host2:1234" }, + 42, + "2.3.1", + new string[] { "gateway", "primary" }, + null ); ``` ## Notes -- The Hosts and Tags properties are string[] arrays; their contents can be mutated after construction since arrays are mutable. If you need true immutability, consider defensive copies or using a read-only collection type in a different design. +- The DTO does not enforce invariants (e.g., you should ensure `Hosts` is non-empty and `UserCount` is non-negative before registration). +- The type is marked `internal`; outside of its containing assembly, code cannot construct or consume it unless test-friendly tooling like `InternalsVisibleTo` is configured. + --- @@ -711,23 +656,15 @@ internal record RegisterServerResult(Guid ServerId, string? ClaimToken) | `ClaimToken` | `string?` | — | -An internal, immutable data carrier that represents the outcome of registering a server in EchoHub's directory service. It holds the assigned ServerId and an optional ClaimToken returned alongside the registration result. Callers construct this type at the end of a registration flow to pass both pieces of information together, rather than returning them separately. +RegisterServerResult is an immutable value object that represents the outcome of registering a server. It contains the server's identity (`ServerId`), a `Guid`, and an optional `ClaimToken` (`string?`) that callers may use for subsequent authenticated operations. ## Remarks -Designed to decouple the registration outcome from the service logic and to expose a stable, immutable snapshot of the operation. By using a record, it gains value-based equality and built-in deconstruction, which makes testing and wiring across layers straightforward. The optional ClaimToken acknowledges scenarios where a token is not issued; consumers should handle its absence gracefully. - -## Example -```csharp -var serverId = Guid.NewGuid(); -var result = new RegisterServerResult(serverId, "token-abc"); -// result.ServerId == serverId -// result.ClaimToken == "token-abc" -``` +This symbol acts as a focused data carrier between the registration flow and its consumers. By leveraging the `record` construct, it gains value-based equality and built-in immutability, ensuring the result is stable once created. Its `internal` visibility confines the contract to the assembly, underscoring that server registration details are an internal concern of the `ServerDirectoryService`. ## Notes -- ClaimToken is nullable; callers should guard against null before use. -- The type is internal, so it is not part of the public API surface outside its assembly. -- Being a record, it supports deconstruction (e.g., `var (id, token) = result;`) and value-based equality, which aids comparisons and pattern-based usage. +- The `ClaimToken` property can be `null` if no token is issued during registration. +- Treat the `ClaimToken` as sensitive data; avoid logging or persisting it in plain text and only keep it in memory for as long as needed. +- `RegisterServerResult` is immutable; do not mutate its properties after construction. Rely on the record's value semantics when comparing results. --- @@ -749,72 +686,63 @@ internal record Response<T>(bool IsSuccess, T? Data, ErrorDetail[]? Errors, stri | `Version` | `string?` | — | -Generic envelope wrapper for directory hub responses. It mirrors the EchoHubSpace contract by wrapping a success indicator, an optional payload, optional errors, and an optional version string into a single, transport-safe object. +Generic, immutable envelope that wraps every directory hub response. It exposes a boolean `IsSuccess`, an optional data payload `Data`, an optional array of `ErrorDetail` in `Errors`, and an optional `Version`. Use `Response<T>` whenever you need a consistent, hub-wide response shape instead of ad-hoc return types: place the operation’s payload in `Data`, set `IsSuccess`, attach any `Errors` if something went wrong, and optionally include `Version` for compatibility. ## Remarks -This envelope isolates transport concerns from business logic by providing a uniform surface for responses. Callers should always check IsSuccess before using Data, and rely on Errors for details when it is false. Data and Version are nullable, so consumers must guard for nulls and treat Version as optional metadata rather than a payload. The generic T makes this wrapper reusable for any payload. - -## Example -```csharp -var response = new Response<string>(true, "directory listing", null, "1.2"); -``` +By mirroring the `EchoHubSpace` contract, this envelope centralizes response structure and simplifies client and server handling of hub results. The generic parameter `T` lets you wrap any payload while preserving a single, predictable transport form. It also separates business data from transport metadata: callers typically check `IsSuccess` first, then read `Data` or `Errors` accordingly. ## Notes -- When IsSuccess is false, Data may be null; always inspect the Errors collection for failure details. -- The symbol is internal to its assembly; to share the envelope across boundaries, you may need a public abstraction or converter on your side. +- `Data` is nullable; always guard against null when consuming `Data`. +- If `IsSuccess` is false, prefer inspecting `Errors` for failure details rather than using `Data`. +- `Version` is optional and may be omitted; treat it as informational metadata rather than a contract guarantee. --- -## ServerDirectoryService (constructor) +## ConnectWithRetryAsync > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` -> **Kind:** constructor +> **Kind:** method ```csharp -public ServerDirectoryService( - IConfiguration configuration, - PresenceTracker presenceTracker, - DirectoryClaimStore claimStore, - ILogger<ServerDirectoryService> logger) +private async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken ct) ``` **Parameters:** | Parameter | Type | Default | |-----------|------|---------| -| `configuration` | `IConfiguration` | — | -| `presenceTracker` | [`PresenceTracker`](PresenceTracker.cs.md) | — | -| `claimStore` | [`DirectoryClaimStore`](DirectoryClaimStore.cs.md) | — | -| `logger` | `ILogger<ServerDirectoryService>` | — | +| `connection` | `HubConnection` | — | +| `ct` | `CancellationToken` | — | +**Returns:** `Task<bool>` -This constructor wires ServerDirectoryService by receiving four dependencies through dependency injection and storing them in private fields. It prepares the service for directory-related operations by providing access to configuration, presence tracking, claim storage, and logging. + +Tries to start the provided `HubConnection` and, on failure, retries with a backoff until the `CancellationToken` is cancelled. It returns `true` if `StartAsync` completes successfully; if the operation is cancelled before a successful start, it returns `false`. ## Remarks -The constructor enforces that ServerDirectoryService cannot operate without configuration, presence tracking, claim storage, and a logger, making its dependencies explicit and testable. It sits at the boundary between configuration and domain logic, coordinating the infrastructure pieces that support directory management. +By isolating this retry logic in `ConnectWithRetryAsync`, the surrounding code can rely on a single, consistent startup strategy for the directory hub. It coordinates the backoff via `GetBackoffDelay`, logs each failure with the upcoming delay, and respects cancellation through the provided `CancellationToken`. ## Notes -- Ensure all dependencies are registered in the application's DI container; missing registrations will cause the service resolution to fail at runtime. -- If any dependency requires specific lifetimes (e.g., scoped vs singleton), align them with the composition root to avoid disposal issues or lifetime mismatches. +- The delay cancellation caveat: if the `CancellationToken` is signaled while `Task.Delay` is awaiting, an `OperationCanceledException` propagates, which means the method would surface cancellation rather than returning `false`. +- Logging: on every failed attempt, a warning is logged with the exception and the upcoming delay. +- Dependency: the retry timing depends on `GetBackoffDelay(attempt)`; callers should ensure this method yields a sensible backoff to avoid long startup times. --- -## UserCountMinInterval +## ReconnectBaseDelay > **File:** `src/EchoHub.Server/Services/ServerDirectoryService.cs` > **Kind:** field ```csharp -private static readonly TimeSpan UserCountMinInterval = TimeSpan.FromSeconds(1) +private static readonly TimeSpan ReconnectBaseDelay = TimeSpan.FromSeconds(2) ``` -Defines a shared, immutable throttling interval used by ServerDirectoryService to regulate how often user-count related work runs. The field is private, static, and readonly, initialized to TimeSpan.FromSeconds(1). This ensures a consistent cadence and avoids magic numbers scattered through the class; it's consulted wherever the service needs to debounce or rate-limit user-count updates. +The `ReconnectBaseDelay` field defines the starting interval used by the service's reconnection logic. As a private static readonly `TimeSpan` initialized with `TimeSpan.FromSeconds(2)`, it provides a single, immutable baseline for calculating backoff delays during reconnect attempts, without exposing the value publicly. Developers thinking about the backoff strategy should consider this constant as the canonical baseline rather than sprinkling literals throughout the codebase. ## Remarks -By centralizing the timing policy in this single member, the class achieves consistent behavior across all usages and simplifies future adjustments. The static readonly combination guarantees that the interval is computed once at type initialization and remains the same for the lifetime of the application, minimizing race-condition risk when read from multiple threads. Because it is private, external callers cannot bypass or alter the cadence; any changes must go through the class logic and a rebuild. +Public exposure is avoided by keeping this value private, but the field still has architectural significance: it centralizes the base delay for the reconnect workflow within `ServerDirectoryService`, ensuring consistent timing across all retry scenarios and simplifying future tuning. ## Notes -- This is not a compile-time constant; it is evaluated at type initialization and cannot be reassigned afterwards. -- External configuration at runtime is not possible unless the class provides a mechanism to override it. -- If cadence needs to vary by environment or load, consider externalizing to configuration or making the interval configurable instead of editing code. +- Changing the private static readonly `TimeSpan` will change the base backoff used by all reconnection attempts in `ServerDirectoryService`; there is no per-call override for this baseline. If configurability is required, expose a parameter or configuration option rather than modifying this field. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md index 514d01b..e88e0e3 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md @@ -7,18 +7,20 @@ ```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 -A[ServerLogsService ReadBacklog] -A --> B[Resolve ServerLogsOptions LogDirectory] -B --> Dir{Directory exists} -Dir -->|Yes| C[Find newest file matching LogFilePattern] -Dir -->|No| E[Return empty list] -C --> F{Newest file found} -F -->|No| E -F -->|Yes| G[Open newest file with FileShare ReadWrite Delete then seek to tail when stream longer than TailReadBytes] -G --> H[Read to end and split by newline into lines] -H --> I[Group lines into LogBacklogEntry with skipLeadingContinuations if seeked and limit from ServerLogsOptions BacklogLines] -I --> J[Return list of LogBacklogEntry] -A -.-> E +ServerLogsService["Start ReadBacklog()"] +ServerLogsOptions["Load ServerLogsOptions (LogDirectory, LogFilePattern, BacklogLines)"] +LogBacklogEntry["Return IReadOnlyList of LogBacklogEntry (backlog or empty)"] + +ServerLogsService -->|"Resolve full path of LogDirectory"| ServerLogsOptions +ServerLogsOptions -->|"If directory does not exist -> return empty list"| LogBacklogEntry +ServerLogsOptions -->|"Find newest file matching LogFilePattern (order by LastWriteTimeUtc)"| ServerLogsService +ServerLogsService -->|"If no newest file -> return empty list"| LogBacklogEntry +ServerLogsService -->|"Open FileStream(newest.FullName, FileMode.Open, FileAccess.Read, FileShare ReadWrite and Delete)"| ServerLogsOptions +ServerLogsOptions -->|"Determine if stream.Length > TailReadBytes (seeked)"| ServerLogsService +ServerLogsService -->|"If seeked -> stream.Seek(-TailReadBytes, SeekOrigin.End)"| ServerLogsOptions +ServerLogsOptions -->|"Read remainder with StreamReader and split into lines"| ServerLogsService +ServerLogsService -->|"Call GroupIntoEntries(lines, skipLeadingContinuations: seeked, BacklogLines)"| LogBacklogEntry +ServerLogsService -->|"On any exception -> return empty list"| LogBacklogEntry ``` ## Contents @@ -37,37 +39,16 @@ public sealed class ServerLogsService ``` -Provides the logic needed to present a read-only "live logs" room: it exposes the room identity, the role-based gate for who may join, and a best-effort reader that returns the most recent log entries from the active rolling Serilog file. Use this service when you need to show a live, read-only backlog of server log entries (rather than storing log lines as chat messages). +Provides utilities for exposing a read-only, live server log room: it knows the room identity and access gate and can read the tail of the current rolling log file into `LogBacklogEntry` items for display. Use `ServerLogsService` when you need to determine whether a channel is the configured logs room, check whether a role may view logs, or retrieve a best-effort backlog snapshot from the most recent log file (rather than relying on persisted messages). ## Remarks -This class centralizes the concerns required for a live log room: determining the configured room name and sender, enforcing the minimum role required to view logs, and extracting a focused backlog from the current log file on disk. It treats the file sink as the single source of truth (Serilog keeps the file open and rolls it), reads only the tail of the newest file up to a bounded byte size, and groups raw lines into logical entries by detecting timestamp-prefixed lines. ReadBacklog is resilient: any I/O or parsing problem yields an empty backlog rather than propagating an error. - -## Example -```csharp -// 'options' is an existing ServerLogsOptions instance configured for the server. -var service = new ServerLogsService(options); - -// Check whether a user role may view/join the live logs room -if (service.CanView(userRole)) -{ - // Read the most recent backlog entries (best-effort; may be empty on error) - var backlog = service.ReadBacklog(); - foreach (var entry in backlog) - { - // LogBacklogEntry exposes a timestamp and the concatenated content - Console.WriteLine($"{entry.Timestamp:O} {entry.Content}"); - } -} - -// Room identity helpers -var isLogs = service.IsLogsChannel("logs"); -var sender = ServerLogsService.SenderName; // "server" -``` +`ServerLogsService` centralizes the concerns around presenting live server logs without persisting log lines as messages. It uses the configured [`ServerLogsOptions`](../../Config/ServerLogsOptions.cs.md) to decide whether logging is enabled, to match a channel name (`NormalizedRoomName`) in `IsLogsChannel`, and to gate access with `CanView` based on `MinRole`. For backlog retrieval, `ReadBacklog` opens the newest file matching `LogFilePattern` in `LogDirectory` with `FileShare.ReadWrite | FileShare.Delete` (to cooperate with a rolling sink like Serilog), reads up to `TailReadBytes` from the file end, and converts raw lines into `LogBacklogEntry` instances via `GroupIntoEntries`. The `GroupIntoEntries` method is public to allow unit testing of the timestamp-based grouping logic. ## Notes -- ReadBacklog swallows all exceptions and returns an empty list on any I/O problem; callers must tolerate an empty backlog as a sign of transient failure or missing files. -- To avoid reading an arbitrarily large file, the reader seeks to the last TailReadBytes bytes; that can start the scan mid-entry, so the grouping logic optionally drops leading continuation lines when the tail was seeked. -- The FileStream is opened with FileShare.ReadWrite | FileShare.Delete because the Serilog file sink typically keeps the file open for writing and may roll it; the service reads concurrently without taking exclusive locks. +- `IsLogsChannel` calls `Trim()` on the provided `channelName`; passing `null` will throw a `NullReferenceException` — callers should ensure they pass a non-null string or guard accordingly. +- `ReadBacklog` is intentionally best-effort: it catches all exceptions and returns an empty list on any I/O or parsing failure. This prevents join failures but can hide filesystem problems; monitor logs or surface errors elsewhere if you need diagnostics. +- The grouping logic depends on lines that start with the timestamp format defined by `TimestampFormat`. If your log sink uses a different timestamp template, `GroupIntoEntries` will treat those timestamped lines as continuations and entries will be merged incorrectly. +- When the newest file is larger than `TailReadBytes`, `ReadBacklog` seeks into the file and sets `skipLeadingContinuations` so a partial entry at the seek boundary is dropped. This is deliberate to avoid presenting truncated entries but means very long single entries near the file end can be partially excluded. --- @@ -87,13 +68,12 @@ public record LogBacklogEntry(DateTimeOffset Timestamp, string Content) | `Content` | `string` | — | -LogBacklogEntry is a tiny, immutable data container that models a single backlog item read from the server log file. It captures the timestamp of the original log line via Timestamp and the associated log text in Content, which may include the initial line plus any continuation lines (such as exception stack traces) that followed it. Use this type when you need to treat a complete backlog segment as a unit, instead of handling raw lines individually; it’s especially helpful for grouping, displaying, or analyzing backlog entries after parsing. +LogBacklogEntry is an immutable value object that captures a backlog entry read from the log file. It consists of a timestamp (`Timestamp`) and the associated content (`Content`), representing the first line of the backlog entry plus any continuation lines (such as exception stack traces) that followed it. ## Remarks -Because LogBacklogEntry is a record, it benefits from value-based equality and concise deconstruction, making it easy to compare backlog entries or to extract the fields in pattern-matching. The Content field holds a multi-line string that includes the initial line and any continuation text that followed it; consumers should be aware that the entry may span multiple lines. This type is commonly produced by the server log reader (e.g., ServerLogsService) when assembling backlog entries from the log file, serving as a stable data carrier between parsing and presentation layers. +Because this is a `record`, it provides value-based equality and deconstruction, which simplifies comparing backlog entries and passing them through the processing pipeline without mutation. It acts as a lightweight data carrier that decouples raw log parsing from higher-level log aggregation or display concerns, allowing the server logs service to operate on coherent chunks of log data. ## Notes -- When collecting backlog lines, ensure that each entry groups the initial timestamped line with its subsequent continuation lines exactly once; splitting or merging entries incorrectly can corrupt the log's temporal grouping. - +- The `Content` may be large and contain newline characters representing multi-line stack traces; treat it as an opaque blob when storing or transmitting. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md index 92297a6..b7e5395 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md @@ -8,12 +8,12 @@ public sealed class ServerLogsSink : ILogEventSink ``` -ServerLogsSink is a Serilog sink that feeds the live log room by buffering log events in a bounded channel and exposing them to the streaming pipeline without persisting them. It enforces a minimum log level, filters out internal sources to avoid feedback loops, and writes accepted events into a single-reader queue consumed by the live broadcast path. This sink thus serves as a lightweight, non-persistent conduit for real-time visibility of logging activity. +ServerLogsSink is a Serilog sink that buffers recent `LogEvent`s into a bounded, single-reader [`Channel<LogEvent>`](../../../EchoHub.Core/Models/Channel.cs.md) and exposes a `Reader` for the live log streaming path. It enforces a minimum level via the `ServerLogsOptions.MinLevel` and filters out internal streaming sources using `ExcludedSourcePrefixes` to prevent a feedback loop where a log would broadcast and re-log itself. ## Remarks -The sink decouples log emission from the live broadcast pathway, providing backpressure via a bounded channel (capacity 512) with drop-oldest semantics to prevent unbounded memory growth. Internal pipeline events are culled by inspecting the SourceContext and excluding known internal prefixes, which prevents the log → broadcast → log feedback loop. By not writing to a database, the component prioritizes timely visibility for operators and clients over long-term auditing. +Serving as a bridge between Serilog and the live log room, `ServerLogsSink` deliberately does not write to a database; events are queued for streaming consumption by [`ServerLogsStreamService`](ServerLogsStreamService.cs.md). The channel is sized with a capacity of 512 and uses `BoundedChannelFullMode.DropOldest` with `SingleReader = true`, which preserves the most recent events while avoiding unbounded memory growth. The internal filtering — checking `Constants.SourceContextPropertyName` and skipping any source that starts with entries in `ExcludedSourcePrefixes` — protects against recursive logging from the streaming infrastructure. ## Notes -- When the channel is full, TryWrite may return false and the log event will be dropped, ensuring the application does not stall due to logging backpressure. -- The channel is configured with SingleReader = true, so there is a single consumer in the streaming path; additional readers would not receive the full event sequence. -- Only events that pass the MinLevel filter and do not originate from excluded internal sources are enqueued for broadcast. \ No newline at end of file +- The bound buffer capacity is 512 and uses `BoundedChannelFullMode.DropOldest`; when full, the oldest buffered events are dropped to make room for newer ones. +- `Emit` uses `TryWrite` and ignores the return value; under load, logs may be dropped if the consumer lags behind. +- Internal sources are excluded by prefix; adding new internal namespaces requires updating `ExcludedSourcePrefixes` to avoid self-logging. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md index 7ffa2b7..fff3b68 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md @@ -8,41 +8,71 @@ public sealed class ServerLogsStreamService : BackgroundService ``` -Streams queued log events to the live log room as ephemeral SignalR messages, never persisting them to the IRC gateway or a database, and with a guard against introducing new logging from the streaming path itself. It runs as a background service, ensuring the destination room exists before sending each event and recreating it if needed, so live viewers can always join the stream without manual intervention. +Description: + +Streams queued log events to the live log room as ephemeral SignalR messages, ensuring the room exists before each publish and recreating it on demand if it was removed. The streaming path is intentionally non-logging to avoid recursive logging and potential message sprawl. Use this service when you want real-time, in-memory broadcasts of server log events to connected clients without persisting those lines to a database. ## Remarks -This symbol acts as a thin, resilient bridge between the server-side log sink and the real-time chat hub. It separates the streaming path from log persistence, enforcing a no-log-from-stream policy to avoid feedback loops where streaming would itself generate more log lines. By lazily resolving the hub context, it avoids tight coupling during service construction and ensures the ChatHub context is available when streaming begins. The class also enforces room existence in a lightweight, interval-bounded way to tolerate transient room removal without blocking the live stream. + +This symbol acts as the dedicated conduit between the server-side log sink and the live chat hub. It coordinates with a channel service to guarantee the existence of the log room (and to recreate it if it disappears), throttling such housekeeping to at most once every 15 seconds to avoid excessive churn. Messages are encrypted before transmission and delivered to the room group via a lazily-resolved `HubContext`, which is intentionally retrieved only after the host has fully configured its DI graph. The static `Format` helper is public for tests, enabling validation of the exact, client-rendered payload without instantiating the streaming pipeline. This separation keeps streaming concerns isolated from the rest of the logging infrastructure and prevents per-event logging from leaking into the stream itself. ## Notes -- The streaming path must never emit logs of its own activity; per-event logging is explicitly suppressed to prevent cascading streams. -- TryEnsureRoomAsync re-checks the room at most once per EnsureInterval (15 seconds) to balance responsiveness with avoiding repeated recreation attempts. -- Messages are formatted and then encrypted before sending; the client receives an encrypted payload and is responsible for decrypting it, mirroring the design that prioritizes privacy and transport safety. The public Format method is exposed for tests, reflecting a desire to validate formatting behavior in isolation. -- If room recreation fails, events are streamed to a group with no members until the next interval, ensuring that the streaming pipeline remains non-blocking and resilient to transient failures. + +- The service reads from `ServerLogsSink.Reader` and, for each event, ensures the destination room exists, formats the log event, encrypts the payload, and sends it to the [`ChatHub`](../../Hubs/ChatHub.cs.md) group corresponding to the room name. +- Room creation/verification is throttled by `EnsureInterval` (15 seconds) to avoid excessive calls during high-frequency log bursts; failed attempts are silently retried on the next interval. +- The streaming path is guarded to swallow non-cancellation exceptions to prevent re-entrancy into the logging pipeline. +- The `Format` method is intentionally public for testability, and truncates messages to `HubConstants.MaxMessageLength` with an ellipsis when necessary. ## Example -- Not included: non-obvious usage from the signature; the behavior is exercised through the background streaming loop and the TryEnsureRoomAsync room-recovery logic. See the source for exact flow and state transitions. + +```csharp +// The example demonstrates formatting a log event for client rendering and ensuring the message is wrapped for transport. +var logEvent = new LogEvent(/* parameters omitted for brevity */); +var payload = ServerLogsStreamService.Format(logEvent); +// payload is then embedded in a [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md), encrypted, and sent to the SignalR hub. +``` + +## Dependencies + +- SignalR, BackgroundService, MessageDto, StringBuilder, TimeSpan, DateTimeOffset, Reader, Guid ## Dependency APIs (verified signatures) + The REAL, parser-verified API surface of this symbol's collaborators: -- MessageDto (src/EchoHub.Core/DTOs/ChatDtos.cs) -- Reader (src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs) -- ServerLogsService (src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs) - - SenderName, RoomTopic, TimestampFormat, TailReadBytes - - ServerLogsService(ServerLogsOptions options) - - Options, IsLogsChannel, CanView(ServerRole) - - ReadBacklog(), GroupIntoEntries(`IReadOnlyList<string>`, bool, int) - - TryParseTimestamp(string, out DateTimeOffset, out string) -- HubContext (src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs) -- HubConstants (src/EchoHub.Core/Constants/HubConstants.cs) - - ChatHubPath, DefaultChannel, IrcConnectionIdPrefix, DefaultHistoryCount, MaxMessageLength - - MaxImageSizeBytes, MaxAudioFileSizeBytes, MaxFileSizeBytes, MaxAvatarSizeBytes - - MaxMessageNewlines, MaxAttachmentsPerMessage, MaxConsecutiveNewlines +- record [`MessageDto`](../../../EchoHub.Core/DTOs/ChatDtos.cs.md) (`src/EchoHub.Core/DTOs/ChatDtos.cs`) +- property `Reader` (`src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs`) +- class [`ServerLogsService`](ServerLogsService.cs.md) (`src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs`) + - field `string SenderName` + - field `string RoomTopic` + - field `string TimestampFormat` + - field `int TailReadBytes` + - `ServerLogsService(ServerLogsOptions options)` + - property `ServerLogsOptions Options` + - `bool IsLogsChannel(string channelName)` + - `bool CanView(ServerRole role)` + - `IReadOnlyList<LogBacklogEntry> ReadBacklog()` + - `IReadOnlyList<LogBacklogEntry> GroupIntoEntries(IReadOnlyList<string> lines, bool skipLeadingContinuations, int maxEntries)` + - `bool TryParseTimestamp(string line, out DateTimeOffset timestamp, out string rest)` +- property `HubContext` (`src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs`) +- class [`HubConstants`](../../../EchoHub.Core/Constants/HubConstants.cs.md) (`src/EchoHub.Core/Constants/HubConstants.cs`) + - field `string ChatHubPath` + - field `string DefaultChannel` + - field `string IrcConnectionIdPrefix` + - field `int DefaultHistoryCount` + - field `int MaxMessageLength` + - field `int MaxImageSizeBytes` + - field `int MaxAudioFileSizeBytes` + - field `int MaxFileSizeBytes` + - field `int MaxAvatarSizeBytes` + - field `int MaxMessageNewlines` + - field `int MaxAttachmentsPerMessage` + - field `int MaxConsecutiveNewlines` - …and 7 more member(s) not shown ## Symbol To Document -- Name: ServerLogsStreamService +- Name: `ServerLogsStreamService` - Kind: class -- File: src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs -- Language: csharp -- ID: fe54ac96-642e-4dbe-af25-3d2559e01299 \ No newline at end of file +- File: `src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs` +- Language: `csharp` +- ID: 24389698-e5ce-4385-b392-f34e08edf31f diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md index 998a5e9..37a8693 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md @@ -8,13 +8,13 @@ public class SignalRBroadcaster : IChatBroadcaster ``` -Implements IChatBroadcaster to deliver chat events to SignalR-connected clients (via an `IHubContext<ChatHub, IEchoHubClient>`). Use this implementation when the application should push messages, presence updates, channel changes and moderation events to SignalR clients; it routes events to groups, specific clients, or all connected SignalR clients as appropriate. +Broadcasts chat events to connected SignalR clients and adapts the generic [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) contract to an `IHubContext<ChatHub, IEchoHubClient>`-backed implementation. Use `SignalRBroadcaster` when you need server-side broadcasting of messages, presence updates, channel lifecycle events and administrative actions to SignalR clients; the class centralizes SignalR-specific delivery details so callers can work with the [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) abstraction. ## Remarks -This class adapts the generic chat-broadcasting contract to SignalR: it resolves an IHubContext lazily from an IServiceProvider and uses the ChatHub/IEchoHubClient surface to send notifications. It cooperates with a PresenceTracker to map channel lists to active SignalR connection ids and intentionally filters out connections belonging to the IRC gateway. The implementation keeps broadcasting logic simple (group vs. all vs. specific clients) and relies on SignalR's client invocation Tasks for async behavior. +`SignalRBroadcaster` resolves and caches an `IHubContext<ChatHub, IEchoHubClient>` lazily from the provided `IServiceProvider`, and uses a [`PresenceTracker`](PresenceTracker.cs.md) to map channels to live connection IDs. It implements the [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) surface by translating high-level events (message send, user joined/left, status changes, channel updates, kicks/bans, deletes, nukes, errors, and forced disconnects) into SignalR calls on `Clients.Group`, `Clients.All`, `Clients.Clients` and `Clients.Client`. The implementation intentionally treats connection IDs that start with the `irc-` prefix as non-SignalR (they are handled by a separate IRC gateway), so several methods either filter those IDs out or no-op for them. ## Notes -- The implementation treats connection IDs prefixed with "irc-" as non-SignalR (IRC gateway) and excludes or ignores those ids in several methods; callers must follow that convention if mixing IRC and SignalR connections. -- SendMessageToChannelAsync intentionally ignores the excludeConnectionId parameter (comment: SignalR clients render their own message echo). For selective exclusion of a SignalR connection use SendUserJoinedAsync (which excludes non-IRC ids) or other targeted methods that call GroupExcept/Clients. -- SendUserStatusChangedAsync will return Task.CompletedTask when no SignalR connections are found for the provided channels — callers should expect no-op behavior in that case. -- HubContext is cached in a private field after first resolution from IServiceProvider; the lazy resolution avoids constructor-time resolution (useful to prevent dependency cycles) and subsequent accesses reuse the same IHubContext instance. \ No newline at end of file +- The `excludeConnectionId` parameter is ignored by `SendMessageToChannelAsync` (the comment in-source explains the IRC exclusion only applies to the IRC gateway because SignalR clients render their own broadcast echo). Callers expecting the exclude behavior for SignalR clients should not rely on it for this method. +- Several methods filter out connection IDs that start with `irc-` (for example `SendUserStatusChangedAsync` and `ForceDisconnectUserAsync`); this convention must be followed by any component that produces or stores mixed connection IDs, otherwise intended recipients may be missed or IRC gateways may receive inappropriate signals. +- `HubContext` is resolved once via `IServiceProvider.GetRequiredService<IHubContext<ChatHub, IEchoHubClient>>()` and cached in a private field. If the application's DI configuration does not provide that service the call will throw at first use; caching avoids repeated resolution but means any change in the resolved instance after first access will not be observed. +- Methods return the `Task` returned by SignalR calls directly; any exceptions thrown by SignalR delivery will propagate to the caller of the [`IChatBroadcaster`](../../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) method. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/SpamGuard.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/SpamGuard.cs.md index afdb799..09a82e0 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/SpamGuard.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/SpamGuard.cs.md @@ -19,17 +19,15 @@ public sealed class SpamGuard ``` -An in-memory, per-user spam protection component used by the server to enforce rate limits and duplicate-content rules across all ingress protocols (SignalR, IRC, etc.). SpamGuard centralizes checks for message sends, channel joins and channel creations so the same limits apply regardless of how a user interacts with the system. Its state is process-local (not persisted) and it delegates configuration to the supplied SpamOptions; the Enabled property exposes whether checks are active. +In-memory, per-user spam protection consulted by the server-side ingress points (for example [`ChatService`](ChatService.cs.md) for messages and joins, and [`ChannelService`](ChannelService.cs.md) for channel creation). Reach for `SpamGuard` when you need a lightweight, process-local policy that enforces rate limits, duplicate-message checks, and simple escalation (auto-mute) without persisting state or inspecting decrypted content. ## Remarks -SpamGuard exists to provide a single place for applying and counting spam-related events so different services (for example ChatService for messages/joins and ChannelService for channel creation) share the same view of a user's recent activity and violations. It keeps compact per-user state (queues of timestamps and a small duplicate-detection cache) and prunes stale entries lazily to avoid unbounded memory growth on busy servers. The class performs all checks under a private lock, so callers do not need to synchronize access; moderators (role >= ServerRole.Mod) are exempted from checks. +`SpamGuard` centralizes cross-protocol ingress throttling so SignalR, IRC, and other entry points share the same limits and violation tracking. State is stored only in-process (the private `_users` dictionary) and is pruned lazily (see `PruneThreshold` and `StaleAfter`) to avoid unbounded growth on busy servers. It operates on content the server already has (so for end-to-end encrypted rooms this is ciphertext) and does not perform decryption. Staff users bypass the guard (`role >= ServerRole.Mod`), rejections are recorded as violations, and repeated rejected messages inside the configured violation window can escalate to `SpamVerdictKind.AutoMute` (the escalation is evaluated only when a message is rejected). ## Notes -- State is process-local and not persisted: restarts or multi-process deployments will reset per-user counters; auto-mutes are recorded in the normal mute store (outside this class). -- Time sources default to DateTimeOffset.UtcNow but can be overridden via the optional nowOverride parameter (useful for deterministic testing). -- Flood detection counts every attempt (including retries), and uses a sliding window configured via SpamOptions (Prune before enqueueing and compare against MaxMessagesPerWindow). -- Duplicate detection compares trimmed, case-insensitive content to the immediately previous message only (back-to-back duplicates); RepeatCount is incremented for consecutive repeats and compared to MaxDuplicateMessages. -- Only rejected actions are recorded as violations for escalation; a non-rejected (clean) message cannot by itself trigger an auto-mute. The escalation logic (violations -> SpamVerdictKind.AutoMute) is evaluated only on rejected messages. +- State is process-local and not persisted: `SpamGuard` does not provide global or cross-instance enforcement. On a multi-server deployment, limits and violation histories are not shared between processes. +- Duplicate detection uses a simple normalization (`Trim()` + `ToLowerInvariant()`): whitespace differences and casing are ignored when comparing `content` to `UserState.LastContent`; `RepeatCount` is reset when normalized content changes. +- All checks run under the internal `Lock` (`lock (_lock)`), so `SpamGuard` is thread-safe but its callers may observe brief blocking under contention; where tests or deterministic timing are needed, use the `nowOverride` parameter to supply a fixed time. --- @@ -53,21 +51,13 @@ public readonly record struct SpamVerdict(SpamVerdictKind Kind, string? Reason = | `MuteDuration` | `TimeSpan` | `default` | -SpamVerdict is an immutable value-type that conveys the outcome of a spam check. It carries a Kind from SpamVerdictKind to describe the verdict, an optional Reason for explaining the result, and a MuteDuration indicating how long to suppress further messages when applicable. Defined as a readonly record struct, it benefits from value-based equality and guarantees immutability across boundaries. A convenient static instance, SpamVerdict.Allowed, represents the common case where a message passes spam checks without penalty. +SpamVerdict is an immutable value-type that conveys the outcome of a spam check. It aggregates the verdict kind (`SpamVerdictKind`), an optional `Reason` for extra context, and a `MuteDuration` that can specify how long to mute the sender when appropriate. A single, shared instance `SpamVerdict.Allowed` is provided for the common case where no action is needed, enabling callers to express acceptance without allocating a new structure. ## Remarks -Because the verdict is packaged as a single object, this abstraction lets the rest of the system reason about spam results without ad-hoc boolean flags scattered through the code. It fits into the SpamGuard workflow by serving as a single, transportable payload that downstream components can inspect via Kind and optionally read the Reason or respect the MuteDuration. The immutability of the type helps prevent accidental mutations once a verdict has been created. - -## Example -```csharp -// Common usage: treat messages as allowed by the spam guard -var verdict = SpamVerdict.Allowed; -``` +SpamVerdict is a `readonly record struct`, which gives it value-based equality, structural deconstruction, and immutability. This design keeps spam-check results small and cheap to pass across boundaries, while centralizing how verdicts are represented and interpreted by the rest of the system. ## Notes -- Reason is nullable; when present, it should be used for diagnostics or logs rather than for control flow. If you need extra context, supply Reason; otherwise leave it null. -- MuteDuration defaults to TimeSpan.Zero. To mute a user or channel for a period, provide a non-zero duration. -- SpamVerdict.Allowed is a convenient singleton for the common allowed case, but it does not encode a Reason or a non-zero MuteDuration. If you need those metadata, construct a new SpamVerdict explicitly. +- The `Reason` is optional; code must account for `null` when presenting or logging context. --- @@ -86,14 +76,12 @@ public enum SpamVerdictKind ``` -SpamVerdictKind enumerates the possible outcomes of a spam evaluation. It is used by enforcement logic to decide whether to allow, reject, or mute a user based on the spam check; AutoMute indicates the caller should apply a timed mute. +The `SpamVerdictKind` enum encapsulates the outcome of a spam policy evaluation performed by the `SpamGuard`. It is used to drive downstream behavior without embedding policy logic in callers: `Allowed` means the action may proceed, `Rejected` means the action is blocked, and `AutoMute` signals that the user has crossed the violation threshold — the caller should apply a timed mute. ## Remarks -By centralizing these verdicts, the codebase can route enforcement consistently without scattering string literals or magic numbers. The AutoMute value communicates a specific consequence (a timed mute) that callers should implement, decoupling the decision from the actual enforcement mechanism. This abstraction helps evolve spam-policy over time while keeping evaluation and enforcement loosely coupled. +This enum separates policy evaluation from enforcement, allowing a single, centralized decision point at the boundary of spam checks. Downstream code can switch on the verdict to implement appropriate behavior; the exact duration and rules of a timed mute are defined elsewhere and are not baked into this type. ## Notes -- AutoMute carries no duration or scope; the enforcement layer must supply the mute length and target(s). -- There is no payload attached to the verdict; if more context is needed (e.g., risk score, user ID), pass it separately alongside the verdict. -- Be mindful when updating this enum; adding values requires updating all readers to handle new cases. +- The duration of an auto mute is not encoded in the enum; callers must resolve duration from configuration or a separate policy engine. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md index f3b83a4..de7aac6 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md @@ -18,16 +18,10 @@ public sealed class ServerStatsCollector ``` -ServerStatsCollector is a thread-safe, in-memory accumulator for server activity counters that have no natural database timestamp (connections, disconnections, kicks, bans, and peak concurrency). It updates counters via lock-free increments and uses a periodic SnapshotAndReset to emit a windowed StatsCounters and prepare the next window, including resetting the peak to the current online count. +The ServerStatsCollector is a thread-safe, in-memory accumulator for server-activity counters that have no natural database timestamp to query after the fact—such as session connects/disconnections, moderation actions, and peak concurrent users. It exposes methods to record connections, disconnections, kicks, and bans, and maintains a running, lock-free estimate of the current peak online. A periodic stats-reporting job calls SnapshotAndReset to atomically capture and reset the window’s counters, seeding the next window’s peak with the provided online count. It is registered as a singleton and is designed to be updated from hot paths like connect/disconnect. ## Remarks - -Because it is registered as a singleton, multiple threads can record events without blocking. The class uses Interlocked and Volatile to implement a lock-free maximum-tracking algorithm for PeakOnline; SnapshotAndReset atomically drains all counters and resets PeakOnline to the provided onlineNow, which defines the starting point for the next window. This design favors low-latency updates in hot paths while deferring aggregation to the reporting window. - -## Notes - -- The next window's PeakOnline baseline is reset to the supplied onlineNow; if that baseline is lower than the actual concurrency at snapshot time, the subsequent peak may undercount. -- SnapshotAndReset resets the per-window counters to zero (except PeakOnline, which is reset to onlineNow); ensure you call it on the cadence that matches your reporting window to align with dashboards. +Architecturally, it provides a low-latency in-memory sink that decouples event counting from persistence, enabling a single, atomic snapshot per reporting window for the server’s activity data. The snapshot resets all counters and optically seeds the next window’s peak with the current online count, maintaining continuity of peak tracking across windows. --- @@ -55,21 +49,14 @@ public readonly record struct StatsCounters( | `PeakOnline` | `int` | — | -StatsCounters is an immutable snapshot of the counters held by ServerStatsCollector. It captures the total connections, disconnections, kicks, bans, and the peak online count at a single moment, enabling safe sharing and logging without mutating the underlying counters. +StatsCounters is an immutable snapshot of the counters held by `ServerStatsCollector`. It records the current values of the counters `Connections`, `Disconnections`, `Kicks`, `Bans`, and the peak online figure `PeakOnline` at the moment of creation. Use this type when you need a read-only view of these statistics or to pass them between components without exposing mutable state. ## Remarks -StatCounters uses a readonly record struct to provide value semantics, meaning two instances with the same values compare equal and it can be passed by value without side effects. It is intended to be produced by the ServerStatsCollector and consumed by telemetry, dashboards, or loggers that need a stable view of current activity. Because it is immutable, readers can snapshot and transport it across threads without additional synchronization concerns. +By design, `StatsCounters` decouples consumers from the mutable internal state of `ServerStatsCollector`, offering a stable, shareable view of statistics. As a `readonly record struct`, it provides value-based equality and cheap copies, ensuring a snapshot can be produced and transported without synchronization concerns. -## Example -```csharp -// Create a snapshot of current counters -var snapshot = new StatsCounters(Connections: 1024, Disconnections: 64, Kicks: 3, Bans: 0, PeakOnline: 128); - -// Deconstruct to access individual values -var (connections, disconnections, kicks, bans, peakOnline) = snapshot; -``` - -## Dependencies -- ServerStatsCollector +## Notes +- This type is immutable; you cannot modify its fields after construction. If you need an updated view, obtain a new `StatsCounters` from the collector. +- Copying a `StatsCounters` instance is cheap because it is a value type, making it safe to pass across threads or components without locking. +- A snapshot reflects the state at the moment it was created; subsequent updates to the collector will not affect already-captured instances. --- \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md index e82ae0b..7696a56 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md @@ -8,12 +8,11 @@ public sealed class ServerStatsReportService : BackgroundService ``` -ServerStatsReportService is a background task that periodically snapshots server activity over the current reporting window, logs the snapshot as pretty-printed JSON (visible in the live server-logs room), and persists a ServerStatsReport to the database for historical trend analysis. The cadence and retention are controlled by StatsOptions; if IntervalHours is non-positive the service uses a 6-hour default. +Background service that periodically snapshots server activity over a rolling window, logs a pretty-printed JSON snapshot (which surfaces in the live server-logs room), and persists the results to the database for historical trends. It reads cadence and retention from [`StatsOptions`](../../Config/StatsOptions.cs.md) and coordinates with [`PresenceTracker`](../PresenceTracker.cs.md), [`ServerStatsCollector`](ServerStatsCollector.cs.md), and [`EchoHubDbContext`](../../Data/EchoHubDbContext.cs.md) to compute windowed metrics such as messages sent, active members, attachments uploaded, and user counts. ## Remarks -To achieve this, the service reads the online user count from PresenceTracker, captures in-memory statistics from ServerStatsCollector, and then opens a scoped EchoHubDbContext to compute metrics such as messages sent, active members, files uploaded, and new users within the reporting window. A fresh DI scope is created per report to ensure proper EF Core lifetimes and isolation between reports. The reporting window is defined by _periodStart and periodEnd to align live counters with database-derived metrics, ensuring the report reflects the same time span across in-memory and persisted data. The pretty-printed JSON log enhances operational visibility by surfacing structured data in the logs. +This symbol acts as an orchestration point between live state, in-memory counters, and durable storage to provide a stable, windowed view of server activity. It builds a [`ServerStatsReport`](../../../EchoHub.Core/Models/ServerStatsReport.cs.md) for each interval and uses a dedicated scope to query [`EchoHubDbContext`](../../Data/EchoHubDbContext.cs.md), ensuring isolation from other requests. By anchoring the window to `_periodStart` and `periodEnd`, it aligns in-memory counters with database-derived counts to avoid drift. ## Notes -- The background loop honors cancellation by awaiting Task.Delay with the provided CancellationToken and catching OperationCanceledException to exit promptly. -- IntervalHours is validated: non-positive values fall back to 6 hours, and the interval is floored at 1 second to avoid a spinning loop. -- Each report uses its own DbContext scope (via _scopeFactory.CreateScope()) to query the database and persist the resulting ServerStatsReport, ensuring clean lifetimes and minimal cross-report contention. \ No newline at end of file +- The interval is computed from `StatsOptions.IntervalHours`; non-positive values default to 6 hours and the interval is clamped to at least 1 second to prevent a runaway loop. +- Restarting the service resets the reporting window; data prior to the restart belongs to the previous period and will not be included in the new interval unless recalculated by the next run. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Services/UserService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Services/UserService.cs.md index 28ea17d..80233f0 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Services/UserService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Services/UserService.cs.md @@ -8,68 +8,31 @@ ```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 RegisterUserAsync"] -Start --> CheckEmpty -CheckEmpty["Check username and password not empty"] -CheckEmpty -->|"missing"| FailMissing -CheckEmpty -->|"present"| CheckUsernameRegex - -FailMissing["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;Username and password are required.#quot;)"] - -CheckUsernameRegex["Validate username with ValidationConstants.UsernameRegex()"] -CheckUsernameRegex -->|"invalid"| FailUsernameRegex -CheckUsernameRegex -->|"valid"| CheckPwdMin - -FailUsernameRegex["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;Username must be 3-50 characters and contain only letters, digits, underscores, or hyphens.#quot;)"] - -CheckPwdMin["Check password length >= 6"] -CheckPwdMin -->|"too short"| FailPwdShort -CheckPwdMin -->|"ok"| CheckPwdMax - -FailPwdShort["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;Password must be at least 6 characters.#quot;)"] - -CheckPwdMax["Check password length <= ValidationConstants.MaxPasswordLength"] -CheckPwdMax -->|"too long"| FailPwdLong -CheckPwdMax -->|"ok"| Normalize - -FailPwdLong["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;Password must not exceed ValidationConstants.MaxPasswordLength characters.#quot;)"] - -Normalize["Normalize username (ToLowerInvariant and Trim)"] -Normalize --> CheckReserved - -CheckReserved["Compare normalized username to UsersController.DeletedUserName"] -CheckReserved -->|"reserved"| FailReserved -CheckReserved -->|"not reserved"| CreateScope - -FailReserved["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;This username is reserved.#quot;)"] - -CreateScope["Create scope and get EchoHubDbContext from _scopeFactory"] -CreateScope --> CheckExists - -CheckExists["Check if db.Users.AnyAsync(u => u.Username == normalizedUsername)"] -CheckExists -->|"exists"| FailAlreadyExists -CheckExists -->|"not exists"| CheckIsFirstUser - -FailAlreadyExists["Return UserOperationResult.Fail(UserError.AlreadyExists, #quot;Username is already taken.#quot;)"] - -CheckIsFirstUser["Determine isFirstUser = !await db.Users.AnyAsync()"] -CheckIsFirstUser -->|"first user"| CreateUser -CheckIsFirstUser -->|"not first"| RegistrationGate - -RegistrationGate["Check RegistrationMode (open / invite / closed)"] -RegistrationGate -->|"closed"| FailRegistrationClosed -RegistrationGate -->|"invite"| TryInvite -RegistrationGate -->|"open"| CreateUser - -FailRegistrationClosed["Return UserOperationResult.Fail(UserError.ValidationFailed, #quot;Registration is closed on this server.#quot;)"] - -TryInvite["Call TryConsumeInviteAsync(db, inviteCode)"] -TryInvite -->|"invite error"| FailInviteError -TryInvite -->|"ok"| CreateUser - -FailInviteError["Return UserOperationResult.Fail(UserError.ValidationFailed, inviteError)"] - -CreateUser["Create new User instance (Id = Guid.NewGuid(), set fields)"] +start["Start RegisterUserAsync"] +start --> checkEmpty["Check username and password not empty"] +checkEmpty -->|"invalid"| emptyFail["Return UserOperationResult.Fail(UserError.ValidationFailed): Username and password required"] +checkEmpty -->|"valid"| regexCheck["Validate username with ValidationConstants.UsernameRegex()"] +regexCheck -->|"invalid"| regexFail["Return UserOperationResult.Fail(UserError.ValidationFailed): Username format invalid"] +regexCheck -->|"valid"| pwMinCheck["Check password length >= 6"] +pwMinCheck -->|"no"| pwMinFail["Return UserOperationResult.Fail(UserError.ValidationFailed): Password must be at least 6 characters"] +pwMinCheck -->|"yes"| pwMaxCheck["Check password length <= ValidationConstants.MaxPasswordLength"] +pwMaxCheck -->|"no"| pwMaxFail["Return UserOperationResult.Fail(UserError.ValidationFailed): Password exceeds max length"] +pwMaxCheck -->|"yes"| normalize["Normalize username (ToLowerInvariant().Trim())"] +normalize --> reservedCheck["If normalized == UsersController.DeletedUserName"] +reservedCheck -->|"yes"| reservedFail["Return UserOperationResult.Fail(UserError.ValidationFailed): This username is reserved"] +reservedCheck -->|"no"| dbScope["Create scope and get EchoHubDbContext"] +dbScope --> existsCheck["If EchoHubDbContext.Users.Any(u => u.Username == normalized)"] +existsCheck -->|"yes"| existsFail["Return UserOperationResult.Fail(UserError.AlreadyExists): Username is already taken"] +existsCheck -->|"no"| isFirstCheck["Determine isFirstUser = !EchoHubDbContext.Users.Any()"] +isFirstCheck -->|"true"| createUser["Create new User entity (new User { ... })"] +isFirstCheck -->|"false"| regMode["Check UserService.RegistrationMode (open, invite, closed)"] +regMode -->|"closed"| closedFail["Return UserOperationResult.Fail(UserError.ValidationFailed): Registration is closed on this server"] +regMode -->|"invite"| inviteTry["Call TryConsumeInviteAsync(EchoHubDbContext, inviteCode)"] +inviteTry -->|"error"| inviteFail["Return UserOperationResult.Fail(UserError.ValidationFailed): invite error returned"] +inviteTry -->|"ok"| createUser +regMode -->|"open"| createUser +createUser --> save["Save new User to EchoHubDbContext and assign roles (ServerRole may apply)"] +save --> success["Return UserOperationResult.Success(UserProfileDto)"] ``` ```csharp @@ -77,28 +40,27 @@ public class UserService : IUserService ``` -Implements user-account operations for the server, most notably account registration. Use this concrete IUserService implementation when you need the server-backed behavior: configuration-driven registration modes (open / invite / closed), automatic owner bootstrap for the very first account, username/password validation, reserved-name checks, and password hashing before persisting users. +Handles user registration and related user-management concerns for the server. Use `UserService` when you need a high-level operation that validates credentials, enforces server-wide registration policy, creates the initial server owner account, hashes passwords, and returns canonical [`UserOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) responses instead of interacting with [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) directly. ## Remarks -UserService is the server-side implementation of IUserService and is responsible for safe, policy-driven user creation. It reads the registration policy from IConfiguration (Server:Registration), uses an IServiceScopeFactory to create a scoped EchoHubDbContext per operation (so the service can be used from different DI lifetimes), and enforces validation rules from ValidationConstants. The very first account created on a fresh database is always promoted to ServerRole.Owner to allow bootstrapping an administration account. Invite consumption (when registration is in "invite" mode) is performed via an atomic/guarded update to avoid races when two registrations attempt to use the last invite simultaneously. +`UserService` encapsulates the rules and side effects required to create a new [`User`](../../EchoHub.Core/Models/User.cs.md) in the application: it validates the `username` with `ValidationConstants.UsernameRegex()`, enforces password length (minimum 6 characters and a maximum of `ValidationConstants.MaxPasswordLength`), normalizes the username to lowercase and trimmed form, prevents use of the reserved `Controllers.UsersController.DeletedUserName`, and checks uniqueness using [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md). The `RegistrationMode` property reads `Server:Registration` from `IConfiguration` and controls whether new sign-ups are allowed (`"open"`), require a valid invite (`"invite"`), or are disallowed (`"closed"`). The very first account created on an empty database is automatically assigned `ServerRole.Owner` to allow server bootstrap. For invite-based registration, `UserService` defers to the private `TryConsumeInviteAsync` routine which (per its comment) performs a guarded update so concurrent registrations cannot both consume the same invite. ## Example ```csharp -// Typical usage from an async context where `userService` is resolved from DI -var result = await userService.RegisterUserAsync("alice", "s3cretP@ss", displayName: "Alice"); +// Given an IUserService instance (e.g. resolved from DI): +var result = await userService.RegisterUserAsync("alice", "s3cret!", displayName: "Alice"); if (result.IsSuccess) { - var profile = result; // UserOperationResult.Success wraps the created UserProfileDto - // proceed with signed-in flow + var profile = result; // result carries the created [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) via `UserOperationResult.Success` + // proceed with login or return profile to caller } else { - // registration failed; map user-visible error to response + // handle failure: message and [`UserError`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) are available from the [`UserOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) returned } ``` ## Notes -- Username handling: the service normalizes usernames by trimming and lower-casing; a specific reserved name (UsersController.DeletedUserName) is rejected. -- The first user bypasses the registration gate and becomes ServerRole.Owner — this is intentional so a fresh server can be bootstrapped. -- Passwords are hashed using BCrypt.Net.BCrypt.HashPassword before being stored; there is no exposed mechanism here to change the hash algorithm. -- Invite consumption uses a guarded database update to prevent two concurrent registrations from both consuming the last available use of a code; if invite validation fails, RegisterUserAsync returns a validation failure with the invite error message. \ No newline at end of file +- The `RegistrationMode` is computed on each access from `IConfiguration["Server:Registration"]`; changing that configuration at runtime affects subsequent calls to `RegisterUserAsync` immediately. +- The first created user bypasses invite/closed checks and is assigned `ServerRole.Owner`; this is intentional to allow initial server bootstrap and means the first successful registration must be protected in deployment scenarios. +- `RegisterUserAsync` normalizes usernames to lowercase and trims them before uniqueness checks, so the system enforces case-insensitive username uniqueness. Passwords are hashed with `BCrypt.Net.BCrypt.HashPassword` before being stored. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md index f6262a2..b5c0ee4 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md @@ -8,49 +8,40 @@ ```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"] -Scope["Create scope and resolve EchoHubDbContext"] -EnsureDefault["Call EnsureDefaultChannelsPublicAsync(EchoHubDbContext)"] -QueryGeneral["Query Channel where Name == HubConstants.DefaultChannel"] -CheckGeneral{"Channel found and Channel.IsPublic == false?"} -MarkPublic["Set Channel.IsPublic = true and save EchoHubDbContext"] -SkipMark["No change"] -MigrateAnsi["Call MigrateAnsiMessagesAsync(EchoHubDbContext)"] -QueryImages["Load messages where Type == MessageType.Image"] -FilterAnsi["Filter messages where Content contains ESC byte"] -CheckCount{"toMigrate.Count == 0?"} -LogFound["Log found messages and prepare migration"] -ConvertLoop["For each message: convert ANSI to color tags and update Content if changed"] -CheckModified{"modified > 0?"} -SaveModified["Save changes to EchoHubDbContext and log migrated count"] -SkipSave["No modifications to save"] -CallEmbed["Call MigrateEmbedJsonToArrayAsync to migrate EmbedDto JSON to array"] -CallAttach["Call MigrateLegacyAttachmentsAsync to migrate Attachment/AttachmentKind"] -CallAdmins["Call EnsureConfiguredAdminsAsync to ensure ServerRole admins configured"] -End["End"] - -Start --> Scope -Scope --> EnsureDefault -EnsureDefault --> QueryGeneral -QueryGeneral --> CheckGeneral -CheckGeneral -->|"Yes"| MarkPublic -CheckGeneral -->|"No"| SkipMark -MarkPublic --> MigrateAnsi -SkipMark --> MigrateAnsi -MigrateAnsi --> QueryImages -QueryImages --> FilterAnsi -FilterAnsi --> CheckCount -CheckCount -->|"Yes"| CallEmbed -CheckCount -->|"No"| LogFound -LogFound --> ConvertLoop -ConvertLoop --> CheckModified -CheckModified -->|"Yes"| SaveModified -CheckModified -->|"No"| SkipSave -SaveModified --> CallEmbed -SkipSave --> CallEmbed -CallEmbed --> CallAttach -CallAttach --> CallAdmins -CallAdmins --> End + Start["DataMigrationService.RunAsync(IServiceProvider)"] + Scope["Create scope and resolve services"] + GetServices["Get EchoHubDbContext, IConfiguration and ILogger"] + + EnsureDefault["Call EnsureDefaultChannelsPublicAsync(db, logger)"] + CheckDefault{"Channel named HubConstants.DefaultChannel exists and IsPublic == false?"} + UpdateDefault["Set Channel.IsPublic = true; await db.SaveChangesAsync(); logger.LogInformation"] + SkipDefault["No change"] + AfterDefault["Continue to next migration"] + + MigrateAnsi["Call MigrateAnsiMessagesAsync(db, logger)"] + LoadImages["Load EchoHubDbContext.Messages where Type == MessageType.Image"] + FilterAnsi["Filter messages where Content contains ESC (0x1B) -> toMigrate list"] + AnsiEmpty{"toMigrate.Count == 0?"} + AnsiProcess["For each message: converted = AnsiToColorTags(Content); if changed set Content and increment modified"] + AnsiSave{"modified > 0?"} + AnsiSaved["await db.SaveChangesAsync(); logger.LogInformation of migrated count"] + + MigrateEmbed["Call MigrateEmbedJsonToArrayAsync(db, logger) - convert legacy embed JSON to EmbedDto array where needed"] + MigrateAttachments["Call MigrateLegacyAttachmentsAsync(db, logger) - migrate Attachment entities to new AttachmentKind/format"] + EnsureAdmins["Call EnsureConfiguredAdminsAsync(db, config, logger) - ensure ServerRole admin users per config"] + End["RunAsync complete"] + + Start --> Scope --> GetServices --> EnsureDefault --> CheckDefault + CheckDefault -- "yes" --> UpdateDefault --> AfterDefault + CheckDefault -- "no" --> SkipDefault --> AfterDefault + + AfterDefault --> MigrateAnsi --> LoadImages --> FilterAnsi --> AnsiEmpty + AnsiEmpty -- "yes" --> MigrateEmbed + AnsiEmpty -- "no" --> AnsiProcess --> AnsiSave + AnsiSave -- "yes" --> AnsiSaved --> MigrateEmbed + AnsiSave -- "no" --> MigrateEmbed + + MigrateEmbed --> MigrateAttachments --> EnsureAdmins --> End ``` ```csharp @@ -58,12 +49,14 @@ public static partial class DataMigrationService ``` -Runs a set of application-level data migrations that update content and small structural pieces of the database when the server starts. Call this during application startup (once) to apply idempotent, content-format migrations such as making the default channel public, converting legacy ANSI art to printable color tags, folding legacy attachment columns into the Attachments table, and other one-off data fixes. +Performs application data migrations that should run at startup. Call `RunAsync(IServiceProvider)` once (for example during application startup) to perform a series of idempotent migrations against the [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md): make the default channel public, convert legacy ANSI color escape sequences to printable color tags, migrate embed JSON to the newer array form, fold legacy single-row attachments into the `Attachments` table, and ensure configured admin users exist. ## Remarks -This class centralizes lightweight, code-driven migrations that operate on row data and content formats rather than schema changes (which belong in EF migrations). Each migration method scopes a DbContext from the provided IServiceProvider and performs targeted, idempotent updates where possible (for example, legacy single-attachment rows are only migrated when no Attachment rows exist). Conversions that change message content (like ANSI→color tags) are deterministic and saved back with SaveChangesAsync. +`DataMigrationService` centralizes small, targeted transformations that evolve persisted chat data between versions. Each migration method (for example, `EnsureDefaultChannelsPublicAsync`, `MigrateAnsiMessagesAsync`, `MigrateEmbedJsonToArrayAsync`, `MigrateLegacyAttachmentsAsync`, and `EnsureConfiguredAdminsAsync`) is written to be safe to run repeatedly: migrated rows are detected and skipped if already-upgraded so the service can be invoked on every startup without duplicating work. The service resolves a scoped [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) (and `IConfiguration`/`ILoggerFactory`) from the provided `IServiceProvider`, performs database changes, and logs what changed. + +The ANSI conversion helper `AnsiToColorTags` is exposed for reuse and relies on a generated regex (`AnsiColorRegex`) to efficiently match 24-bit foreground (`38;2;R;G;B`) and background (`48;2;R;G;B`) color sequences and the reset code (`0`). Matches are transformed to `{F:RRGGBB}`, `{B:RRGGBB}`, and `{X}` respectively. ## Notes -- AnsiToColorTags only recognizes/reset sequences produced as "\x1b[38;2;R;G;Bm", "\x1b[48;2;R;G;Bm" and the reset "\x1b[0m"; other ANSI sequences are left unchanged. -- MigrateAnsiMessagesAsync only examines messages of type Image and checks for the ESC (0x1B) character before attempting conversion, reducing unnecessary work. -- Each migration method calls SaveChangesAsync only when there are actual modifications; however RunAsync itself is asynchronous and can be long-running depending on DB size—callers should await it during startup and avoid calling concurrently. \ No newline at end of file +- `RunAsync` resolves [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md), `IConfiguration`, and `ILoggerFactory` from the provided `IServiceProvider`; ensure those services are registered in DI before calling `RunAsync`. +- `MigrateAnsiMessagesAsync` assumes `Message.Content` is populated (the code calls `m.Content.Contains('\x1b')`). If `Message.Content` can be null in your schema, the migration may throw a `NullReferenceException` — validate non-null constraints or add a null-check before running this migration. +- `AnsiToColorTags` only converts the specific 24-bit RGB sequences (`38;2` and `48;2`) and the reset code (`0`). Other ANSI sequences are left unchanged by design; if older clients used different ANSI sequences they will not be translated by this helper. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md index bf74d26..22e0bf7 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md @@ -8,11 +8,11 @@ public static class DatabaseSetup ``` -DatabaseSetup is a startup bootstrapper that ensures the EchoHub database is ready by applying migrations, seeding a default channel, and running data migrations. It creates a scoped DbContext and logger, migrates the database, seeds a default channel when missing, and then triggers data migrations; if a legacy SQLite database is detected (no migrations history but legacy tables exist), it backs up the current file and recreates the database to enable the modern migration path. +DatabaseSetup is a startup-time orchestration helper that ensures the database is ready for use by applying migrations, seeding initial data, and performing post-migration data transformations. When you call `InitializeAsync` with an `IServiceProvider`, it creates a scoped container, resolves the required [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and a logger from `ILoggerFactory`, and then runs three phases: migrate the database (including legacy-handling) via `MigrateAsync`, seed the default channel via `SeedDefaultChannelAsync`, and finally invoke `DataMigrationService.RunAsync` to apply data migrations such as ANSI-to-color-tag conversions. ## Remarks -DatabaseSetup centralizes the one-time bootstrap concerns for the database, isolating migration, seeding, and legacy-handling logic from the rest of the startup flow. It relies on EF Core’s migration pipeline and coordinates with DataMigrationService to perform data transformations (e.g., ANSI-to-color-tag conversions) and to ensure essential defaults (like the General channel) exist, aligning the persisted state with the application's current expectations. +DatabaseSetup centralizes the startup bootstrap workflow for the database, encapsulating migrations, schema upgrades, seeding, and legacy handling behind a single entry point. It coordinates collaborators like [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) and [`DataMigrationService`](DataMigrationService.cs.md), and uses a scoped `IServiceProvider` so bootstrapping code does not leak scoped lifetimes to the caller. The legacy handling path ensures a clean migration story for older SQLite databases by backing up the file when a legacy schema is detected and then recreating the database with migrations support. ## Notes -- Legacy-path destructive behavior: when a legacy database is detected, the code creates a timestamped backup and then deletes the database so migrations can proceed against a fresh schema. This trade-off is intentional to enable a safe migration path from older schemas. -- Startup-time invocation: the initialization runs at application startup and establishes its own service scope; avoid multiple concurrent invocations to prevent duplicate work or conflicting migrations during a single process lifecycle. \ No newline at end of file +- Legacy backup: If a legacy SQLite database is detected, the code may back up the original file to a path like `{dbPath}.legacy_{timestamp}` before deletion. This preserves a recoverable snapshot when possible. +- Startup failure: If `MigrateAsync` fails, the exception is logged and rethrown, which can cause startup to fail so the issue is addressed before the app runs. diff --git a/docs/auriondocs/Code/src/EchoHub.Server/Setup/FirstRunSetup.cs.md b/docs/auriondocs/Code/src/EchoHub.Server/Setup/FirstRunSetup.cs.md index 2460196..1097bce 100644 --- a/docs/auriondocs/Code/src/EchoHub.Server/Setup/FirstRunSetup.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Server/Setup/FirstRunSetup.cs.md @@ -8,18 +8,13 @@ public static class FirstRunSetup ``` -FirstRunSetup is a small bootstrap utility that guarantees a usable appsettings.json and seeds it with cryptographic secrets on first run. On startup, it copies appsettings.example.json to appsettings.json if the destination is missing, then ensures a valid JWT secret and an encryption key exist by generating them when needed. +FirstRunSetup is a small bootstrap utility that ensures essential configuration exists on the first run of the application. Calling `EnsureAppSettings` will create `appsettings.json` from `appsettings.example.json` if the former is missing, and then guarantee that security-related values are present by generating them when necessary. Specifically, it will ensure the JWT secret at `Jwt.Secret` is non-empty and not a placeholder, and it will ensure an AES encryption key at `Encryption.Key` is present. Generated secrets are cryptographically strong base64 values written back into `appsettings.json`, and progress is reported to the console. ## Remarks -Centralizing this bootstrap logic keeps startup concerns cohesive and makes the secrets generation deterministic and auditable. It relies on cryptographically secure RNG and writes back to the configuration file with indentation for human readability, while tolerating JSON comments during read. - -## Example -```csharp -// Typical usage during application startup -FirstRunSetup.EnsureAppSettings(); -``` +The class centralizes the bootstrapping of critical security configuration, enabling a smooth first-run startup without manual edits. It is designed to be invoked during startup or a dedicated setup routine, populating missing cryptographic material so downstream components can rely on `Jwt.Secret` and `Encryption.Key` being present from the outset. The implementation favors an in-place, file-based approach that aligns with conventional .NET configuration loading, so subsequent code that reads configuration from `appsettings.json` will see the generated values. ## Notes -- Idempotent: existing Jwt.Secret or Encryption.Key are preserved; new values are generated only if missing or marked as CHANGE_ME. -- Path dependency: relies on the current working directory (the project root). In non-standard deployments, you may need to adjust the working directory or extend the helper to accept explicit paths. -- Silent on parse failure: if the JSON cannot be parsed (root is null), the method returns without writing changes. \ No newline at end of file +- IO or JSON parsing/writing operations may throw if the filesystem is inaccessible or the JSON is malformed; there is no explicit exception handling in this bootstrap path. +- The JWT secret is only regenerated if it is missing, empty, or begins with `CHANGE_ME`, preventing accidental overwrites on a healthy existing configuration. +- The encryption key is generated only when the `Encryption.Key` value is absent; existing keys are preserved to avoid needless rotation. +- The logic relies on the current working directory to locate `appsettings.json` and `appsettings.example.json`, so running from an unexpected directory can affect behavior. \ No newline at end of file diff --git a/docs/auriondocs/Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md b/docs/auriondocs/Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md index 0d5627e..7a8c1b6 100644 --- a/docs/auriondocs/Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md +++ b/docs/auriondocs/Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md @@ -22,29 +22,29 @@ internal sealed class FakeChannelService : IChannelService ``` -A lightweight test double that implements IChannelService by returning pre-configured results for each operation. Use this in unit tests to simulate success, failure, or specific payloads from channel-related operations without wiring up real storage or network dependencies. +A test double that implements [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) and lets tests control the results of channel-related operations by setting public properties. Use `FakeChannelService` in unit tests when you need a simple, configurable implementation of [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) without using a mocking framework. ## Remarks -The fake exposes properties you set from tests (for example CreateResult, TopicResult, ChannelListToReturn, MembershipResult, CryptoToReturn, KeyEnvelopeToReturn, ChannelMetaToReturn, SystemChannelToReturn and others). Most methods return Task.FromResult(...) of those properties or a default failure (ChannelOperationResult.Fail(ChannelError.ValidationFailed, "Not configured")) when a result property has not been provided. EnsureSystemChannelAsync returns the configured SystemChannelToReturn or a minimal default ChannelDto (with a new Guid and IsSystem = true) when not configured. +`FakeChannelService` is a lightweight, stateful fake intended for unit tests. Each operation returns the value of a corresponding public property (for example, `CreateResult`, `UpdateTopicResult`, `DeleteResult`), or a sensible default when a property is not set. This makes it easy to simulate success, failure, and edge cases for callers of [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) without wiring up a full service or external dependencies. [`EnsureSystemChannelAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) will return `SystemChannelToReturn` if set; otherwise it constructs a fallback [`ChannelDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) with deterministic fields (a new `Guid`, the supplied `channelName`/`topic`, `DateTimeOffset.UnixEpoch`, and other boolean flags as shown in the implementation). ## Example ```csharp -// Arrange: create the fake and configure the CreateChannelAsync result +// Arrange var fake = new FakeChannelService(); -var created = new ChannelDto(Guid.NewGuid(), "my-channel", "topic", false, 0, DateTimeOffset.UnixEpoch, false, false, false); -fake.CreateResult = ChannelOperationResult.Success(created); +var createdChannel = new ChannelDto(Guid.NewGuid(), "room", null, false, 0, DateTimeOffset.UtcNow, false, false, false); +fake.CreateResult = ChannelOperationResult.Success(createdChannel); -// Act: call the service (synchronously here via Task.Result for brevity in tests) -var result = fake.CreateChannelAsync(Guid.NewGuid(), "my-channel", "topic", isPublic: true).Result; +// Act +var result = await fake.CreateChannelAsync(Guid.NewGuid(), "room", null, true); -// Assert: the configured success is returned +// Assert if (!result.IsSuccess) throw new Exception("expected success"); ``` ## Notes -- The fake uses Task.FromResult and no asynchronous I/O; it's intended only for synchronous-style unit tests and will not reproduce concurrency or latency characteristics of a real implementation. -- If you forget to set a specific "Result" property (e.g. CreateResult, UpdateTopicResult, RekeyResult), the fake returns ChannelOperationResult.Fail(ChannelError.ValidationFailed, "Not configured"). -- EnsureSystemChannelAsync will fabricate a new ChannelDto with a fresh Guid when SystemChannelToReturn is not set; tests that rely on a stable id should explicitly set SystemChannelToReturn. +- The fake exposes mutable public properties; tests must set the appropriate property (for example `CreateResult` or `UpdateTopicResult`) before invoking the corresponding method. Properties are read/write and not thread-safe. +- Several methods return a failure when their result property is unset: operations like `CreateChannelAsync`, [`UpdateTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), [`SetChannelPasswordAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`, and `DeleteChannelAsync` return `ChannelOperationResult.Fail(ChannelError.ValidationFailed, "Not configured")` unless the corresponding property is provided. Tests that expect success must assign a matching [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) first. +- The XML summary suggests the fake "records method calls," but the implementation only exposes configurable return properties and does not record call history. If call-count or argument inspection is required, extend the fake (or use a mocking library) to capture that information. --- @@ -57,34 +57,34 @@ internal sealed class FakeChatService : IChatService ``` -A test double that implements IChatService for use in unit tests. It records calls (connected users, disconnections, joins/leaves, sent messages, status updates) into public lists and returns configurable, pre-seeded results (history, errors, online users, channels). Reach for this when you need a deterministic, inspectable chat service in tests rather than the real implementation. +Fake test double implementing [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) that records every call into in-memory lists and returns configurable, pre-set responses. Use `FakeChatService` in unit or integration tests when you need to assert which [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) methods were invoked or control what the hub/client sees without running a real chat backend. ## Remarks -FakeChatService exists solely to make tests observable and controllable: callers can assert that particular chat operations were invoked by inspecting the public lists, and tests can control what operations return by setting the configurable properties (HistoryToReturn, JoinError, SendMessageError, etc.). It does not perform any real validation or I/O and intentionally records inputs (including join passwords) so tests can verify them. +`FakeChatService` is a combined stub-and-spy: each [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) method either appends a record to one of the public lists (for later assertions) and/or returns the values exposed on its configurable properties. This lets tests both (a) inject specific return values such as `HistoryToReturn`, `JoinError`, `SendMessageError`, `ChannelsForUserToReturn`, and `OnlineUsersToReturn`, and (b) verify side-effects by inspecting `ConnectedUsers`, `DisconnectedConnections`, `JoinedChannels`, `LeftChannels`, `SentMessages`, `StatusUpdates`, and `JoinKeys`. It implements the full [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) surface so it can be passed anywhere the production service is expected without additional shimming. ## Example ```csharp -// Typical usage in a unit test -var svc = new FakeChatService(); - -// Simulate a user connecting -await svc.UserConnectedAsync("conn-1", Guid.NewGuid(), "alice"); -// svc.ConnectedUsers now contains "alice" - -// Simulate joining a channel (password may be null) -var (history, error, passwordRequired) = await svc.JoinChannelAsync("conn-1", Guid.NewGuid(), "alice", "#room", null); -// svc.JoinedChannels contains ("#room", "alice") and svc.JoinKeys contains the password passed - -// Simulate sending a message and configure an error result -svc.SendMessageError = "rate-limited"; -var sendErr = await svc.SendMessageAsync(Guid.NewGuid(), "alice", "#room", "hello world"); -// sendErr == "rate-limited" and svc.SentMessages contains ("#room", "hello world") +// Arrange +var fake = new FakeChatService(); +var sampleMessage = new MessageDto(/* ... */); // construct as needed +fake.HistoryToReturn = new List<MessageDto> { sampleMessage }; +fake.JoinPasswordRequired = true; + +// Act +var joinResult = await fake.JoinChannelAsync("conn-1", Guid.NewGuid(), "alice", "general", "secret"); +await fake.SendMessageAsync(Guid.NewGuid(), "alice", "general", "hello world"); + +// Assert (inspect recorded calls and configured return) +// joinResult.History contains the configured MessageDto +// fake.JoinedChannels contains ("general", "alice") +// fake.JoinKeys contains the supplied password "secret" +// fake.SentMessages contains ("general", "hello world") ``` ## Notes -- The public list properties are mutable and intended for test inspection; tests should reset or recreate the FakeChatService between cases to avoid cross-test contamination. -- JoinChannelAsync records the supplied password into JoinKeys — this test double intentionally captures sensitive inputs for verification, so treat recorded passwords carefully in test logs. -- This implementation makes no concurrency guarantees; if tests exercise the fake from multiple threads you may encounter race conditions. +- `FakeChatService` is not thread-safe: all recorded collections are plain `List<T>` instances and are mutated without synchronization. Reset or recreate the instance between parallel tests. +- Several methods ignore some input parameters: for example, [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) always returns `HistoryToReturn` and does not use the `count` or `offset` arguments; tests relying on real paging behavior will not be exercised by this fake. +- Default behaviors are simple and explicit: [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns `null` by default, [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns whatever `SendMessageError` is set to, and the various `Broadcast*` methods are no-ops. Tests that need side-effects from broadcasts must simulate them explicitly. --- @@ -97,26 +97,15 @@ internal sealed class FakeEncryptionService : IMessageEncryptionService ``` -It is a test double that mimics encrypted content by prefixing plaintext with a fixed marker, allowing tests to verify code paths that handle encrypted data without introducing real cryptography. +FakeEncryptionService is a compact, test-oriented implementation that simulates encryption by prefixing plaintext with a fixed marker. It implements [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) and is intended for test scenarios where deterministic, invertible behavior is enough to exercise encryption flows without pulling in real cryptography. Encrypt("hello") produces `"$ENC$hello"`, and Decrypt("$ENC$hello") returns the original text. If a value to decrypt does not start with the expected prefix, Decrypt simply returns the input unchanged. The nullable helpers `EncryptNullable` and `DecryptNullable` mirror the non-nullable versions, preserving null semantics. The `EncryptDatabaseEnabled` flag is always true in this fake, enabling components that check encryption per database usage to behave consistently in tests. ## Remarks -This internal, sealed class provides a deterministic, reversible "encryption" scheme for testing scenarios that depend on encrypted strings. By implementing IMessageEncryptionService, it enables tests to validate integration points that consume or produce ciphertext without relying on real cryptographic routines. The EncryptDatabaseEnabled property being true signals that encryption should be considered active in the test database layer. Use this class when you need predictable, fast behavior in unit tests that exercise encryption-related code paths. - -## Example -```csharp -var service = new FakeEncryptionService(); -string ciphertext = service.Encrypt("hello"); // "$ENC$hello" -string plaintext = service.Decrypt(ciphertext); // "hello" - -string? nullCipher = service.EncryptNullable(null); // null -string? nullPlain = service.DecryptNullable(null); // null -string? recovered = service.DecryptNullable(ciphertext); // "hello" -``` +Designed as a lightweight test double, this class enforces the [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) contract while avoiding real crypto. It makes the encryption step observable through a constant prefix, enabling tests to locate and verify encrypted payloads, and to simulate database encryption paths via `EncryptDatabaseEnabled`. By keeping it internal and sealed, the implementation is deliberately opaque to prevent accidental misuse outside tests and to preserve a predictable test surface. ## Notes -- This is a fake encryption shim for tests; it is not cryptographically secure. -- Decrypt only removes the prefix if present; non-prefixed content is returned unchanged. -- EncryptNullable/DecryptNullable are null-safe helpers that simplify test code. +- This is a non-secure stub and should never be used for production encryption or storage. +- Because the type is `internal`, it is intended for test code within the same assembly (or a friend-accessible setup). If you need to reference it from production-like tests, ensure appropriate internals visibility is configured. + --- @@ -129,15 +118,35 @@ internal sealed class FakeUserService : IUserService ``` -A lightweight test double of IUserService that records calls and returns pre-configured results. Use this in unit or integration tests when you need to simulate authentication, registration, and profile lookups without exercising the real user backend. +Lightweight test double implementing [`IUserService`](../../EchoHub.Core/Contracts/IUserService.cs.md) that lets tests control return values and observe calls. Set the public properties `AuthResult`, `RegisterResult`, and `ProfileToReturn` to force specific outcomes; inspect `RegisterInviteCodes` to assert which `inviteCode` values were passed to `RegisterUserAsync`. ## Remarks -This class exposes mutable properties (AuthResult, RegisterResult, ProfileToReturn) that callers set to control the behavior of the corresponding IUserService methods. It also records invite codes passed to RegisterUserAsync in RegisterInviteCodes so tests can assert which invite codes were used. The SuccessResult helper creates a typical successful UserOperationResult with a UserProfileDto for convenience; other operations (UpdateProfileAsync, SetAvatarAsync) are intentionally left to always return a NotFound failure to indicate they are not implemented in this fake. +`FakeUserService` exists as an in-memory test helper to avoid exercising real persistence or external systems. All [`IUserService`](../../EchoHub.Core/Contracts/IUserService.cs.md) methods return completed tasks via `Task.FromResult`, so calls are synchronous from the test's perspective and easy to arrange. Use `SuccessResult` to construct a successful [`UserOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) containing a [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) (it sets `UserStatus.Online`, `ServerRole.Member`, and timestamps using `DateTimeOffset.UtcNow`). The class is intended for unit tests where you need deterministic control over authentication/registration/profile responses and simple verification of arguments. + +## Example +```csharp +// Arrange +var svc = new FakeUserService(); +var userId = Guid.NewGuid(); +svc.AuthResult = FakeUserService.SuccessResult(userId, "alice"); +svc.RegisterResult = UserOperationResult.Fail(UserError.AlreadyExists, "already"); + +// Act +var auth = await svc.AuthenticateUserAsync("alice", "pw"); +await svc.RegisterUserAsync("bob", "pw", inviteCode: "INV-123"); + +// Assert +if (!auth.IsSuccess) throw new Exception("expected success"); +// Verify that the invite code passed to RegisterUserAsync was recorded +if (svc.RegisterInviteCodes.Count != 1 || svc.RegisterInviteCodes[0] != "INV-123") + throw new Exception("invite code not recorded"); +``` ## Notes -- The fake is stateful: AuthResult, RegisterResult, ProfileToReturn and RegisterInviteCodes are mutable. Reset or recreate the FakeUserService between tests to avoid cross-test contamination. -- If AuthResult or RegisterResult are left null, AuthenticateUserAsync and RegisterUserAsync return default Fail results (InvalidCredentials and AlreadyExists respectively) with explanatory messages. -- UpdateProfileAsync and SetAvatarAsync always return a NotFound failure ("Not configured"). They are placeholders rather than working implementations. +- `RegisterInviteCodes` is a plain `List<string?>` that records the raw `inviteCode` argument (including `null`) in call order and is not synchronized; concurrent test runs must not share a single instance without synchronization. +- `SuccessResult` populates timestamps using `DateTimeOffset.UtcNow`, so created [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md) instances will have varying timestamp values; avoid strict equality checks against fixed timestamps. +- [`UpdateProfileAsync`](../../EchoHub.Client/Services/ApiClient.cs.md) and `SetAvatarAsync` always return a failure (`UserError.NotFound` with message "Not configured") unless the test replaces these behaviors; they are placeholders rather than functioning update/asset implementations. + --- @@ -150,17 +159,17 @@ internal sealed class TestDuplexStream : Stream ``` -A lightweight in-memory duplex Stream intended for tests: it supplies readable bytes from a preloaded input buffer and captures all written bytes to a separate output buffer that can be inspected. Use this when you need to simulate a readable/writable stream (for example, feeding input to code that reads a Stream and asserting what that code wrote) without interacting with files or the console. +A lightweight in-memory duplex `Stream` intended for tests: it exposes a readable input buffer (populated from the `input` constructor argument) and a separate writable output buffer that callers can inspect via `GetOutput` and `GetOutputLines`. Use `TestDuplexStream` when you need to inject deterministic input into code that reads from a `Stream` and capture what that code writes, without opening real network sockets or files. ## Remarks -This class models a unidirectional read buffer and a separate write buffer so consumers can read a fixed input and concurrently write output without interfering with each other. It intentionally implements only the Stream surface required by simple producers/consumers: reading delegates to an internal MemoryStream created from the provided input string; writing appends to a second MemoryStream. The stream is non-seekable to better emulate pipes or network streams and to discourage tests from relying on seeking behavior. +`TestDuplexStream` intentionally implements only the minimal `Stream` surface needed for typical read/write/flush scenarios in tests. Reads come from the private `_readBuffer` initialized from the constructor `input`, while writes are appended to the private `_writeBuffer` and later returned by `GetOutput`. The class is not seekable (seeking, `Length`, and `Position` throw `NotSupportedException`) because the read and write sides are logically independent buffers rather than a single random-access backing store. The implementation disposes both internal buffers in `Dispose(bool)` so test code should treat a disposed `TestDuplexStream` as unusable. ## Notes -- The stream does not support seeking: Position, Length, Seek and SetLength all throw NotSupportedException. -- GetOutput trims a UTF-8 BOM from the captured bytes because StreamWriter may emit one; tests that rely on raw bytes should use _writeBuffer directly instead of GetOutput (the internal buffer is disposed on Dispose()). -- GetOutputLines splits on the Windows CRLF sequence ("\r\n") and removes empty entries; inputs using only "\n" will not be split by this helper. -- The class is intended for test use and does not provide synchronization; concurrent access from multiple threads is not guaranteed to be safe. - +- `GetOutput` strips a leading UTF-8 BOM (the code calls `TrimStart('\uFEFF')`); that handles writers that emit a BOM but also means a deliberate leading U+FEFF in written data will be removed. +- `GetOutputLines` splits on the literal CR+LF sequence (`"\r\n"`) and uses `StringSplitOptions.RemoveEmptyEntries`, so lone `"\n"` line endings or blank lines may not be handled as callers expect. +- Seeking and length-related members are not supported: `Seek`, `SetLength`, `Position` and `Length` throw `NotSupportedException`. +- Reading consumes the provided input buffer; once `Read`/`ReadAsync` drain the `_readBuffer`, subsequent reads return 0 (end-of-stream) unless a new instance is created. +- The class is a test helper and does not provide synchronization for concurrent callers; concurrent reads/writes from multiple threads are not guaranteed to be safe. --- @@ -173,22 +182,8 @@ internal static class TestIrcConnectionFactory ``` -Creates a convenient factory for constructing IrcClientConnection instances that are wired to an in-memory TestDuplexStream, enabling deterministic unit tests of IRC-related behavior without a real network. Use Create to supply a set of incoming lines that the client will read from; the method returns both the constructed IrcClientConnection and the TestDuplexStream so you can inspect what the client writes. Use CreateAuthenticated to obtain a connection that is already registered and authenticated, with nickname/username and a UserId, so tests can focus on post-auth flow without performing login or handshake. - -## Remarks -TestIrcConnectionFactory centralizes test harness setup, reducing boilerplate in tests that exercise IRC server interaction. It returns both the connection and the stream to let tests feed input and observe output, including greeting lines or protocol messages. The authenticated variant preconfigures identity and flags (IsRegistered and IsAuthenticated) to simulate a fully connected client, enabling tests that assume a ready-to-use session. - -## Example -```csharp -// Example: raw connection with input lines -var (conn, stream) = TestIrcConnectionFactory.Create("PING :server", ":server 001 :Welcome"); - -// Example: authenticated connection -var (authConn, authStream) = TestIrcConnectionFactory.CreateAuthenticated(nickname: "alice"); -``` - -## Notes -- The factory creates a real TcpClient under the hood; the IrcClientConnection uses that client but all I/O is performed via the in-memory TestDuplexStream, so tests must dispose the resources (the stream and client) when finished to avoid leaks. -- CreateAuthenticated defaults nickname to "alice" and generates a new GUID for UserId if none is supplied; pass explicit values for deterministic testing. +TestIrcConnectionFactory is an internal static test helper that creates [`IrcClientConnection`](../../EchoHub.Server.Irc/IrcClientConnection.cs.md) instances backed by a `TestDuplexStream` for unit testing. It provides two entry points: `Create`, which builds a new connection wired to an in-memory duplex stream seeded with the supplied input lines; and `CreateAuthenticated`, which builds a pre-authenticated, registered connection by populating identity fields and authentication flags. The input lines are joined with ` +` and a trailing ` +` is appended if any lines are provided, simulating lines received from an IRC server. The returned tuple gives tests both the [`IrcClientConnection`](../../EchoHub.Server.Irc/IrcClientConnection.cs.md) and the `TestDuplexStream`, enabling observation of outgoing data and control of inbound data. --- \ No newline at end of file diff --git a/docs/auriondocs/README.md b/docs/auriondocs/README.md index 6b11dd0..9437c95 100644 --- a/docs/auriondocs/README.md +++ b/docs/auriondocs/README.md @@ -1,8 +1,8 @@ # HueByte/EchoHub — Documentation -> Continuously generated, source-verified documentation for **HueByte/EchoHub**, built from branch `master` at commit `4dcb480d`. Every page is derived from the code itself and cross-checked against the source before it ships. +> Continuously generated, source-verified documentation for **HueByte/EchoHub**, built from branch `master` at commit `40aea9a0`. Every page is derived from the code itself and cross-checked against the source before it ships. -EchoHub is a client/server chat system that exposes an HTTP API implemented by multiple controllers and a realtime messaging surface via a hub (ChatHub), with client-side components for connecting and playback. The server hosts application services and background workers (e.g. file cleanup, data migrations) that implement business logic and maintenance tasks. Persistent state is stored in the Entity Framework DbContext (EchoHubDbContext) which is used by controllers and services. Clients interact with the server through the ApiClient and implement messaging callbacks against the IEchoHubClient contract. +This repository implements a chat and file-sharing server with a desktop client: server-side controllers expose an HTTP API (e.g. `AuthController`, `UsersController`, `FilesController`) while a real-time hub (`ChatHub`) handles live messaging and presence. Background workers and maintenance tasks run as hosted services (for example `FileCleanupService` and `DataMigrationService`), and persistent state is stored in the Entity Framework DbContext (`EchoHubDbContext`). The desktop client communicates with the server via an API client (`ApiClient`) and includes local services like audio playback and message encryption. ## Start here @@ -14,14 +14,14 @@ EchoHub is a client/server chat system that exposes an HTTP API implemented by m Cross-file guides on the themes that shape this codebase: -- [API client and authentication](Synthesis/api-client-authentication.md) — This guide explains how the EchoHub client performs HTTP operations and manages authentication tokens, and it documents the DTOs the client uses when talking to the server. -- [Attachments and file transfers](Synthesis/attachments-transfer.md) — Outgoing attachments are staged in the UI, packaged as transport objects, and then coordinated through the app orchestrator into the live connection for transmission. -- [Clipboard utilities](Synthesis/clipboard-tools.md) — This guide describes the clipboard-focused utilities in the client: one helper that exposes file-list clipboard contents, another that normalizes image clipboard data into PNG bytes, and the UI entry points that call… -- [Command handling](Synthesis/command-handling.md) — This guide explains how user-entered slash commands move from text input into application behavior and network actions. -- [Encryption and room key management](Synthesis/encryption-roomkeys.md) — End-to-end encryption in the client is implemented as a few focused components: a runtime encryptor that mirrors the server format, a protector that encrypts per-channel room keys at rest, a store that binds persisted… -- [Real-time connection management](Synthesis/real-time-connection.md) — This guide explains how the client-side pieces manage a SignalR-based chat connection, surface server events to the UI, and carry message and attachment DTOs across those boundaries. -- [Theming and UI color management](Synthesis/ui-theming.md) — Theming and UI color management -- [Update management](Synthesis/update-management.md) — Update management +- [API client authentication](Synthesis/api-client-authentication.md) — A short, focused orientation to how the client authenticates to the server and then uses those credentials when making API calls. +- [Attachments transfer](Synthesis/attachments-transfer.md) — This guide describes how the client UI stages user-provided files and how the application constructs the immutable attachment objects that travel with outgoing chat messages. +- [Clipboard utilities](Synthesis/clipboard-tools.md) — This topic covers small, focused helpers that let the UI treat clipboard contents as first-class attachments: one helper extracts file paths from a file-list clipboard, another canonicalizes whatever image bytes are a… +- [Encryption and room keys](Synthesis/encryption-roomkeys.md) — Client-side message confidentiality is implemented in two cooperating pieces: a runtime encryptor that performs AES-256-GCM on outgoing and incoming message payloads, and a storage protector that keeps per-room conten… +- [Real-time connection and messaging](Synthesis/real-time-connection.md) — A compact overview of the client-side real-time layer: how the connection is created, the runtime surface it exposes to higher layers, and the DTOs used to carry chat and channel data. +- [Slash command handling](Synthesis/command-handling.md) — This guide explains how slash-style chat input is parsed and executed across three collaborating components: a parser/dispatcher, an application orchestrator that implements command behavior and UI coordination, and a… +- [UI theming and theme management](Synthesis/ui-theming.md) — This guide explains the client-side theming pieces: the Theme data model, the ThemeManager that provides built-in and user-provided themes and applies them at runtime, and the AppOrchestrator that coordinates UI behav… +- [Update management](Synthesis/update-management.md) — This topic covers the small set of services and the orchestrator that detect available application updates, snapshot state before an update, and hand off the heavy update work so it happens after the Terminal.Gui main… ## Recurring workflows @@ -33,7 +33,7 @@ Step-by-step templates for the patterns this codebase repeats, each grounded in ## By the numbers - **128** documentation pages covering **128** source files -- **598** symbols documented and validated +- **617** symbols documented and validated - **22,133** lines of code analyzed - Languages: C# (128) @@ -45,4 +45,4 @@ Step-by-step templates for the patterns this codebase repeats, each grounded in --- -*Generated by [AurionDocs](https://auriondocs.com) from `master` at commit `4dcb480d` on 2026-07-23 05:56 UTC.* +*Generated by [AurionDocs](https://auriondocs.com) from `master` at commit `40aea9a0` on 2026-07-23 09:35 UTC.* diff --git a/docs/auriondocs/Synthesis/Architecture.md b/docs/auriondocs/Synthesis/Architecture.md index 41b9c47..31da42d 100644 --- a/docs/auriondocs/Synthesis/Architecture.md +++ b/docs/auriondocs/Synthesis/Architecture.md @@ -1,99 +1,93 @@ # Architecture — HueByte/EchoHub -> *Auto-synthesized from 598 documented symbols across 128 files on `master`.* +> *Auto-synthesized from 617 documented symbols across 128 files on `master`.* ## Topic Guides Deep-dives into cross-cutting concerns synthesized from the per-symbol corpus. -- [API client and authentication](api-client-authentication.md) — How the EchoHub client authenticates with the server, handles tokens, and defines authentication DTOs. -- [Theming and UI color management](ui-theming.md) — Representing themes, color palettes, and runtime theme application. -- [Real-time connection management](real-time-connection.md) — Managing the SignalR hub connection lifecycle and connection state. -- [Encryption and room key management](encryption-roomkeys.md) — End-to-end encryption plumbing and secure handling of per-channel room keys. -- [Command handling](command-handling.md) — Slash-command parsing and dispatching command actions from UI and orchestrator. -- [Attachments and file transfers](attachments-transfer.md) — Staging and sending attachments in chat messages and coordinating outbound attachments. -- [Clipboard utilities](clipboard-tools.md) — Helpers for clipboard interactions: files and images. -- [Update management](update-management.md) — Data and update flow: backup prior to updates and update checks. +- [API client authentication](api-client-authentication.md) — How the client authenticates with the server, including login, token refresh, and token usage across API calls. +- [Real-time connection and messaging](real-time-connection.md) — How the client establishes and maintains a real-time connection to the server and handles channel messaging. +- [UI theming and theme management](ui-theming.md) — Theme data models and the system that loads, stores, and applies themes to the UI. +- [Slash command handling](command-handling.md) — Parsing and executing user commands entered as slash commands in chat. +- [Attachments transfer](attachments-transfer.md) — Attachment handling for staged files and outgoing attachments in chat messages. +- [Clipboard utilities](clipboard-tools.md) — Clipboard helpers for files and images used in the UI. +- [Update management](update-management.md) — Checking for updates and backing up state related to updates. +- [Encryption and room keys](encryption-roomkeys.md) — Client-side encryption and per-room key protection for secure messaging. ## Architecture Diagram ```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 - n0["src/EchoHub.Core/DTOs · ChatService (11 files)"] - n1["src/EchoHub.Client/Services · AppOrchestrator (14 files)"] - n2["src/EchoHub.Client/UI · MainWindow (18 files)"] - n3["src/EchoHub.Core/DTOs · ApiClient (10 files)"] - n4["src/EchoHub.Server · User (12 files)"] - n5["src/EchoHub.Core/DTOs · ChannelService (7 files)"] - n6["src/EchoHub.Server · Program (12 files)"] - n7["src/EchoHub.Client/Config (5 files)"] - n8["src/EchoHub.Client/UI · Channel (4 files)"] + n0["src/EchoHub.Client/Services · AppOrchestrator (22 files)"] + n1["src/EchoHub.Client/UI · MainWindow (18 files)"] + n2["src/EchoHub.Core/DTOs · ApiClient (10 files)"] + n3["src/EchoHub.Server · User (13 files)"] + n4["src/EchoHub.Core/DTOs · ChannelService (7 files)"] + n5["src/EchoHub.Server · Program (12 files)"] + n6["src/EchoHub.Core/Contracts · ChannelDto (6 files)"] + n7["src/EchoHub.Client/UI · Channel (4 files)"] + n8["src/EchoHub.Client/UI · UserStatus (3 files)"] n9["src/EchoHub.Core/DTOs · ChannelsController (2 files)"] n10["src/EchoHub.Core/Models (5 files)"] - n0 -->|5| n10 - n0 -->|6| n4 + n0 -->|8| n1 + n0 -->|4| n10 + n0 -->|5| n2 + n0 -->|9| n3 + n0 -->|4| n5 n0 -->|7| n6 - n0 -->|4| n7 + n0 -->|7| n7 n0 -->|6| n8 - n1 -->|14| n0 - n1 -->|5| n2 + n1 -->|4| n0 n1 -->|4| n3 - n1 -->|6| n4 - n1 -->|8| n7 - n1 -->|5| n8 - n2 -->|8| n0 - n2 -->|4| n4 - n3 -->|6| n0 - n3 -->|8| n4 - n3 -->|4| n9 - n4 -->|9| n3 - n5 -->|7| n0 - n5 -->|4| n4 - n5 -->|5| n8 - n6 -->|6| n0 - n6 -->|7| n4 + n2 -->|5| n0 + n2 -->|9| n3 + n2 -->|4| n9 + n3 -->|9| n2 + n4 -->|4| n3 + n4 -->|5| n6 + n4 -->|5| n7 + n5 -->|7| n3 + n5 -->|4| n6 + n6 -->|9| n0 + n6 -->|4| n7 + n6 -->|4| n8 + n9 -->|4| n3 n9 -->|4| n4 - n9 -->|4| n5 ``` ## System Overview -EchoHub is a client/server chat system that exposes an HTTP API implemented by multiple controllers and a realtime messaging surface via a hub (ChatHub), with client-side components for connecting and playback. The server hosts application services and background workers (e.g. file cleanup, data migrations) that implement business logic and maintenance tasks. Persistent state is stored in the Entity Framework DbContext (EchoHubDbContext) which is used by controllers and services. Clients interact with the server through the ApiClient and implement messaging callbacks against the IEchoHubClient contract. +This repository implements a chat and file-sharing server with a desktop client: server-side controllers expose an HTTP API (e.g. `AuthController`, `UsersController`, `FilesController`) while a real-time hub (`ChatHub`) handles live messaging and presence. Background workers and maintenance tasks run as hosted services (for example `FileCleanupService` and `DataMigrationService`), and persistent state is stored in the Entity Framework DbContext (`EchoHubDbContext`). The desktop client communicates with the server via an API client (`ApiClient`) and includes local services like audio playback and message encryption. ## Key Components -**Controllers** — HTTP API surface for client and administrative actions. Implemented by [`AuthController`](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md), [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md), [`FilesController`](../Code/src/EchoHub.Server/Controllers/FilesController.cs.md), [`InvitesController`](../Code/src/EchoHub.Server/Controllers/InvitesController.cs.md), [`ModerationController`](../Code/src/EchoHub.Server/Controllers/ModerationController.cs.md), [`ServerController`](../Code/src/EchoHub.Server/Controllers/ServerController.cs.md), [`UsersController`](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md). +**Controllers** — HTTP API surface for authentication, user and channel management, file operations and moderation. Implemented by [`AuthController`](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md), [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md), [`FilesController`](../Code/src/EchoHub.Server/Controllers/FilesController.cs.md), [`InvitesController`](../Code/src/EchoHub.Server/Controllers/InvitesController.cs.md), [`ModerationController`](../Code/src/EchoHub.Server/Controllers/ModerationController.cs.md), [`ServerController`](../Code/src/EchoHub.Server/Controllers/ServerController.cs.md), [`UsersController`](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md). -**Services** — Application services implement business logic, file handling, background tasks, and utilities used by controllers and hubs. Implemented by [`ChannelService`](../Code/src/EchoHub.Server/Services/ChannelService.cs.md), [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md), [`FileStorageService`](../Code/src/EchoHub.Server/Services/FileStorageService.cs.md), [`FileCleanupService`](../Code/src/EchoHub.Server/Services/FileCleanupService.cs.md), and supporting utilities such as [`AsciiBannerService`](../Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md), [`AudioPlaybackService`](../Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md), [`ClientEncryptionService`](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md), and [`DataMigrationService`](../Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md). +**Services** — Core application logic and long-running background tasks, including channel/chat handling, file storage/cleanup, migrations, and utility services used by client and server. Implemented by [`ChannelService`](../Code/src/EchoHub.Server/Services/ChannelService.cs.md), [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md), [`FileCleanupService`](../Code/src/EchoHub.Server/Services/FileCleanupService.cs.md), [`FileStorageService`](../Code/src/EchoHub.Server/Services/FileStorageService.cs.md), [`DataMigrationService`](../Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md), [`AsciiBannerService`](../Code/src/EchoHub.Core/Services/AsciiBannerService.cs.md), [`AudioPlaybackService`](../Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md), [`ClientEncryptionService`](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md). -**Workers / Hubs** — Real-time messaging and hosted work are provided by SignalR-style hubs and background services; the primary realtime hub is implemented by [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md). +**Workers/Hubs** — Real-time messaging and presence channel for live client-server communication. Implemented by [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md). -**Data Access** — Persistent application state is managed via the Entity Framework DbContext used across services and controllers: [`EchoHubDbContext`](../Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md). +**Repositories / Data Access** — Persistence layer backed by Entity Framework Core; where application state is stored. Implemented by [`EchoHubDbContext`](../Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md). -**External Clients** — Client-side communication with the server is encapsulated by an HTTP/real-time client and the client contract. Implemented by [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) and the client interface [`IEchoHubClient`](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md). - -**Configuration** — Server runtime options and feature flags are defined in option classes used by services and integrations. Implemented by [`IrcOptions`](../Code/src/EchoHub.Server.Irc/IrcOptions.cs.md), [`ServerLogsOptions`](../Code/src/EchoHub.Server/Config/ServerLogsOptions.cs.md), [`SpamOptions`](../Code/src/EchoHub.Server/Config/SpamOptions.cs.md), [`StatsOptions`](../Code/src/EchoHub.Server/Config/StatsOptions.cs.md). - -**IRC integration** — IRC-related command handling and service wiring are provided by IRC support classes and extensions. Implemented by [`IrcServiceExtensions`](../Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md) and [`IrcCommandHandler`](../Code/src/EchoHub.Server.Irc/IrcCommandHandler.cs.md). - -**Core Contracts** — Domain and integration interfaces that define service boundaries and encryption abstractions. Implemented by [`IChannelService`](../Code/src/EchoHub.Core/Contracts/IChannelService.cs.md), [`IChatService`](../Code/src/EchoHub.Core/Contracts/IChatService.cs.md), [`IMessageEncryptionService`](../Code/src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md), and [`IUserService`](../Code/src/EchoHub.Core/Contracts/IUserService.cs.md). +**External integrations** — Client and protocol integrations used to communicate with the server or external systems (IRC). Implemented by [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md), [`IrcServiceExtensions`](../Code/src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md), [`IrcCommandHandler`](../Code/src/EchoHub.Server.Irc/IrcCommandHandler.cs.md), [`IrcOptions`](../Code/src/EchoHub.Server.Irc/IrcOptions.cs.md). ## Component Map *Subsystems below are 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.Client/Services · AppOrchestrator** — 22 documented files - **src/EchoHub.Client/UI · MainWindow** — 18 documented files -- **src/EchoHub.Client/Services · AppOrchestrator** — 14 documented files +- **src/EchoHub.Server · User** — 13 documented files - **src/EchoHub.Server · Program** — 12 documented files -- **src/EchoHub.Server · User** — 12 documented files -- **src/EchoHub.Core/DTOs · ChatService** — 11 documented files - **src/EchoHub.Core/DTOs · ApiClient** — 10 documented files - **src/EchoHub.Core/DTOs · ChannelService** — 7 documented files -- **src/EchoHub.Client/Config** — 5 documented files +- **src/EchoHub.Core/Contracts · ChannelDto** — 6 documented files - **src/EchoHub.Core/Models** — 5 documented files - **src/EchoHub.Client/Themes** — 4 documented files - **src/EchoHub.Client/UI · Channel** — 4 documented files - **src/EchoHub.Server · ServerLogsStreamService** — 4 documented files -- *…and 11 more subsystem folders* +- **src/EchoHub.Client/Services · UpdateBackupService** — 3 documented files +- *…and 10 more subsystem folders* ### Components by Role @@ -144,4 +138,4 @@ EchoHub is a client/server chat system that exposes an HTTP API implemented by m - `IUserService` — `src/EchoHub.Core/Contracts/IUserService.cs` --- -*Generated by Aurion on 2026-07-23 05:56:01 UTC* +*Generated by AurionDocs on 2026-07-23 09:35:48 UTC* diff --git a/docs/auriondocs/Synthesis/Onboarding.md b/docs/auriondocs/Synthesis/Onboarding.md index d425cd3..9a88ca2 100644 --- a/docs/auriondocs/Synthesis/Onboarding.md +++ b/docs/auriondocs/Synthesis/Onboarding.md @@ -2,28 +2,28 @@ > *A curated reading path through this codebase for new contributors. Work through the stops in order.* -This reading path gets a new team member from zero to a place where they can run the app and make a small contribution. Read the short architecture overview first to understand the system's collaboration pattern, then inspect the entry points to see how the pieces are wired; from there follow a single request through the ingress layer into the services and state so you can start making safe, focused changes. +This onboarding reading path gets a new contributor from zero to the point where they can make a small, safe change in the EchoHub codebase. Read these stops in order: start with the generated architecture overview to understand the system shape, open the entry points to see how programs and the server host are configured, follow a request through the ingress layer into the business services, and finish by locating the conventional folders where you should add new controllers, services, or hubs. ## Stop 1: What this project is -At this stop skim the auto-generated system description to learn the overall collaboration pattern and where state is owned; the document also highlights the main components and their responsibilities. Start by opening [Architecture](Architecture.md) to pick up the big-picture boundaries and the primary data stores so later code-level reads map to that conceptual model. +At this stop you will skim the auto-generated system overview to learn the high-level collaboration pattern, the main components, and where persistent state is kept. Read the [Architecture](Architecture.md) document to pick up the generated dependency map and the summary descriptions the project uses to show which services, APIs, and storage pieces are primary. ## Stop 2: Where execution starts -Read the two Program entry points to see how the client and server are bootstrapped, which early runtime concerns are wired, and what cross-cutting services are registered. Inspect the client [Program.cs](../Code/src/EchoHub.Client/Program.cs.md) to see startup tasks like rollback handling, permission checks, configuration provisioning, logging setup, and PATH preparation; then open the server [Program.cs](../Code/src/EchoHub.Server/Program.cs.md) to see how configuration, logging, data access, authentication, service registrations and the ASP.NET Core pipeline are arranged. +This stop shows how the client and server processes are bootstrapped so you can see dependency injection, one-time setup, and host configuration before diving deeper. Open the client [Program.cs](../Code/src/EchoHub.Client/Program.cs.md) to see its CLI handling (the --rollback flag), best-effort Unix execute-permission check, and how it provisions configuration; then inspect the server [Program.cs](../Code/src/EchoHub.Server/Program.cs.md) to see the call to FirstRunSetup.EnsureAppSettings(), the bootstrap logger configuration, and the code path that starts the ASP.NET Core host. ## Stop 3: Where requests come in -Trace a single end-to-end interaction by following the client command entry, the server HTTP controller, and the real-time hub used for chat. Read the client [CommandHandler.cs](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) to learn how user commands are emitted, then the server [UsersController.cs](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) to see the API surface that handles user-related requests, and finally the SignalR [ChatHub.cs](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) to understand real-time message routing and authorization checks. +Trace an incoming action end-to-end by reading the client-side command processor and the server ingress points that handle requests and real-time messages. Examine the client [CommandHandler.cs](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) to learn how client commands are dispatched, the [UsersController](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) to see the HTTP API surface exposed by an [ApiController], and the real-time path via the authorized [ChatHub](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) to understand how authenticated SignalR messages are handled. ## Stop 4: Where the business logic lives -Drill into the substantive services that perform work for the client: network calls, audio, encryption, and backup orchestration. Read the client [ApiClient.cs](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) that manages HTTP requests and disposal, the [AudioPlaybackService.cs](../Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md) that handles playback concerns, the [ClientEncryptionService.cs](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) which implements IMessageEncryptionService for message protection, the [NotificationSoundService.cs](../Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md) for user-facing alerts, and the [UpdateBackupService.cs](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) which is involved in BackupInfo serialization and backup flows. +Follow the workhorses invoked by the ingress layer: the HTTP/SignalR handlers call into these services to perform the real operations. Read the client-side [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) (a disposable HTTP client wrapper), [AudioPlaybackService](../Code/src/EchoHub.Client/Services/AudioPlaybackService.cs.md) for playback responsibilities, [ClientEncryptionService](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) which implements IMessageEncryptionService for message-level encryption, [NotificationSoundService](../Code/src/EchoHub.Client/Services/NotificationSoundService.cs.md) for UI sounds, and [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) which participates in backup serialization (JsonSerializable for BackupInfo). ## Stop 5: Where state lives -Look at the code that owns connection state, persisted backups, and the commands that drive application state changes. Revisit [CommandHandler.cs](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) to understand the commands that mutate client state, inspect [ConnectionManager.cs](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) for the lifecycle and disposal of live connections, open [UpdateBackupService.cs](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) to see how BackupInfo is serialized for persistence, and check the UI [ConnectDialog.cs](../Code/src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md) to learn where connection information is captured and handed off to the connection manager. +Identify the concrete types that own runtime and persisted state so you know what to change when you add data or lifecycle concerns. Revisit [CommandHandler.cs](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) for command-driven client state transitions, inspect the connection lifecycle in the internal sealed [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) (IAsyncDisposable), see how UI-driven folder selection is encapsulated in the static [NativeFolderPicker](../Code/src/EchoHub.Client/Services/NativeFolderPicker.cs.md), and review [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) for how BackupInfo is serialized for persistence. ## Stop 6: Where to put new code -Use the conventional places represented by controllers, server services, and hubs when deciding where to add features or fixes. For HTTP and auth-related endpoints add or update controllers like [AuthController.cs](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md); server-side domain operations belong in services such as [ChannelService.cs](../Code/src/EchoHub.Server/Services/ChannelService.cs.md) (which implements IChannelService); and real-time or cross-connection behavior belongs in the SignalR hub [ChatHub.cs](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md). +Learn the conventional locations to add controllers, services, and hubs by looking at existing examples in the server surface. The server exposes authentication endpoints in [AuthController](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md) (an [ApiController]), long-running or domain behavior belongs in services such as [ChannelService](../Code/src/EchoHub.Server/Services/ChannelService.cs.md) which implements IChannelService, and real-time endpoints belong in hubs like the authorized [ChatHub](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md). ## Next steps -Run the app locally: read the two [Program.cs](../Code/src/EchoHub.Server/Program.cs.md) and [Program.cs](../Code/src/EchoHub.Client/Program.cs.md) files to learn how to start the server and client, then launch both projects and use the Connect dialog to exercise the [ChatHub](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) path. +Try this as your first contribution: read the [Architecture](Architecture.md) overview, run the dev server from the server [Program.cs](../Code/src/EchoHub.Server/Program.cs.md) entry point, and make a tiny change (for example, add a log line in [ChatHub](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md)) to verify your local build and run loop. --- -*Synthesised by Aurion on 2026-07-23 05:54:49 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:34:36 UTC* diff --git a/docs/auriondocs/Synthesis/Workflows/controller.md b/docs/auriondocs/Synthesis/Workflows/controller.md index defcb1b..6443bc7 100644 --- a/docs/auriondocs/Synthesis/Workflows/controller.md +++ b/docs/auriondocs/Synthesis/Workflows/controller.md @@ -2,10 +2,12 @@ > *Workflow template auto-derived from 7 existing exemplar(s).* -This template describes how to add a new HTTP controller to the server: reach for this pattern when you need a new API surface implemented as an [ApiController] class that exposes routes and actions. Use the reference controller below as the concrete shape to copy (attributes, base class, constructor injection, and action patterns), and consult the existing examples to match naming and placement. +Adding a new controller in this codebase means adding an ASP.NET Core API controller class under src/EchoHub.Server/Controllers that follows the shape shown in the reference FilesController. A developer would reach for this pattern when they need to expose a new HTTP API surface: controllers are decorated with controller attributes, derive from ControllerBase, and implement actions (HttpGet/HttpPost/etc.) that the framework routes to. ## Reference implementation +Real code from src/EchoHub.Server/Controllers/FilesController.cs that you can model a new controller on: + ```csharp [ApiController] [Route("api/files")] @@ -69,11 +71,11 @@ public class FilesController : ControllerBase ## Where it lives -Controllers in this codebase are placed under src/EchoHub.Server/Controllers, and exemplar files use names such as AuthController.cs, ChannelsController.cs, FilesController.cs, InvitesController.cs, ModerationController.cs, ServerController.cs, and UsersController.cs with corresponding public classes named AuthController, ChannelsController, FilesController, InvitesController, ModerationController, ServerController, and UsersController. Use that same folder and naming pattern when adding a new controller file. +Controllers in this project appear under src/EchoHub.Server/Controllers and use the Controller naming form (for example FilesController, AuthController, ChannelsController, etc.). Each controller is a class that carries the [ApiController] attribute and derives from ControllerBase; routing is provided with [Route("...")] on the class and action attributes like [HttpGet] on methods. ## Wiring -A specific registration site for controllers was not detected in the symbol graph provided. Inspect the existing controllers listed below to see how they are referenced in the project and to follow the same runtime usage patterns used by the application. +A registration/composition site for controllers was not detected in the provided wiring list. To see how controllers are used and how their action surface looks in practice, inspect the existing controllers listed in "Existing examples" below and model your new controller on those files. ## Existing examples @@ -86,4 +88,4 @@ A specific registration site for controllers was not detected in the symbol grap - [`UsersController`](../../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) --- -*Synthesised by Aurion on 2026-07-23 05:55:15 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:35:00 UTC* diff --git a/docs/auriondocs/Synthesis/Workflows/service.md b/docs/auriondocs/Synthesis/Workflows/service.md index d822f6a..8c768f3 100644 --- a/docs/auriondocs/Synthesis/Workflows/service.md +++ b/docs/auriondocs/Synthesis/Workflows/service.md @@ -2,14 +2,10 @@ > *Workflow template auto-derived from 8 existing exemplar(s).* -Adding a new service - -When you need to encapsulate a piece of server functionality—either a long-lived background job or an application service consumed by controllers and other services—you add a new service type in this codebase. Use the existing service types in src/EchoHub.Server/Services as your models: pick a clear name that ends with "Service", place the source alongside the other services, and wire it up where services are registered. +When you need to add a new application service or a long-running background task to EchoHub.Server, add a new type alongside the existing services and register it where services are composed. The examples in src/EchoHub.Server/Services show both ordinary services and BackgroundService-based hosted tasks; model a new instance on those concrete types and then wire it into the application startup. ## Reference implementation -Real code from `src/EchoHub.Server/Services/MuteExpirationService.cs` that a new instance can be modelled on: - ```csharp /// <summary> /// Background service that periodically unmutes users whose timed mute has expired. @@ -73,11 +69,11 @@ public sealed class MuteExpirationService : BackgroundService ## Where it lives -Service source files are placed in src/EchoHub.Server/Services. Existing service types include names such as ChannelService, ChatService, FileCleanupService, FileStorageService, LinkEmbedService, MessageEncryptionService, MuteExpirationService, and ServerDirectoryService; each service file in that folder defines the corresponding type (for example, public class ChannelService : IChannelService and public sealed class FileCleanupService : BackgroundService). Follow the same placement and name your new type with a Service suffix so it sits alongside these exemplars. +Create the new service type under src/EchoHub.Server/Services. The repository contains multiple service types in that folder whose type names end with "Service", for example ChannelService, ChatService, FileCleanupService, FileStorageService, LinkEmbedService, MessageEncryptionService, MuteExpirationService, and ServerDirectoryService. ## Wiring -Detected registration/composition site: src/EchoHub.Server/Program.cs. Inspect that file to see how services from src/EchoHub.Server/Services are registered and how hosted/background services are added to the application; new service types should be wired there consistent with the existing registrations. +Registration and composition of services was detected in src/EchoHub.Server/Program.cs. Add the new service's registration in that file alongside the existing service registrations; inspect src/EchoHub.Server/Program.cs and the exemplars to follow the same wiring approach used for other services. ## Existing examples @@ -91,4 +87,4 @@ Detected registration/composition site: src/EchoHub.Server/Program.cs. Inspect t - [`ServerDirectoryService`](../../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) --- -*Synthesised by Aurion on 2026-07-23 05:55:34 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:35:20 UTC* diff --git a/docs/auriondocs/Synthesis/api-client-authentication.md b/docs/auriondocs/Synthesis/api-client-authentication.md index 47d2604..52255a6 100644 --- a/docs/auriondocs/Synthesis/api-client-authentication.md +++ b/docs/auriondocs/Synthesis/api-client-authentication.md @@ -1,54 +1,29 @@ -# API client and authentication +# API client authentication -> How the EchoHub client authenticates with the server, handles tokens, and defines authentication DTOs. +> How the client authenticates with the server, including login, token refresh, and token usage across API calls. -This guide explains how the EchoHub client performs HTTP operations and manages authentication tokens, and it documents the DTOs the client uses when talking to the server. It focuses on the client-side [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) as the central point for login/refresh/logout and common API operations, and the small set of DTOs and client callback interface the ApiClient consumes and produces. Read this when you need to understand which types carry credentials and tokens, how attachments and avatar uploads are represented, and where server-initiated events are delivered on the client. +A short, focused orientation to how the client authenticates to the server and then uses those credentials when making API calls. The three files described below show a single HTTP façade that owns token state and many API operations ([ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md)), plus the small immutable DTOs that carry credentials and message/attachment metadata between the client and server. Read these together to understand the runtime flow: sign in -> store tokens -> refresh when needed -> attach tokens to requests; and how message attachments are represented when uploaded or downloaded. ## ApiClient.cs -Implements token management and API calls to the EchoHub server. +Performs login, token refresh, and authenticated API calls. -The [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) class is a high-level HTTP client that centralizes authentication lifecycle (LoginAsync, LoginWithRefreshTokenAsync, RefreshTokenAsync, LogoutAsync, SetTokens) and exposes token state via properties like Token, RefreshToken, and ExpiresAt. It provides helper methods for authenticated requests (AuthenticatedRequestAsync, AuthenticatedGetAsync, EnsureAuthenticated, GetValidTokenAsync) and common server operations surfaced to callers: channel and message management (CreateChannelAsync, DeleteChannelAsync, SendMessageWithAttachmentsAsync, DeleteMessageAsync, RekeyChannelAsync, NukeChannelAsync), moderation actions (AssignRoleAsync, BanUserAsync, KickUserAsync, MuteUserAsync, UnbanUserAsync, UnmuteUserAsync), profile and upload flows (UploadAvatarAsync, DownloadFileToTempAsync, UpdateProfileAsync, ExportMyDataAsync, DeleteMyAccountAsync), and utilities for handling file content types (GetContentType). The ApiClient implements IDisposable (Dispose) and contains response handling helpers (EnsureSuccessAsync) so callers get a single, managed surface for HTTP/authorization concerns. According to its relationships it depends on the DTO definitions in [AuthDtos](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md), [ChatDtos](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md), [ModerationDtos](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md), and [ProfileDtos](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md); in practice the ApiClient serializes and deserializes instances of those DTOs when calling corresponding endpoints and when returning structured results to its callers. +The [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) is a sealed, disposable HTTP façade that centralizes authentication state (access token, refresh token, and expiration) and exposes the concrete operations the UI or other client code calls. The doc lists properties and members such as `BaseUrl`, `Token`, `RefreshToken`, `SetTokens`, and lifecycle helpers like `Dispose`, plus auth-focused methods `LoginAsync`, `LoginWithRefreshTokenAsync`, `RefreshTokenAsync`, and `GetValidTokenAsync` — these are the explicit entry points for establishing and renewing credentials. For making requests it provides `AuthenticatedRequestAsync` and `AuthenticatedGetAsync` (and `EnsureAuthenticated` / `EnsureSuccessAsync`) to attach the current token and validate responses; higher-level API operations are implemented as methods like `SendMessageWithAttachmentsAsync`, `DownloadFileToTempAsync`, `UploadAvatarAsync`, and many channel/user management calls (e.g., `CreateChannelAsync`, `BanUserAsync`, `AssignRoleAsync`). Within this topic the `ApiClient` depends on the DTO types defined in the other files to marshal request and response payloads (see relationships: depends on ChatDtos.cs, AuthDtos.cs) and therefore hands off typed payloads like `LoginRequest`/`LoginResponse` and `AttachmentDto` when calling the server. ## AuthDtos.cs -Defines login request data structure used to authenticate. +Defines the login response DTO used by the API client. -The [AuthDtos](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) file defines the transport types used by the authentication endpoints: the immutable positional [LoginRequest](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) record carrying Username and Password, the [LoginResponse](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) record that bundles Token, RefreshToken, ExpiresAt and basic user identity fields, plus a [RefreshRequest](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) and [RegisterRequest](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md). These DTOs are pure data containers (no business logic) intended to be serialized over HTTP; the documentation calls out that Password is sensitive and that LoginResponse is what clients consume to establish an authenticated session. The ApiClient uses these DTOs when performing login and token-refresh flows (see relationships: used by ApiClient.cs). - -## AuthDtos.cs (LoginResponse) -Represents server response after authentication including tokens. - -The [LoginResponse](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) record is the structured server reply to a successful authentication, containing the short-lived Token, the RefreshToken, an ExpiresAt timestamp, and identifying fields like Username with optional display personalization. Clients (like the [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md)) consume LoginResponse to populate their in-memory token state and to drive expiration/refresh logic; because it contains the expiry moment, consumers can decide when to call RefreshTokenAsync or LoginWithRefreshTokenAsync instead of issuing unauthenticated requests. - -## AuthDtos.cs (RefreshRequest) -Represents refresh token request for renewing authentication. - -The [RefreshRequest](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) is the DTO used to request new authentication tokens from the server using a refresh token. It is the lightweight, immutable payload the ApiClient will serialize when it invokes its refresh endpoint (RefreshTokenAsync / LoginWithRefreshTokenAsync) so the server can validate the refresh token and return a new [LoginResponse](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md). - -## IEchoHubClient.cs -Interface for EchoHub client surface used by ApiClient to perform operations. - -The [IEchoHubClient](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) interface defines the callback surface that a client implementing the real-time hub must provide: methods such as ReceiveMessage(MessageDto), UserJoined(channelName, username, UserPresenceDto?), UserLeft, ChannelUpdated(ChannelDto), UserStatusChanged, UserKicked, UserBanned, MessageDeleted, ChannelDeleted, ChannelNuked, ForceDisconnect, and Error. The doc shows example minimal implementations that log or handle these events quickly and non-blockingly. While the ApiClient handles HTTP and token management, this interface is the typed contract used by any hub/transport layer to deliver server-initiated events to client code; the relationship shows IEchoHubClient depends on DTO types like those in [ChatDtos](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) and [ProfileDtos](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md), which are delivered through these callbacks. +This file contains small immutable records that model the authentication payloads the client sends and receives. Notably, `LoginRequest(string Username, string Password)` packages credentials for `LoginAsync` calls, and `LoginResponse(string Token, string RefreshToken, DateTimeOffset ExpiresAt, string Username, string? DisplayName, string? NicknameColor)` is the typed response carrying the `Token`, `RefreshToken`, and `ExpiresAt` values that the [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) stores and uses to authorize subsequent requests. There are also `RefreshRequest` and `RegisterRequest` records for refresh and registration flows; these DTOs are value objects (records) intended for transport only and are the direct inputs/outputs used by ApiClient methods like `LoginAsync`, `RefreshTokenAsync`, and `LoginWithRefreshTokenAsync` as the source of truth for token state. ## ChatDtos.cs -`AttachmentDto` collaborates directly with `ApiClient` and other members of this topic (10 dependency links). - -The [ChatDtos](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) file defines message and channel payloads used across both HTTP API and hub callbacks. In particular, the [AttachmentDto](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) record carries Kind (AttachmentKind), Url, FileName, FileSize, and an optional AsciiPreview; it represents a message attachment's metadata and is the shape ApiClient sends or receives when uploading, downloading, or rendering attachments. Other DTOs in the same file (MessageDto, ChannelDto, ChannelMetaDto, SendMessageRequest, SendUrlRequest, ReplyRefDto, etc.) are the structured inputs and outputs ApiClient uses for channel operations and that appear on the [IEchoHubClient](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) callbacks. The docs note an important detail: in end-to-end encrypted channels the content behind the Url (and previews) may be ciphertext opaque to the server, which affects how clients process the Url returned in AttachmentDto. - -## ModerationDtos.cs -`AssignRoleRequest` collaborates directly with `ApiClient` and other members of this topic (4 dependency links). - -The [ModerationDtos](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md) file provides small, immutable payloads for moderation actions; the [AssignRoleRequest](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md) record carries a Username and a ServerRole value and is intended to be sent to moderation endpoints to request a role change. The file also contains BanRequest, KickRequest, and MuteRequest records used for banning, kicking, and muting operations. The ApiClient serializes these DTOs when invoking its moderation methods (AssignRoleAsync, BanUserAsync, KickUserAsync, MuteUserAsync), so moderation actions are expressed as data objects across the HTTP boundary. - -## ProfileDtos.cs -`AvatarUploadResponse` collaborates directly with `ApiClient` and other members of this topic (4 dependency links). +`AttachmentDto` collaborates directly with `ApiClient` and other members of this topic (8 dependency links). -The [ProfileDtos](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) file defines small user-profile payloads used by profile/update and avatar upload endpoints. The [AvatarUploadResponse](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) record holds AvatarAscii, the ASCII-art representation returned after an avatar upload; ApiClient's UploadAvatarAsync returns or deserializes this DTO so callers can display or store the ASCII preview. UpdateProfileRequest and UpdateStatusRequest are optional-field records used for partial profile updates and are the payloads ApiClient will send via UpdateProfileAsync. +`ChatDtos.cs` defines the message- and channel-related transport shapes that the [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) consumes and returns. The `AttachmentDto(AttachmentKind Kind, string Url, string FileName, long FileSize, string? AsciiPreview = null)` record encapsulates an attachment's metadata: a retrieval `Url`, `FileName`, `FileSize`, and optional `AsciiPreview`. The file also contains `SendMessageRequest`, `SendUrlRequest`, `ChannelDto`, `ChannelMetaDto`, `MessageDto`, `UserDto`, and `ChannelCryptoDto` among others; these records are the concrete payloads `ApiClient` methods accept and return for operations such as `SendMessageWithAttachmentsAsync`, `SendUrlAsync`, `GetChannelMetaAsync`, and `DownloadFileToTempAsync`. In practice the `AttachmentDto.Url` is the link the client will follow (via `DownloadFileToTempAsync`) to retrieve an attachment and the structured send requests are the bodies used by the ApiClient when posting messages or creating channels. How the pieces fit -The ApiClient is the HTTP façade: it consumes and produces the DTOs in AuthDtos, ChatDtos, ModerationDtos, and ProfileDtos when calling server endpoints and populating client state. Authentication flows center on the LoginRequest/LoginResponse/RefreshRequest DTOs and ApiClient methods that set and refresh Token/RefreshToken and expose helpers like GetValidTokenAsync and EnsureAuthenticated. Separately, real-time server-to-client events are delivered through the [IEchoHubClient](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) callback interface using the same Chat and Profile DTOs, keeping transport and event handling decoupled while the ApiClient handles request/response semantics and token lifecycle. +The runtime collaboration is straightforward: the [ApiClient](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) is the orchestrator that holds token state emitted by the auth DTOs (e.g., [LoginResponse](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md)). Callers invoke `LoginAsync`/`LoginWithRefreshTokenAsync` to obtain or restore that state, `GetValidTokenAsync`/`RefreshTokenAsync` to keep it current, and the client then uses `AuthenticatedRequestAsync`/`AuthenticatedGetAsync` to attach the access token to calls. For message and file operations the ApiClient sends and receives the chat records from [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) — for example, `AttachmentDto` conveys the `Url` and metadata that `DownloadFileToTempAsync` and `SendMessageWithAttachmentsAsync` operate on — so DTOs remain passive carriers while ApiClient implements the network and auth behavior that uses them. --- -*Covers 8 of 8 source files identified for this topic.* +*Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:50:51 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:30:19 UTC* diff --git a/docs/auriondocs/Synthesis/attachments-transfer.md b/docs/auriondocs/Synthesis/attachments-transfer.md index 7bb614b..7d7ffdf 100644 --- a/docs/auriondocs/Synthesis/attachments-transfer.md +++ b/docs/auriondocs/Synthesis/attachments-transfer.md @@ -1,41 +1,31 @@ -# Attachments and file transfers +# Attachments transfer -> Staging and sending attachments in chat messages and coordinating outbound attachments. +> Attachment handling for staged files and outgoing attachments in chat messages. -Outgoing attachments are staged in the UI, packaged as transport objects, and then coordinated through the app orchestrator into the live connection for transmission. This topic shows the small set of types and methods that carry file streams and metadata from the MainWindow staging UI through AppOrchestrator into the connection layer so they can be uploaded (optionally encrypted) and surfaced as attachment DTOs in messages. +This guide describes how the client UI stages user-provided files and how the application constructs the immutable attachment objects that travel with outgoing chat messages. It explains the UI surface that users interact with, the small data carrier used to represent a prepared attachment, and the orchestrator that connects the two when a send or clear action occurs. Read this to quickly locate the methods you'll call to stage files, build upload payloads, and clean up temporary paste artifacts. -## OutgoingAttachment.cs -Represents an attachment queued for sending to a channel or user. +## MainWindow +Stage and manage file attachments in the chat input. -The [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) record is the in-process transport object used to carry a single file stream and its filename through the sending pipeline. It declares four properties: the raw Stream and FileName (required), and two optional strings DeclaredKind and EncryptedPreview which are intended for end-to-end encrypted scenarios. As a record it provides value-based equality for tracking/deduplication but notably does not manage the Stream lifetime — callers open and dispose streams around instances of this type. In this topic it is produced/consumed by the orchestrator layer (see [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md)). +The [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) type is the UI surface for composing messages and managing staged attachments. Its documented members include explicit input- and attachment-focused operations such as SetStagedAttachments and StageFiles (for adding files from disk or paste), ClearAll and HandleCmdClearAttachments-related flows, plus many UI helpers (FocusInput, UpdateInputTitle, UpdateInputReadOnly) that keep the compose area in sync. Per the file relationships, MainWindow is used by [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md); the orchestrator drives MainWindow to display or clear staged attachments and reacts to user commands emitted from the window. -## MainWindow.cs -Provides UI hooks for staging attachments and displaying progress. +## OutgoingAttachment +Represents attachments prepared for sending with messages. -The [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) component exposes the user-facing hooks that allow files to be staged and progress or status to be shown. Among its many members are StageFiles (to accept user-selected files) and SetStagedAttachments (to update the UI with the current list of staged items), plus UI update methods such as UpdateSpinner/UpdateInputTitle to reflect in-progress operations. MainWindow depends on the message/attachment DTO types in [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) for rendering metadata and is called by [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) when orchestrated work (prepare/send/clear attachments) must update the UI. +The [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) record is a compact, immutable data carrier containing a Stream and the original FileName plus two optional fields: DeclaredKind and EncryptedPreview. As a record it provides value-based equality so attachments can be compared or deduplicated as they move through the pipeline. The DeclaredKind/EncryptedPreview pair is used to carry presentation/encryption metadata for end-to-end encrypted channels, while normal (non-encrypted) sends typically populate only Stream and FileName. The file is consumed by the orchestrator when preparing payloads for transmit. -## AppOrchestrator.cs -Builds outbound attachments and coordinates sending operations. +## AppOrchestrator +`AppOrchestrator` collaborates directly with `OutgoingAttachment` and other members of this topic (2 dependency links). -The [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) owns the high-level send flow: it implements BuildOutgoingAttachmentAsync to assemble outbound attachment payloads (creating [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) instances), provides cleanup helpers such as CleanupPastedTempFiles, and contains command handlers like HandleCmdSendFile and HandleCmdClearAttachments that respond to user actions. It depends on the DTO types in [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) when preparing message payloads and coordinates with the UI by reading staged files from and writing status back to [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md). For transmission the orchestrator delegates connection and delivery responsibilities to the connection layer ([ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md)). - -## ConnectionManager.cs -`ConnectionManager` collaborates directly with `AppOrchestrator` and other members of this topic (4 dependency links). - -The [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) encapsulates the live chat connection lifecycle: authentication, optional end-to-end key fetching, and instantiation/wiring of the SignalR hub connection. It exposes a thin event surface so UI code (principally [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md)) can subscribe to SignalR events without dealing with SignalR details, and it implements IAsyncDisposable so the orchestrator can tear down network resources cleanly. ConnectAsync (documented in the file) reports progress via a provided onStatus callback, treats failure to obtain an E2E key as non-fatal, and returns compound results (the internal [ConnectResult](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) record) that include login, channel list, and histories for the orchestrator to use. - -## ChatDtos.cs -`AttachmentDto` collaborates directly with `AppOrchestrator` and other members of this topic (4 dependency links). - -The [AttachmentDto](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) is the immutable transport representation of an attachment that travels with messages: it records the attachment Kind, a Url where the resource can be retrieved, FileName, FileSize, and an optional AsciiPreview used for character-art rendering. The DTO is the canonical metadata shape used across UI, API, and connection boundaries; the orchestrator uses these DTO types when composing or processing message payloads, and the MainWindow reads them to render attachments in the UI. In end-to-end encrypted channels the DTO’s Url and AsciiPreview may represent ciphertext that the server cannot interpret. +The [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) mediates between UI actions and the attachment/send logic. Notable documented members include BuildOutgoingAttachmentAsync (the builder that produces an [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) from a file/clipboard source), CleanupPastedTempFiles (removes temporary files created when pasting), and explicit command handlers such as HandleCmdSendFile and HandleCmdClearAttachments. AppOrchestrator depends on [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) to reflect staged attachments in the UI and to respond to user-driven events; it constructs the immutable OutgoingAttachment values and manages lifecycle concerns (downloads, ensuring room unlocked for send, and cleanup). How the pieces fit -- UI staging: users pick files via [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md). MainWindow.StageFiles and SetStagedAttachments hold the files and show progress to the user while AppOrchestrator drives the workflow. -- Packaging: [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) constructs [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) records (via BuildOutgoingAttachmentAsync), cleans up temp files, and maps to the DTO shapes from [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) when preparing messages. -- Delivery: the orchestrator delegates network work to [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md), which manages connection/auth/E2E keys and forwards events so the UI and orchestrator can report progress and completion. +- MainWindow is the UI owner of staged files: it exposes StageFiles, SetStagedAttachments, ClearAll and other composition helpers so users can add, view, and remove attachments before sending. +- AppOrchestrator listens for UI commands, calls BuildOutgoingAttachmentAsync to turn staged input into an [OutgoingAttachment](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md), and invokes the send/download/cleanup flows (including CleanupPastedTempFiles) as needed. +- OutgoingAttachment is the immutable transport object passed from the orchestrator into the send pipeline; optional DeclaredKind and EncryptedPreview carry E2EE-specific metadata when applicable. --- -*Covers 5 of 5 source files identified for this topic.* +*Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:53:19 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:32:36 UTC* diff --git a/docs/auriondocs/Synthesis/clipboard-tools.md b/docs/auriondocs/Synthesis/clipboard-tools.md index 9425ba1..8c90da3 100644 --- a/docs/auriondocs/Synthesis/clipboard-tools.md +++ b/docs/auriondocs/Synthesis/clipboard-tools.md @@ -1,31 +1,29 @@ # Clipboard utilities -> Helpers for clipboard interactions: files and images. +> Clipboard helpers for files and images used in the UI. -This guide describes the clipboard-focused utilities in the client: one helper that exposes file-list clipboard contents, another that normalizes image clipboard data into PNG bytes, and the UI entry points that call those helpers to stage attachments or consume images. +This topic covers small, focused helpers that let the UI treat clipboard contents as first-class attachments: one helper extracts file paths from a file-list clipboard, another canonicalizes whatever image bytes are available into a PNG, and the window UI calls them when the user pastes or triggers clipboard-driven actions. The helpers centralize platform differences (Windows, Linux, macOS fallbacks) and intentionally present a simple success/failure API so the UI can degrade gracefully when clipboard content or platform support is missing. ## ClipboardFiles.cs -Clipboard file utilities for handling file lists. +Provides clipboard file handling utilities. -The [ClipboardFiles](../Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md) static class provides a single, platform-aware API (exposed via TryGetFiles) to read file paths when the OS clipboard contains a file-list. It hides OS-specific handling—on Windows it reads CF_HDROP with a short retry loop to tolerate clipboard contention, on Linux it uses text/uri-list through wl-paste or xclip—and it performs existence checks and filters out non-file entries so callers receive only existing paths. TryGetFiles returns true only when at least one valid file path is found, otherwise false, allowing callers to fall back if no usable file-list is present; this class is consumed by the UI layer ([MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md)). +The [ClipboardFiles](../Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md) type is a static helper that exposes a single, simple consumption pattern: call TryGetFiles to ask the OS clipboard for a list of file paths. TryGetFiles returns true only when one or more existing file paths are discovered; it filters out non-existent or malformed entries and returns false on platforms that don’t support a file-list clipboard or when no valid paths are present. Internally the helper normalizes platform differences (CF_HDROP on Windows, text/uri-list on Linux using command-line helpers) and logs exceptions rather than throwing, so callers receive a boolean+list result they can act on without having to catch clipboard-specific exceptions. In the app this helper is consumed by the UI layer: the [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) calls into ClipboardFiles.TryGetFiles to obtain file paths to be staged or attached. ## ClipboardImage.cs -Clipboard image utilities for copying images to the clipboard. +Provides clipboard image utilities. -The [ClipboardImage](../Code/src/EchoHub.Client/Services/ClipboardImage.cs.md) static class exposes TryGetPng to extract whatever image is currently on the OS clipboard and return it as PNG-encoded bytes suitable for saving, embedding, or transmitting. It normalizes multiple clipboard image formats: it prefers a native PNG clipboard format to preserve alpha, and falls back to platform bitmaps (CF_DIB on Windows) by wrapping DIB bytes in a minimal BMP header and decoding/re-encoding to PNG via DibToPng; malformed DIB input yields null and TryGetPng surfaces that as a failure (false). TryGetPng routes to OS-specific helpers, logs errors rather than throwing, and returns false on unsupported platforms or on failure; [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) depends on this helper to obtain clipboard image bytes. +The [ClipboardImage](../Code/src/EchoHub.Client/Services/ClipboardImage.cs.md) static class exposes TryGetPng to produce a canonical PNG byte array from whatever image representation the OS clipboard currently holds. TryGetPng dispatches platform-specific work to methods such as TryGetWindows, TryGetLinux, or TryGetMacOS based on runtime OperatingSystem checks, preserves native PNG clipboard bytes when present (using PngMagic or platform-registered formats), and converts other formats — notably DIB/CF_DIB on Windows — by wrapping the DIB in a minimal BMP and using an image loader to re-encode as PNG via the DibToPng helper. The API favors robustness: all clipboard- and image-decoding exceptions are logged and swallowed so callers get a simple true/false outcome, and the docs call out platform and threading caveats (for example, STA requirements on Windows and header validation for DIB inputs). The [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) uses ClipboardImage.TryGetPng when it needs a pasteable PNG payload from the clipboard for staging or insertion. ## MainWindow.cs `MainWindow` collaborates directly with `ClipboardFiles` and other members of this topic (2 dependency links). -The [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) UI class defines a large set of interactive behaviors and a handful of members that interact with the clipboard: notably methods named StageFiles, SetStagedAttachments and GuardedClipboardAction appear in its surface. Per the documented relationships, MainWindow delegates platform specifics to the clipboard helpers: it invokes [ClipboardFiles](../Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md).TryGetFiles to obtain file paths copied by the user and then uses its own staging APIs (SetStagedAttachments/StageFiles) to prepare those paths for attachment. Likewise, MainWindow can call [ClipboardImage](../Code/src/EchoHub.Client/Services/ClipboardImage.cs.md).TryGetPng to obtain a normalized PNG byte array when the user has copied an image, allowing the UI to save, embed, or attach that image without per-OS handling. GuardedClipboardAction provides a place to centralize error handling and UI feedback around those clipboard calls so failures from the helpers (they return false rather than throwing) can be handled gracefully. +The [MainWindow](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) source defines the interactive UI surface and numerous event handlers and helpers related to input, message lists, and clipboard interactions. Of particular relevance to this topic are methods such as GuardedClipboardAction, StageFiles, SetStagedAttachments, and CopyToClipboard: GuardedClipboardAction is the safe wrapper for performing clipboard operations (honoring the helpers’ failure semantics), StageFiles and SetStagedAttachments are the paths by which file lists or image bytes obtained from the clipboard are moved into the UI’s pending-attachment state, and CopyToClipboard implements copy behavior the UI exposes. When a paste or clipboard-driven accept occurs, MainWindow calls into the clipboard helpers — invoking [ClipboardImage](../Code/src/EchoHub.Client/Services/ClipboardImage.cs.md).TryGetPng to request a PNG payload or [ClipboardFiles](../Code/src/EchoHub.Client/Services/ClipboardFiles.cs.md).TryGetFiles to obtain file paths — and then uses its staging methods to present those attachments to the rest of the UI or to the send/attach pipeline. How the pieces fit -- The two service classes encapsulate platform-specific clipboard concerns: [ClipboardFiles] returns a filtered list of existing file paths or false; [ClipboardImage] returns a PNG byte array or false. -- [MainWindow] orchestrates user-facing clipboard flows: it calls those helpers from StageFiles/SetStagedAttachments and related clipboard actions, then integrates the results into the message-composition and attachment UI. -- The helpers favor returning a simple success/failure result (and normalized data) so the UI can decide whether to stage attachments, embed image bytes, or fall back to alternative input methods. +MainWindow is the orchestrator: on paste or clipboard actions it uses GuardedClipboardAction to safely call the two helpers and translate their boolean+payload results into staged attachments (files or PNG bytes). The clipboard helpers isolate platform differences and error handling so the window code only needs to check success/failure and process the returned paths or bytes. This keeps clipboard I/O contained in small, testable utilities while the window code focuses on user flow and attachment lifecycle. --- *Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:53:51 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:33:02 UTC* diff --git a/docs/auriondocs/Synthesis/command-handling.md b/docs/auriondocs/Synthesis/command-handling.md index 1950154..a6b8585 100644 --- a/docs/auriondocs/Synthesis/command-handling.md +++ b/docs/auriondocs/Synthesis/command-handling.md @@ -1,46 +1,53 @@ -# Command handling +# Slash command handling -> Slash-command parsing and dispatching command actions from UI and orchestrator. +> Parsing and executing user commands entered as slash commands in chat. -*Figure: How Command handling works.* +*Figure: How Slash command handling 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'}}}%% sequenceDiagram participant Client -participant ConnectionManager_cs as ConnectionManager.cs -participant AppOrchestrator_cs as AppOrchestrator.cs -participant CommandHandler_cs as CommandHandler.cs -Client->>ConnectionManager_cs: UI sends slash command -ConnectionManager_cs->>AppOrchestrator_cs: forward command to orchestrator -AppOrchestrator_cs->>CommandHandler_cs: invoke command parsing and dispatch -CommandHandler_cs-->>AppOrchestrator_cs: return parsed action/result -AppOrchestrator_cs->>ConnectionManager_cs: dispatch action / send response -ConnectionManager_cs-->>Client: deliver response to UI +participant ConnectionManager +participant AppOrchestrator +participant CommandHandler + +Client->>ConnectionManager: Send slash command +activate ConnectionManager +ConnectionManager->>AppOrchestrator: OnSlashCommandReceived +activate AppOrchestrator +AppOrchestrator->>CommandHandler: ParseAndExecute(command) +activate CommandHandler +CommandHandler-->>AppOrchestrator: ExecutionResult +deactivate CommandHandler +AppOrchestrator-->>ConnectionManager: Response(result) +deactivate AppOrchestrator +ConnectionManager-->>Client: Send response +deactivate ConnectionManager ``` -This guide explains how user-entered slash commands move from text input into application behavior and network actions. It describes the parsing and event surface (the command-to-event bridge), the central orchestrator that implements command handlers and coordinates UI-side concerns, and the connection manager that owns the live SignalR connection and performs the network work the orchestrator requests. +This guide explains how slash-style chat input is parsed and executed across three collaborating components: a parser/dispatcher, an application orchestrator that implements command behavior and UI coordination, and a connection manager that exposes server and SignalR events. Read this to understand which file performs parsing, which one implements the command actions and UI glue, and which one owns the network and lifecycle concerns so you can correctly subscribe handlers and marshal events to the UI thread. ## CommandHandler.cs -Parses and executes chat commands; determines if input is a command. +Parses and executes slash commands from the chat input. -The [CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) class is the input-to-event bridge: it recognizes whether a text input is a slash command (via IsCommand) and runs a suite of HandleXxx parsing routines (for example HandleSetStatus, HandleSendAction, HandleCreateInvite, HandleExportData and many others listed in the source). It does not perform side effects itself; instead it exposes one event per supported command (OnSetStatus, OnSendAction, OnCreateInvite, OnExportData, etc.) and raises asynchronous events after parsing. The class also contains parsing helpers and semantics notes (status handling, StripQuotes, IsValidHex, ParsePathAndSizeFlag) so subscribers can depend on a consistent interpretation of user input. According to the topic relationships, this component is consumed by the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md), which subscribes to those events to implement behavior. +The [CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) class is the parser and event-based dispatcher for any string that looks like a slash command. Its primary entry is HandleAsync which analyses the incoming text, maps it to one of many command handlers (the file lists HandleAvatar, HandleBan, HandleSend, HandleSetStatus, HandleJoin, etc.), raises the corresponding asynchronous On... events (consumer-provided Func<..., Task> handlers), and returns a CommandResult describing success, usage, or error. CommandHandler also includes parsing helpers such as IsCommand, IsValidHex, StripQuotes and ParsePathAndSizeFlag and exposes small helpers like StatusUsage and HandleDownloadPath so callers can rely on consistent argument parsing. Relationship: App code (the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md)) consumes CommandHandler by subscribing to its events so that parsed commands are executed by the orchestrator's handlers rather than by the parser itself. ## AppOrchestrator.cs -Central coordinator handling command-related actions and user commands across the app. +Wires command events to UI and coordinates command handling with the app lifecycle. -The [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) wires the command parsing surface into application behavior: it subscribes to the events emitted by the [CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) and implements the concrete handlers named in the source (a large set of HandleCmd* methods such as HandleCmdSetStatus, HandleCmdSendFile, HandleCmdJoinChannel, HandleCmdCreateInvite, HandleCmdExportData, HandleCmdKickUser, HandleCmdNukeChannel, etc.). It also owns UI-side responsibilities like BuildOutgoingAttachmentAsync, EnsureRoomUnlockedForSendAsync, CleanupPastedTempFiles, pending reply management, and resource cleanup (Dispose). Per its relationships the orchestrator depends on both [CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md) for parsing and [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) for performing network operations; the source shows it translating parsed commands into calls and requests that drive the connection layer. The file also documents many small, focused flow steps (ApplyAsciiSize, HandleChannelSelected, HandleEditProfile, etc.) that adapt command intent into concrete application actions. +The [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) implements the concrete behavior for the commands exposed by the parser: it defines a large set of HandleCmd* methods (for example HandleCmdSetStatus, HandleCmdSendFile, HandleCmdJoinChannel, HandleCmdKickUser, HandleCmdCreateInvite, HandleCmdExportData and many more) plus UI-oriented helpers (MainWindow, BuildOutgoingAttachmentAsync, DownloadAttachmentAsync, ApplyAsciiSize, AsciiSizeLabel, CleanupPastedTempFiles). In practice the orchestrator subscribes the CommandHandler events to these HandleCmd* methods so that when the parser raises an On... event the orchestrator performs the actual action, updates UI state, manages attachments and download paths, and enforces room locking or permission checks (for example EnsureRoomUnlockedForSendAsync). Relationship: AppOrchestrator depends on the parser ([CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md)) to receive parsed commands and on the connection layer to execute server-facing actions; it wires command events into UI flows and uses ConnectionManager to carry out network operations. ## ConnectionManager.cs `ConnectionManager` collaborates directly with `AppOrchestrator` and other members of this topic (4 dependency links). -The [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) owns the full lifecycle of a live chat connection: authentication and token handling, attempting to fetch and apply end-to-end encryption keys, instantiating and wiring the EchoHub (SignalR) connection, tracking which channels are joined, and forwarding SignalR callbacks as simple .NET events the UI can subscribe to. It exposes ConnectAsync semantics (reporting progress via an onStatus callback and throwing on authentication failure) and implements IAsyncDisposable so callers can call DisposeAsync to tear down the hub and underlying ApiClient. The file also defines the [ConnectResult](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) record (Login, Channels, Histories) that packages the login response, joined channels list, and message histories returned by ConnectAsync. Notes in the source call out important behaviors: failures to fetch encryption keys are non-fatal, forwarded events may arrive on background threads, and callers (principally the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md)) must handle marshal-to-UI-thread concerns. +The [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) is the single place that manages the server connection lifecycle: it performs authentication (via the API client referenced in the docs), attempts to fetch and apply end-to-end encryption keys, constructs and registers handlers on the hub connection, and tracks channel membership state. It exposes high-level events forwarded from the underlying hub (MessageReceived, UserJoined, ChannelUpdated, ConnectionStatusChanged and similar) so callers like the orchestrator can subscribe without binding SignalR handlers directly. Important operational notes surfaced by the doc: ConnectAsync reports progress through an onStatus callback and will throw on authentication failure, its event callbacks may run on SignalR threads so UI code must marshal to the UI thread, and the manager implements IAsyncDisposable so callers should await disposal to release connection and API resources. Relationship: AppOrchestrator uses ConnectionManager to perform server actions and to observe incoming runtime events; ConnectionManager is therefore the network-facing collaborator the orchestrator relies on. How the pieces fit -User input flows into [CommandHandler](../Code/src/EchoHub.Client/Commands/CommandHandler.cs.md), which parses text and emits a focused event per command. [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) subscribes to those events and implements the HandleCmd* methods that translate parsed intent into application actions and requests; when a command requires network interaction, AppOrchestrator delegates to [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md). ConnectionManager manages the SignalR connection and returns results or raises network events back to the orchestrator, while AppOrchestrator handles UI concerns (attachments, pending replies, local state) and coordinates lifecycle and cleanup. +CommandHandler is the stateless parser/dispatcher that turns raw slash input into event invocations. AppOrchestrator subscribes to those events and implements the actual command semantics, UI updates, and attachment/download flows. ConnectionManager centralizes authentication, E2E key application, hub creation and SignalR event forwarding so AppOrchestrator can call into the network layer and react to server-originated events without handling low-level connection details. --- *Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:52:46 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:32:15 UTC* diff --git a/docs/auriondocs/Synthesis/encryption-roomkeys.md b/docs/auriondocs/Synthesis/encryption-roomkeys.md index 3a46d3a..80e3015 100644 --- a/docs/auriondocs/Synthesis/encryption-roomkeys.md +++ b/docs/auriondocs/Synthesis/encryption-roomkeys.md @@ -1,79 +1,24 @@ -# Encryption and room key management +# Encryption and room keys -> End-to-end encryption plumbing and secure handling of per-channel room keys. +> Client-side encryption and per-room key protection for secure messaging. -*Figure: How Encryption and room key management 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'}}}%% -sequenceDiagram -participant ConnectionManager -participant ClientConfig -participant RoomKeyProtector -participant RoomKeyStore -participant ClientEncryptionService - -ConnectionManager->>ClientConfig: Load AccountPreset -ClientConfig->>RoomKeyProtector: Initialize/Acquire protector -RoomKeyProtector-->>ClientConfig: Protector instance - -ConnectionManager->>RoomKeyStore: Initialize RoomKeyStore -RoomKeyStore->>ClientConfig: Read AccountPreset/config -ClientConfig-->>RoomKeyStore: Config data - -RoomKeyStore->>RoomKeyProtector: Protect/Unprotect room keys -RoomKeyProtector-->>RoomKeyStore: Protected/Decrypted key - -ConnectionManager->>ClientEncryptionService: Register IMessageEncryptionService -ClientEncryptionService-->>ConnectionManager: Encryption service ready - -ConnectionManager->>RoomKeyStore: Request room key for channel -RoomKeyStore->>RoomKeyProtector: Decrypt room key -RoomKeyProtector-->>RoomKeyStore: Plain room key -RoomKeyStore-->>ConnectionManager: Return room key - -ConnectionManager->>ClientEncryptionService: Encrypt/Decrypt message with room key -ClientEncryptionService-->>ConnectionManager: Encrypted/Decrypted payload -``` - -# Encryption and room key management - -End-to-end encryption in the client is implemented as a few focused components: a runtime encryptor that mirrors the server format, a protector that encrypts per-channel room keys at rest, a store that binds persisted server entries to an in-memory cache, and a connection manager that wires those pieces into the live SignalR connection. This guide explains what each file actually implements, how they call each other, and where responsibilities (in-memory keys, persisted protected keys, and message-level cryptography) are split. +Client-side message confidentiality is implemented in two cooperating pieces: a runtime encryptor that performs AES-256-GCM on outgoing and incoming message payloads, and a storage protector that keeps per-room content keys encrypted on disk. The runtime service expects a 32-byte key (provided as base64) and emits self-contained ciphertext that carries nonce and tag; the protector hides those room keys at rest behind platform-specific protections so the config file never stores raw base64 keys. ## ClientEncryptionService.cs -Implements IMessageEncryptionService for encrypting/decrypting messages. +Implements client-side encryption for messages and room keys. -The [ClientEncryptionService](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) class is the client-side AES-256-GCM encryptor/decryptor that mirrors the server’s encryption format so clients and server exchange the same payload shape. It exposes SetKey to accept a 32-byte, server-provided base64 key, Encrypt to produce a prefixed base64 payload containing nonce and ciphertext+tag, and Decrypt to reverse that encoding; before a key is set Encrypt is intentionally a no-op and returns plaintext, and Decrypt returns a sentinel failure message when decryption fails. Per the documentation, the service isolates cryptography behind a swappable implementation and is used by higher-level connection code to apply message encryption only when a key is loaded; in this topic it is referenced by [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md). +The [ClientEncryptionService](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) is a sealed implementation of the message-encryption contract that performs AES-256-GCM on plaintext before it leaves the client. Its public surface includes SetKey (accepts a base64-encoded key and enforces exactly 32 bytes), Encrypt (generates a fresh 12-byte nonce, produces a 16-byte authentication tag, and returns a string that begins with an EncryptionPrefix and contains base64-encoded nonce and payload), and Decrypt (which returns plaintext unchanged if no key is set or if the input lacks the expected prefix). The class also provides nullable-friendly helpers EncryptNullable and DecryptNullable; decryption failures are handled gracefully by returning a sentinel message rather than throwing. Because it expects a server-provisioned key via SetKey, it does not manage persistent key storage itself and therefore can be paired with a separate on-disk protector to obtain that key material at runtime. ## RoomKeyProtector.cs -Provides protection around room keys for secure storage/usage. - -The [RoomKeyProtector](../Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md) class is the single API for protecting and unprotecting per-user room content keys before they are written to or read from client configuration. Its Protect method returns a storage-ready string that is prefixed to indicate the protection method ("dp1:" for Windows DPAPI or "k1:" for an AES-GCM-encrypted master key file on other platforms), and TryUnprotect attempts to recover the raw room key while reporting whether the stored value was a legacy plain-base64 entry and whether unprotection succeeded. The class accepts a directory (to locate the master key file) and caches the master key after guarded loading; callers such as [ClientConfig](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) and [RoomKeyStore](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) rely on it to convert between in-memory bytes and protected storage strings without having to deal with platform-specific details. - -## RoomKeyStore.cs -Stores and retrieves room keys securely for channels. - -[RoomKeyStore](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) binds runtime state (a decrypted, in-memory cache of room keys) to persisted per-server entries so users don't re-enter passphrases every launch. You call LoadForServer(serverUrl) to bind the store to a SavedServer in [ClientConfig](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md); the store will read that SavedServer's ChannelKeys, call into [RoomKeyProtector](../Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md) to unprotect them, and populate its thread-safe cache. The class exposes methods to TryGetKey, StoreKey, Replace/Remove keys, and TryStoreFromEnvelope (which unwraps a wrapped key with a KEK) and will upgrade legacy plain/base64 entries to the protected format when possible; changes are persisted back to the SavedServer through ClientConfig and unreadable entries are logged rather than failing hard. Connection-side code (notably [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md)) uses RoomKeyStore to determine which channels are encrypted and to retrieve keys for encrypting/decrypting messages at send/receive time. +Provides protection for per-room encryption keys used in chats. -## ClientConfig.cs -`ClientConfig` collaborates directly with `RoomKeyProtector` and other members of this topic (3 dependency links). - -[ClientConfig](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) is the central container for a user's persisted preferences and runtime state, and it holds SavedServer entries that include the persisted, protected ChannelKeys consumed by [RoomKeyStore](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md). ClientConfig provides the serialized place where RoomKeyProtector-generated strings live (the protector prefixes such as "dp1:" or "k1:" are stored here), and callers such as RoomKeyStore read and write these SavedServer entries to keep the on-disk picture in sync with the in-memory cache. Because RoomKeyProtector derives its key-file location from a directory you pass to its constructor, ClientConfig’s location and usage patterns determine where the master key file will be stored and how RoomKeyStore persists upgrades from legacy entries. - -## ConnectionManager.cs -`ConnectionManager` collaborates directly with `ClientEncryptionService` and other members of this topic (2 dependency links). - -[ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) is the high-level lifecycle owner for authentication, establishing the EchoHub SignalR connection, and wiring end-to-end encryption into runtime behavior. During ConnectAsync it performs authentication (throwing on auth failure), attempts to fetch and apply an E2E encryption key (failure to fetch is non-fatal and the manager logs a warning), and then uses the [ClientEncryptionService](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) to encrypt outbound messages and decrypt inbound ones when a key is present. It relies on [RoomKeyStore](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) to know which channels are encrypted and to obtain per-channel room keys, and it uses [ClientConfig](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) as the backing persisted configuration for saved servers; ConnectionManager forwards SignalR events as simple .NET events and implements IAsyncDisposable so callers can cleanly tear down network and API resources. +The [RoomKeyProtector](../Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md) encrypts the cached per-room content keys so the client configuration does not hold plain base64 keys. It exposes Protect(byte[] roomKey) to produce a storable string and TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy) to recover raw key bytes. On Windows it prefers DPAPI in the current-user scope and marks values with the DpapiPrefix (dp1:); on other platforms it encrypts keys with AES-GCM using a per-user master key file stored next to the config (KeyFilePrefix, k1:) with 0600 permissions. Entries with no known prefix are treated as legacy plain-base64 keys: they are loaded once and re-encrypted under the active scheme on save. The class caches the per-user master key, chooses the protection mechanism by platform, and explicitly never stores the room passphrase itself. How the pieces fit -- ConnectionManager is the orchestrator: it authenticates, attempts to fetch the server-provided E2E key, wires SignalR events to the UI, and delegates message-level cryptography to [ClientEncryptionService](../Code/src/EchoHub.Client/Services/ClientEncryptionService.cs.md) when a key is present. -- RoomKeyStore sits between persisted state and runtime: it loads and persists ChannelKeys via [ClientConfig](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) and uses [RoomKeyProtector](../Code/src/EchoHub.Client/Services/RoomKeyProtector.cs.md) to unprotect/protect those keys so the on-disk config never contains raw base64 room keys (legacy unprotected values are upgraded when possible). -- RoomKeyProtector implements the platform-specific protection formats (DPAPI or a file-backed AES-GCM master key) and presents a stable Protect/TryUnprotect API so the higher-level store and config code do not need to handle cryptography details. - -Together these components keep plaintext room keys out of persistent storage, keep a decrypted cache for active sessions, and ensure message encryption happens only when a server-supplied key has been loaded and applied by the client encryptor. +At runtime the pattern is: RoomKeyProtector is responsible for safe at-rest storage of raw room key bytes; callers call TryUnprotect to obtain the byte[] for a room, base64-encode that raw key and pass it to ClientEncryptionService.SetKey, and then call Encrypt/Decrypt to protect message payloads. Conversely, when a new room key is generated or received from the server, callers call Protect to produce the on-disk representation (with the dp1: or k1: prefix) so future runs can recover the same raw bytes. The direction of dependency is clear: the protector controls persistent formats and prefixes and hands raw key bytes to higher-level encryption (which enforces the 32-byte requirement and performs AES-GCM message operations). --- -*Covers 5 of 5 source files identified for this topic.* +*Covers 2 of 2 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:52:19 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:34:11 UTC* diff --git a/docs/auriondocs/Synthesis/real-time-connection.md b/docs/auriondocs/Synthesis/real-time-connection.md index 1db6744..4efa2ea 100644 --- a/docs/auriondocs/Synthesis/real-time-connection.md +++ b/docs/auriondocs/Synthesis/real-time-connection.md @@ -1,34 +1,29 @@ -# Real-time connection management +# Real-time connection and messaging -> Managing the SignalR hub connection lifecycle and connection state. +> How the client establishes and maintains a real-time connection to the server and handles channel messaging. -# Real-time connection management - -This guide explains how the client-side pieces manage a SignalR-based chat connection, surface server events to the UI, and carry message and attachment DTOs across those boundaries. Read it to understand which types own the HubConnection lifecycle, which types represent messages and attachments, and how connection orchestration hands events and histories back to the UI layer. +A compact overview of the client-side real-time layer: how the connection is created, the runtime surface it exposes to higher layers, and the DTOs used to carry chat and channel data. Read this to understand which types you call to open a SignalR-backed, end-to-end-capable chat connection, what events and exceptions to expect, and which transport records carry message and attachment metadata. ## EchoHubConnection.cs +Implements the SignalR connection lifecycle and messaging. -Encapsulates the SignalR connection to the server and join history with encryption info. - -The [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) type is a thin, SignalR-backed client wrapper that owns a HubConnection and translates server callbacks into plain .NET events (for example OnMessageReceived, OnUserJoined, OnChannelUpdated). It also integrates client-side encryption and room-key lookup: incoming payloads are decrypted before being raised to subscribers, and join history plus encryption metadata is tracked so callers can present past messages. EchoHubConnection declares focused exception types such as ChannelPasswordRequiredException (thrown when a join fails for password reasons) to enable UI-driven retry flows. According to the file relationships, EchoHubConnection consumes message and channel shapes from [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) and is instantiated and used by [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md). +This file defines a small domain exception and a single, high-level connection wrapper. The `ChannelPasswordRequiredException` is a dedicated exception type that carries a `ChannelName` and signals that a channel join failed due to missing or invalid credentials; the doc recommends UIs catch this specific type to prompt for a password and retry. The `EchoHubConnection` class is an event-driven wrapper around a SignalR `HubConnection`: it registers the server callback handlers defined by the server contract, decrypts incoming content when necessary, exposes simple events for messages, presence and channel updates, surfaces connection-state changes, and centralizes token provision and reconnection wiring. Within this topic `EchoHubConnection` consumes the transport records from [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) and is constructed/managed by the [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md). ## ConnectionManager.cs +Orchestrates connection state and event wiring for the real-time layer. -Coordinates connection lifecycle and connection events across the client. - -The [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) is the high-level owner of authentication, E2E key retrieval, HubConnection creation, wiring of SignalR callbacks, and tracking of joined channels. It exposes a small event surface that forwards the EchoHubConnection events to the UI (the doc notes AppOrchestrator subscribes), implements IAsyncDisposable to tear down both the hub wrapper and the underlying client, and reports progress from ConnectAsync via an onStatus callback while throwing on authentication failure. ConnectionManager also defines the [ConnectResult](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) record that packages the login response, a list of channel DTOs, and a dictionary of channel histories (the histories contain [MessageDto] entries defined in ChatDtos). Per the relationships, ConnectionManager depends on [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) for payload shapes and on [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) to manage the live SignalR interactions. +`ConnectionManager` is the composition root for a full client connection: it performs authentication (via the project's HTTP API client), attempts to fetch and apply end-to-end encryption keys, constructs and wires an [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md), and maintains channel membership state. It forwards the hub's runtime events (for example message and presence events) as higher-level events such as `MessageReceived`, `UserJoined`, and `ConnectionStatusChanged`, so UI orchestrators can subscribe without touching SignalR internals; these forwarded events may be raised from SignalR threads and callers must marshal to the UI thread if required. `ConnectAsync` reports progress through an `onStatus` callback and returns a `ConnectResult` (also declared in this file) to indicate outcome; the class and the underlying `EchoHubConnection` both implement `IAsyncDisposable`, and the doc emphasizes awaiting disposal so resources (connection, tokens, keys) are cleaned up. ## ChatDtos.cs - `AttachmentDto` collaborates directly with `ConnectResult` and other members of this topic (9 dependency links). -[ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) defines the immutable DTOs used across the connection boundary: records such as AttachmentDto, ChannelDto, ChannelMetaDto, MessageDto, JoinChannelResult, and request shapes like SendMessageRequest. The [AttachmentDto](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) record carries metadata for file attachments (kind, URL, filename, filesize, optional ASCII preview) and is explicitly designed to work with end-to-end encrypted channels where the resource behind Url may be ciphertext the server cannot read. These DTOs are the concrete payload shapes that both [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) and [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) send, receive, and store in histories. +This file declares the transport records used across the real-time boundary: types such as `AttachmentDto`, `ChannelCryptoDto`, `ChannelDto`, `ChannelMetaDto`, `CreateChannelRequest`, `EmbedDto`, `JoinChannelResult`, `MessageDto`, `RekeyChannelRequest`, `ReplyRefDto`, `SendMessageRequest`, `SendUrlRequest`, `UpdateTopicRequest`, and `UserDto` model messages, channels, users and channel crypto metadata. Concretely, `AttachmentDto` is a value record holding `Kind`, `Url`, `FileName`, `FileSize`, and an optional `AsciiPreview`; the doc highlights that `Url` and `AsciiPreview` may be ciphertext for end-to-end encrypted channels, and that attachments are carried as metadata so clients can fetch or preview content on demand. `ChannelCryptoDto` is a small record with `IsEncrypted` and an optional `EncryptionSalt` and is used to indicate whether channel payloads are protected. These DTOs are the typed payloads that [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) emits and that [ConnectionManager](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) tracks when reporting join results and message events. How the pieces fit -ConnectionManager is the orchestration layer: it authenticates, attempts to acquire E2E keys, builds and wires an [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md), and exposes forwarded events to the UI while tracking joined channels and histories. EchoHubConnection is the SignalR-focused implementation that manages the HubConnection lifecycle, maps server callbacks to events, performs decryption of incoming payloads, and throws focused exceptions (for example ChannelPasswordRequiredException) so the UI can prompt and retry joins. The DTOs in [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) are the shared, immutable shapes (messages, channels, attachments) that flow between the manager, the hub wrapper, and the UI; ConnectResult packages those DTOs back to callers after an initial connect sequence. +ConnectionManager is the orchestration layer: it authenticates, applies E2E keys, constructs an [EchoHubConnection](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md), and subscribes to its events so UI-level orchestrators can observe high-level events and results without dealing with SignalR. EchoHubConnection implements the low-level SignalR wiring, dispatches strongly-typed events and domain exceptions (for example `ChannelPasswordRequiredException`), and uses the DTOs from [ChatDtos.cs](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) as its message and channel payloads. Together they present a clear separation: DTOs define the wire shape, EchoHubConnection maps wire messages to runtime events and errors, and ConnectionManager composes those primitives into a single lifecycle and event surface for the UI. --- *Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:51:44 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:31:01 UTC* diff --git a/docs/auriondocs/Synthesis/ui-theming.md b/docs/auriondocs/Synthesis/ui-theming.md index ad9123b..cf70d4f 100644 --- a/docs/auriondocs/Synthesis/ui-theming.md +++ b/docs/auriondocs/Synthesis/ui-theming.md @@ -1,31 +1,29 @@ -# Theming and UI color management +# UI theming and theme management -> Representing themes, color palettes, and runtime theme application. +> Theme data models and the system that loads, stores, and applies themes to the UI. -Theming and UI color management +This guide explains the client-side theming pieces: the Theme data model, the ThemeManager that provides built-in and user-provided themes and applies them at runtime, and the AppOrchestrator that coordinates UI behavior (including theme usage). Read this when you need to add a new theme, wire theme selection into the UI, or understand how theme persistence and runtime application are handled. -The files in this topic define how the EchoHub client represents color themes, exposes a curated set of built-in and user-provided themes, and wires theme selection into the running application. Read these three artifacts to understand the Theme data model, the static ThemeManager API that discovers/applies/persists themes, and the AppOrchestrator entry point that reacts to user commands and delegates theme work to the manager. - -## Theme.cs -Represents a UI theme. +## ThemeManager.cs +Manages built-in themes, theme lookup, and application. -The [Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) class is the data container for a complete UI appearance. It exposes a required Name plus four area-specific palettes—Base, Menu, Dialog, and Status—each typed as a [ThemeColors](../Code/src/EchoHub.Client/Themes/Theme.cs.md) instance, and an optional Border palette that, when set, overrides only frame-border colors while leaving other chrome tied to Base. The writer notes sensible defaults: each palette initializes to a new ThemeColors so a Theme is usable with minimal configuration, and Border accepts hex literals or named colors to let designers tint edges without touching text palettes. This file is the canonical representation of a theme and is consumed by the [ThemeManager](../Code/src/EchoHub.Client/Themes/ThemeManager.cs.md) to build and persist theme choices and by the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) when the application needs to apply or react to theme changes. +[ThemeManager](../Code/src/EchoHub.Client/Themes/ThemeManager.cs.md) is a static helper that centralizes theming for the client UI. The class defines a fixed set of built-in theme instances (named constants such as DefaultTheme, TransparentTheme, DraculaTheme, NordTheme, etc.), exposes a ThemeDir and JsonOptions for disk-backed theme discovery and persistence, and provides methods callers use to enumerate, fetch, apply, and save themes: GetAvailableThemes() merges built-ins with user theme files (skipping duplicates and malformed files and falling back to built-ins if the directory cannot be read), GetTheme(name) retrieves a theme by name, SaveTheme persists a Theme to disk, and ApplyTheme performs the runtime application of a Theme to the UI. The file also contains utility logic used by those flows — ParseColor to turn color strings into runtime values and BuildColorScheme(ThemeColors) which maps a Theme's ThemeColors into the editor/UI surfaces so properties like transparency are preserved. ThemeManager stores and manipulates instances of the [Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) model and is the primary integration point other code uses to present, switch, or persist themes. -## ThemeManager.cs -Loads, caches, and applies themes across the app. +## Theme.cs +Represents a theme data model used by the theming system. -[ThemeManager](../Code/src/EchoHub.Client/Themes/ThemeManager.cs.md) is a static API that bridges theme data and runtime application. It exposes discovery and retrieval functions such as GetAvailableThemes and GetTheme, mutation points like SaveTheme, and the runtime switch ApplyTheme; utility functions include ParseColor and BuildColorScheme, the latter ensuring colors for editable/read-only roles and transparency behave correctly so inputs remain legible under transparent themes. ThemeManager maintains a curated set of built-in theme factory methods (DefaultTheme, DraculaTheme, LightTheme, etc.), attempts to load additional themes from a user directory (ThemeDir), and falls back to built-ins if the directory cannot be read; SaveTheme is implemented best-effort and quietly swallows failures. Because it returns and manipulates [Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) instances, ThemeManager is the component the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) calls when the app needs to enumerate, choose, or persist a theme and when it needs the computed color scheme to apply to the UI. +[Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) is the data descriptor for a visual style. A Theme groups per-surface color sets (Base, Menu, Dialog, Status) and optionally supplies a Border color that overrides the window frame independently of the surface colors; if Border is null, consumers fall back to Base. Each surface is represented by a [ThemeColors](../Code/src/EchoHub.Client/Themes/Theme.cs.md) instance, which bundles Foreground, Background, FocusForeground, and FocusBackground tokens. ThemeColors provides sensible defaults (a high-contrast dark baseline) but is mutable via public setters, so callers can tweak palettes after construction; Theme objects are the units ThemeManager stores, enumerates, and writes to disk. ## AppOrchestrator.cs `AppOrchestrator` collaborates directly with `Theme` and other members of this topic (2 dependency links). -[AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) is the application-level coordinator that owns the MainWindow and a large set of command handlers; among its many responsibilities it includes a handler named HandleCmdSetTheme which responds to theme-change requests. In practice the orchestrator calls into [ThemeManager](../Code/src/EchoHub.Client/Themes/ThemeManager.cs.md) to fetch or apply a [Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) (for example via GetTheme and ApplyTheme) and then ensures the active UI reflects the manager-provided color scheme. The doc block lists the constructor and MainWindow property plus the command handlers (including HandleCmdSetTheme) so the intended runtime flow is: user or code issues a theme command to AppOrchestrator, AppOrchestrator delegates theme discovery/load/apply to ThemeManager, and the Theme instance shapes the MainWindow styling. +[AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) is the application-level coordinator that owns the MainWindow and many UI command handlers and lifecycle operations (the class lists a constructor, MainWindow, and dozens of handler and utility methods). Per its declared relationships it depends on the [Theme](../Code/src/EchoHub.Client/Themes/Theme.cs.md) model and the [ThemeManager](../Code/src/EchoHub.Client/Themes/ThemeManager.cs.md) helper. In practice AppOrchestrator is the place where UI-driven behavior is orchestrated: it presents or responds to user actions and calls into ThemeManager to retrieve available Theme objects, fetch a Theme by name, or request that a Theme be applied or saved so the MainWindow and its child surfaces reflect the current style. Because AppOrchestrator centralizes command handling and window-level concerns, it is the natural integration point to wire theme selection UI into the running application and to persist user choices through ThemeManager. How the pieces fit -Theme is the immutable-ish data model for visual choices; ThemeManager is the static service that discovers, builds, parses, and persists those models and produces a concrete color scheme via BuildColorScheme; AppOrchestrator is the runtime conductor that responds to user commands and uses ThemeManager to fetch and ApplyTheme to the UI. The dependency direction is AppOrchestrator -> ThemeManager -> Theme, with ThemeManager also responsible for supplying built-in Theme instances and reading user themes from disk when available. +ThemeManager is the provider and manipulator of Theme instances: it supplies built-in Theme objects, discovers and loads user themes from ThemeDir, parses color text, builds the UI color scheme, and persists themes to disk. The Theme class and its ThemeColors containers are the plain-data contract ThemeManager uses to describe a palette and to hand color sets to UI code. AppOrchestrator acts as the runtime coordinator: it uses ThemeManager to enumerate and fetch Theme objects in response to UI commands and ensures the MainWindow and related surfaces receive the Theme (and thus the color scheme) to render the chosen look. Together they form a simple pipeline: Theme data (Theme/ThemeColors) ↦ ThemeManager I/O and mapping (BuildColorScheme / ParseColor / SaveTheme) ↦ AppOrchestrator-driven application to the live UI. --- *Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:51:21 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:31:32 UTC* diff --git a/docs/auriondocs/Synthesis/update-management.md b/docs/auriondocs/Synthesis/update-management.md index 20c7c24..f59a888 100644 --- a/docs/auriondocs/Synthesis/update-management.md +++ b/docs/auriondocs/Synthesis/update-management.md @@ -1,32 +1,66 @@ # Update management -> Data and update flow: backup prior to updates and update checks. +> Checking for updates and backing up state related to updates. -Update management +*Figure: How Update management works.* -This topic covers the client-side update workflow: detecting available versions from the running Terminal.Gui app, deferring heavy update work until the UI has shut down, and snapshotting the application state so you can roll back if an update goes wrong. The two files coordinate a safe in-place updater by separating user interaction and terminal ownership (in UpdateChecker) from the filesystem snapshot and metadata (in UpdateBackupService). +```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'}}}%% +sequenceDiagram +participant AppOrchestrator +participant UpdateChecker +participant UpdateBackupService -## UpdateBackupService.cs -Provides backup of user data before updates. +AppOrchestrator->>UpdateChecker: "CheckForUpdates()" +activate UpdateChecker +UpdateChecker->>UpdateBackupService: "PrepareBackup()" +activate UpdateBackupService +UpdateBackupService->>UpdateChecker: "RequestBackupValidation()" +UpdateChecker-->>UpdateBackupService: "ValidateBackup()" +UpdateBackupService-->>UpdateChecker: "BackupPrepared" +deactivate UpdateBackupService +alt "Update available" + UpdateChecker-->>AppOrchestrator: "ReportUpdateAvailable()" + AppOrchestrator->>UpdateBackupService: "CreateBackupState()" + activate UpdateBackupService + UpdateBackupService-->>AppOrchestrator: "BackupInfo (serialized)" + deactivate UpdateBackupService +else "No update" + UpdateChecker-->>AppOrchestrator: "ReportNoUpdate()" +end +deactivate UpdateChecker + +AppOrchestrator->>UpdateChecker: "Dispose()" +AppOrchestrator->>UpdateBackupService: "Dispose()" +``` -The file declares three related symbols that implement pre-update snapshotting. [BackupJsonContext](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) is an internal, source-generated JsonSerializerContext that supplies reflection-free JSON metadata for serializing the on-disk metadata type. The public [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) static class performs the actual backup/rollback responsibilities: it creates a ZIP snapshot of the running application under ~/.echohub/update-backup/ (backup.zip) and writes a companion backup-info.json (the [BackupInfo](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) contract) that records the version, application directory, and UTC timestamp. The service exposes operations to CreateBackup before applying an update, to check presence via BackupExists, and to read metadata with GetBackupInfo; it also exposes an IsPostUpdate flag that lets startup logic detect a recent update backup and react accordingly. Because the JSON context is internal and generated, callers within the assembly configure JsonSerializerOptions with BackupJsonContext when they read or write backup-info.json. +# Update management -The file is used by the update coordination logic in [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md): the checker defers the updater work but relies on UpdateBackupService to attempt a pre-update snapshot when the update is actually applied. +This topic covers the small set of services and the orchestrator that detect available application updates, snapshot state before an update, and hand off the heavy update work so it happens after the Terminal.Gui main loop has exited. The pieces separate responsibilities: a background checker and confirmation flow, a backup/metadata helper that writes a ZIP and JSON, and the application orchestrator that stores the post-TUI delegate the host must invoke. Together they avoid console deadlocks and provide a predictable rollback surface for the updater. ## UpdateChecker.cs -Checks for updates and coordinates update flow. -[UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md) is a disposable helper that runs background polling and supports manual checks, while keeping all user interaction on the provided Terminal.Gui IApplication main loop. Its responsibilities are: poll for newer versions via an internal Updater, present a TUI confirmation dialog by marshalling callbacks with _app.Invoke, and — crucially — avoid performing download/extract/restart while the TUI still owns the terminal. When the user accepts an update, UpdateChecker sets PendingUpdate to an awaitable delegate (the internal ApplyUpdateAsync) and captures the chosen version, then signals the TUI to stop; the host is expected to call PendingUpdate after the main loop exits so the update can run headless and safely restart the process. +Checks for updates and reports availability. + +The [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md) type is a sealed, disposable service that runs update checks on a background schedule and coordinates a safe, post-TUI update process. It listens for the underlying updater events, shows a confirmation UI (via the confirmation dialog flow described in the docs), and—when the user confirms—sets the public [PendingUpdate](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) delegate and requests the Terminal.Gui UI to stop so the host can perform the download/extract/restart work on a plain console. `Start()` only enables periodic checks in RELEASE builds, `CurrentVersion` exposes the assembly version (falling back to "0.0.0" if unavailable), and the checker attempts to create a pre-update snapshot by calling into [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) before the heavy update work runs. The checker is consumed by the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) and depends on [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) for backup creation. + +## UpdateBackupService.cs + +Maintains backups for update-related data and state. + +The [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) is a static helper that centralizes pre-update snapshot and rollback metadata management. Its `CreateBackup()` routine snapshots `AppContext.BaseDirectory` into a `backup.zip` (skipping log files to avoid locking and using `CompressionLevel.Fastest`) and writes a `backup-info.json` that records the current version, application directory, and timestamp; it annotates that metadata using the `CurrentVersion` supplied by [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md). `BackupExists()` verifies that both the ZIP and the JSON exist, and `GetBackupInfo()` reads the stored metadata. Serialization for the `BackupInfo` metadata is handled by the source-generated [BackupJsonContext](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) to provide reflection-free `JsonSerializer` metadata. The service stores backups under the user profile at `~/.echohub/update-backup/`, depends on [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md) for the reported version, and is used by both the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) and [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md). + +## AppOrchestrator.cs + +`AppOrchestrator` collaborates directly with `UpdateBackupService` and other members of this topic (2 dependency links). -Concrete behaviors documented in the class include: Start() only activates the periodic poller in RELEASE builds; PendingUpdate is intentionally a Task-returning delegate to be invoked by the host after the console is restored; ApplyUpdateAsync attempts to create a pre-update backup by calling [UpdateBackupService.CreateBackup](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) and logs but does not fail the update flow if backup creation fails; ApplyUpdateAsync also sets Console.OutputEncoding = UTF8 while swallowing exceptions for non-interactive stdout; and CurrentVersion reads the assembly version with a fallback of "0.0.0". +The [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) is the TUI host and coordinator that declares UI components (like `MainWindow`) and the public [PendingUpdate](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) delegate referenced by the update flow. In this topic its role is to be the object that stores the pending, post-TUI update action that [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md) can set when the user accepts an update; the application or host must examine and invoke that [PendingUpdate](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) delegate after the Terminal.Gui main loop exits. `AppOrchestrator` depends on the backup and checker services to implement the safe update flow and is the natural boundary between the interactive UI and the plain-console updater. How the pieces fit -- Update detection and user confirmation happen inside [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md) running on the Terminal.Gui main loop; when the user accepts an update, the checker defers the actual work by setting PendingUpdate and requesting the TUI to stop. -- The deferred update work (ApplyUpdateAsync) calls into [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) to snapshot the application: it writes backup.zip and backup-info.json (the serialized [BackupInfo](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) using [BackupJsonContext](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md)). Backup creation failures are logged but do not block the update. -- The host is responsible for invoking PendingUpdate only after the TUI main loop has fully exited and the console is restored, at which point the update runs headless (and may restart the process). +Update detection and user confirmation are handled by [UpdateChecker](../Code/src/EchoHub.Client/Services/UpdateChecker.cs.md), which runs periodically (in RELEASE builds) and listens for updater events. When an update is accepted the checker asks [UpdateBackupService](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) to create a snapshot, sets the [PendingUpdate](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) delegate on the [AppOrchestrator](../Code/src/EchoHub.Client/AppOrchestrator.cs.md), and requests the TUI to stop. After the Terminal.Gui main loop exits the host (or the orchestrator) must invoke the stored [PendingUpdate](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) delegate to run the download/extract/restart work on a plain console; that work may restart the process and should be considered a non-returning operation. The backup metadata is serialized through the source-generated [BackupJsonContext](../Code/src/EchoHub.Client/Services/UpdateBackupService.cs.md) and stored under `~/.echohub/update-backup/` so the updater has a clear rollback artifact if needed. --- -*Covers 2 of 2 source files identified for this topic.* +*Covers 3 of 3 source files identified for this topic.* -*Synthesised by Aurion on 2026-07-23 05:54:19 UTC* +*Synthesised by AurionDocs on 2026-07-23 09:33:37 UTC*