Compare commits

175 Commits
Author SHA1 Message Date
Hue 8dbcc517a4 Merge pull request #77 from HueByte/aurion_23-07-2026
[AurionDocs] Documentation update for merge
2026-07-23 23:15:04 +02:00
Hue 91ea76ffc7 docs: Update documentation for 6 files
Generated by AurionDocs
Job ID: a40f281c-249b-4dcc-aac5-3cc48888e00f
Source commit: 2d5f8ee
2026-07-23 23:13:40 +02:00
Hue 2d5f8eeded Merge pull request #73 from HueByte/dev
Dev
2026-07-23 23:07:58 +02:00
HueByte 65766eab1f test: add regression tests for IRC multi-line message formatting 2026-07-23 23:06:15 +02:00
Stone_Red 675a39cb15 fix: formatting 2026-07-23 23:06:15 +02:00
Stone_Red cda890de60 chore: add changelog 2026-07-23 23:06:14 +02:00
Stone_Red 7eced63608 feat: warn when server and client versions differ during connection setup 2026-07-23 23:06:14 +02:00
Stone_Red 07996a78ed chore: bump version and add changelog 2026-07-23 23:06:14 +02:00
Stone_Red 329dfe422b fix: irc messages not getting split by line 2026-07-23 23:06:14 +02:00
HueByte b1aeb8cbb2 fix: add 'docs/auriondocs/**' to ignore list in markdownlint configuration 2026-07-23 23:06:14 +02:00
Hue 7964355e08 Merge pull request #76 from HueByte/aurion_23-07-2026
[AurionDocs] Documentation update
2026-07-23 11:55:26 +02:00
Hue 1e877cf886 docs: Update documentation for 1 files
Generated by AurionDocs
Job ID: b2ed7c78-22c5-4977-8d85-4f9ea17e3b0a
Source commit: HEAD
2026-07-23 11:54:43 +02:00
Hue 0105f798e0 Merge pull request #74 from HueByte/aurion_23-07-2026
[AurionDocs] Full documentation update
2026-07-23 11:47:30 +02:00
Hue 607217b314 docs: Update documentation for 145 files
Generated by AurionDocs
Job ID: 934f8c39-8082-4942-8d17-72ed8f5f8d50
Source commit: 40aea9a
2026-07-23 11:44:20 +02:00
Hue 40aea9a04b Merge pull request #72 from HueByte/aurion_23-07-2026
[AurionDocs] Documentation update
2026-07-23 08:12:41 +02:00
Hue f8f4e03ddd docs: Update documentation for 145 files
Generated by AurionDocs
Job ID: c99fff50-67a3-4294-b4df-3e73f4f12de9
Source commit: 4dcb480
2026-07-23 08:10:35 +02:00
Hue 4dcb480d1d Merge pull request #65 from HueByte/dev
Dev
2026-07-17 23:38:59 +02:00
Hue 45dc0f9d0f Merge branch 'master' into dev 2026-07-17 23:38:47 +02:00
HueByte fbeb546ac5 refactor: update contributors section in README for improved layout and clarity 2026-07-17 23:30:01 +02:00
HueByte e3654c637c fix: allow README files to open with a centered HTML header block 2026-07-17 23:19:42 +02:00
HueByte 2297b58c74 refactor: streamline README structure and enhance clarity of project description 2026-07-17 23:17:55 +02:00
Hue 6e57247a45 Merge pull request #64 from HueByte/dev_logs_extended
feat: add periodic server stats reporting and logging
2026-07-17 22:23:13 +02:00
HueByte fd61d9cb9c fix: remove invisible character from migration file 2026-07-17 22:18:49 +02:00
HueByte 79e5a1191f feat: update changelog for v0.2.16 release with new features and improvements 2026-07-17 22:17:21 +02:00
HueByte 7525f8b1d8 feat: add periodic server stats reporting and logging
- Introduced ServerStatsReport and ServerStatsCollector for tracking server activity.
- Implemented ServerStatsReportService to generate and persist stats reports periodically.
- Added configuration options for stats reporting in appsettings and .env.example.
- Enhanced logging in ChannelsController and ModerationController to include stats-related actions.
- Updated database context and migrations to support new ServerStatsReport entity.
- Adjusted logging levels in IrcGatewayService and ChatService for better performance.
2026-07-17 22:14:55 +02:00
Hue 0c2e8eae87 Merge pull request #62 from HueByte/dev_logs_room
Dev logs room
2026-07-17 21:08:48 +02:00
HueByte 75ff10c5fd fix: remove invisible character from migration file 2026-07-17 21:00:34 +02:00
HueByte 46afd2a195 feat: add unit tests for ChannelService, FileStorageService, and UserService 2026-07-17 20:59:59 +02:00
HueByte 38eca99fb1 feat: add server logs functionality with role-based access and system channel management
- Introduced a new migration to add the IsSystem column to the Channels table.
- Updated the DbContext model snapshot to reflect the new IsSystem property.
- Enhanced the ChannelService to manage system channels, including creation, visibility control, and protection against deletion.
- Implemented ServerLogsService to handle live server logging, including reading from log files and managing access based on user roles.
- Created ServerLogsSink to queue log events for streaming to the live log room.
- Developed ServerLogsStreamService to stream log events to clients in real-time.
- Added configuration options for server logs in appsettings.
- Implemented comprehensive unit tests for channel service system channel behavior and server logs functionality.
2026-07-17 20:44:47 +02:00
HueByte df1bd0119c feat: Update configuration files for storage path and spam protection settings 2026-07-17 19:54:30 +02:00
HueByte 71d82b45fb fix: Remove invisible character from using directive 2026-07-17 19:49:38 +02:00
HueByte 53d0d326cb feat: Implement spam protection with configurable limits and auto-mute escalation 2026-07-17 19:47:57 +02:00
HueByte 3281064720 feat: Add invite codes and message replies functionality
- Introduced a new migration to add InviteCodes table and ReplyToMessageId column in Messages.
- Updated ChatHub to support replying to messages.
- Enhanced ChatService to handle message replies and validate reply targets.
- Modified UserService to implement invite-only registration mode with invite code consumption.
- Added configuration options for registration modes in appsettings.
- Created unit tests for new features including invite code registration and message reply formatting.
2026-07-17 19:47:57 +02:00
Hue bb987dda82 Merge pull request #60 from HueByte/dev_irc_fixes_adjustments
Dev irc fixes adjustments
2026-07-17 18:01:00 +02:00
HueByte e71575d920 feat: update changelog for v0.2.15 release with new features and bug fixes 2026-07-17 17:11:06 +02:00
HueByte 96736d69df feat: enhance message broadcasting to exclude sender's connection and improve IRC compliance 2026-07-17 17:05:03 +02:00
HueByte 953e081123 feat: add image handling actions and improve IRC message formatting 2026-07-17 16:52:15 +02:00
Stone_Red b9d099dd73 chore: bump version and add changelog 2026-07-17 16:36:10 +02:00
Stone_Red a9519a52cc feat: add support for broadcasting and handling channel deletions across clients 2026-07-17 16:35:59 +02:00
Hue 4d25458717 Merge pull request #58 from HueByte/dev
Dev
2026-07-16 20:33:43 +02:00
Hue 584ce45979 Merge branch 'master' into dev 2026-07-16 20:26:16 +02:00
HueByte 53927b130c Add markdown linting tools and scripts
- Create package.json to manage markdownlint-cli2 as a dev dependency.
- Add PowerShell script for linting Markdown files with options for fixing issues.
- Update shell script to prefer locally installed markdownlint-cli2 or fallback to npx.
2026-07-16 20:22:57 +02:00
HueByte ca3c1ec2b5 feat: add documentation for TUI, messages & attachments, and IRC gateway; update index and TOC 2026-07-16 20:02:18 +02:00
Hue 35a4eec749 Merge pull request #57 from HueByte/dev_copy_paste_qol
Dev copy paste qol
2026-07-16 19:44:32 +02:00
HueByte 78f18a36ba docs: update changelog for v0.2.14 to include clipboard image and multi-file paste features 2026-07-16 19:41:15 +02:00
HueByte 2157884e61 refactor: replace Key constants with KeyCode for improved clarity and performance 2026-07-16 19:40:18 +02:00
HueByte 4f2cffa372 feat: add clipboard image handling for pasting and staging attachments 2026-07-16 19:40:18 +02:00
Hue 31a0bb8c9f Merge pull request #56 from HueByte/dev_fixes_features_and_nightmares
feat: enhance IRC support with display name handling, private channel…
2026-07-16 19:32:15 +02:00
HueByte e439c8ae72 feat: implement end-to-end encryption for room keys with secure storage and migration support 2026-07-16 18:47:28 +02:00
HueByte b6c01dab15 feat: enhance IRC support with display name handling, private channel tracking, and connection identification 2026-07-16 18:22:34 +02:00
Hue 1f4af9a543 Merge pull request #55 from HueByte/dev
Dev
2026-07-16 16:52:23 +02:00
HueByte b62729dc95 feat: enhance message decryption for IRC clients and improve attachment handling 2026-07-16 16:35:34 +02:00
HueByte aae788028e feat: enhance chat functionality with auto-join, persistent read positions, and theme border customization 2026-07-16 08:19:43 +02:00
HueByte c048b39e42 feat: release v0.2.13 with chat visual overhaul and new features 2026-07-16 07:54:17 +02:00
HueByte 3886e7148c feat(chat): enhance chat UI with new features and visual improvements
- Added new attributes for chat colors including rail, date rules, and unread markers.
- Enhanced ChatLine class to support colored segments for continuation lines and horizontal separator rules.
- Introduced a WelcomeBanner class to display a splash screen when no channel is selected.
- Implemented NickColorHelper for deterministic per-nick colors, ensuring consistent user color representation.
- Updated ChannelListSource to highlight channels with unread messages and mentions.
- Improved MainWindow to include a spinner for transitional connection states and display activity in the status bar.
- Added unit tests for NickColorHelper to ensure stable and predictable color indexing.
2026-07-16 07:48:47 +02:00
Hue b9a80806e1 Merge pull request #54 from HueByte/dev_update_fixes
Dev update fixes
2026-07-16 07:10:50 +02:00
HueByte e797ec2542 feat: add /meta command for channel metadata retrieval
- Implemented the `/meta` command to fetch and display channel metadata including room ID, topic, message count, unique user count, estimated size, and protection level.
- Added `ChannelMetaDto` to encapsulate channel metadata.
- Updated `ChannelsController` to handle the new `/meta` endpoint.
- Introduced `UploadLimits` configuration for admin-defined upload size limits for files, images, audio, and avatars.
- Enhanced error handling and user feedback for metadata retrieval.
- Updated documentation to reflect changes in encryption and room metadata.
- Added tests for the new functionality and upload limits.
2026-07-16 07:04:58 +02:00
HueByte dbf6565d18 feat: Implement update confirmation handling and streamline update process 2026-07-16 07:04:26 +02:00
HueByte 494dcb46cf feat: Update CI workflows to trigger on successful completion of previous CI runs 2026-07-16 05:59:27 +02:00
Hue ce41ef9c0a Merge pull request #53 from HueByte/dev_QOL_And_Gateway
Dev qol and gateway
2026-07-16 05:53:33 +02:00
HueByte 9aea6ecfc3 refactor: Update DroppedFileParserTests to use absolute path helper for improved readability and maintainability 2026-07-16 05:52:06 +02:00
HueByte 5040c5c201 feat: Enhance ThemeManager to ensure transparent themes render correctly for editable fields 2026-07-16 05:46:02 +02:00
HueByte a3a413f0b1 feat: Add "Copy message ID" option to message context menu 2026-07-16 05:38:41 +02:00
HueByte 6292e82cec feat: Add ASCII art size selection for image attachments and enhance message context menu 2026-07-16 05:35:11 +02:00
HueByte 2d33773c24 feat: Add support for message attachments
- Introduced Attachment model to handle file attachments associated with messages.
- Updated ModerationController to manage message deletions and attachment cleanup.
- Enhanced ChatService to include attachments in message retrieval.
- Implemented migration for legacy single-attachment messages to the new Attachments model.
- Added unit tests for attachment handling in message formatting and parsing.
- Updated database context and migrations to support new Attachments table.
2026-07-16 05:02:12 +02:00
HueByte e05b420ce9 feat: Implement end-to-end encryption for channels
- Added EncryptionSalt and WrappedRoomKey properties to Channel model.
- Introduced RoomCrypto class for client-side encryption and decryption.
- Updated ChannelService to handle encrypted channels, including creation and rekeying.
- Modified ChannelsController to expose crypto metadata and rekey functionality.
- Enhanced IrcCommandHandler to block joining encrypted channels over IRC.
- Updated database schema with migration for new encryption fields.
- Refactored file validation and image processing services to accommodate encrypted channels.
- Added unit tests for RoomCrypto functionality and updated existing tests for channel services.
2026-07-16 03:50:23 +02:00
HueByte ea8e583ee5 feat: add image validation and ASCII conversion services 2026-07-16 03:50:08 +02:00
HueByte 3ca9dbfd91 feat: add password protection for channels
- Updated IChatService to include password parameter in JoinChannelAsync method.
- Modified ChannelDto and related models to support password functionality.
- Implemented password handling in ChannelService for channel creation and membership validation.
- Enhanced IrcCommandHandler to manage channel join requests with passwords.
- Added ChannelPasswordDialog for user input when joining protected channels.
- Created database migration to add PasswordHash column to Channels table.
- Updated tests to cover new password functionality in channel joining and management.
2026-07-16 03:13:03 +02:00
Stone_Red 15187c4665 chore: bump version and add changelog 2026-07-16 01:05:21 +02:00
Stone_Red 64bca51619 feat: show date on messages older than a day and improve time formatting 2026-07-16 01:04:53 +02:00
Hue 3538ec8005 Merge pull request #49 from HueByte/dev
feat: update authors and project URL in nuspec file
2026-07-01 23:03:07 +02:00
HueByte 1b234e39e1 feat: update authors and project URL in nuspec file 2026-07-01 23:00:04 +02:00
Hue 19bf123c8c Merge pull request #48 from HueByte/dev
Dev
2026-07-01 22:40:16 +02:00
Stone_Red 6cf284a475 chore: remove LICENSE.txt and VERIFICATION.txt from choco tools directory 2026-07-01 22:35:44 +02:00
HueByte 3b45bb5661 feat: update DirectoryHubUrl to point to the production server 2026-04-24 16:39:16 +02:00
Hue 6235565480 Merge pull request #46 from HueByte/dev
Dev merge
2026-04-24 16:13:39 +02:00
HueByte ecb20c4c52 feat: update .env.example with additional server configuration comments and add release checklist workflow 2026-04-24 15:51:01 +02:00
HueByte a85d16fff8 feat: update installation script and documentation for version 0.2.11 release 2026-04-24 15:51:01 +02:00
HueByte 6db93ecfea feat: enhance server registration handling with response envelope and error management 2026-04-24 15:51:01 +02:00
HueByte 67587dafc2 feat: implement DirectoryClaimStore for managing directory claims and enhance ServerDirectoryService with registration error handling 2026-04-24 15:51:01 +02:00
HueByte 1bbe099835 feat: enhance presence tracking and server registration with user count updates and multi-host support 2026-04-24 15:51:01 +02:00
Hue 769aa5b468 Merge pull request #44 from HueByte/dev
fix: resolve client startup crash by explicitly passing Configuration…
2026-04-20 18:18:01 +02:00
Hue 6660944588 Merge branch 'master' into dev 2026-04-20 18:16:20 +02:00
HueByte 4b41438af0 fix: resolve client startup crash by explicitly passing ConfigurationReaderOptions for Serilog 2026-04-20 18:14:54 +02:00
Hue a4fe432992 Merge pull request #43 from HueByte/dev
Dev v0.2.10
2026-04-20 17:45:39 +02:00
HueByte 9279e8be06 fix: update release notes for v0.2.10 to reflect Command Palette and Infinite History Scroll features 2026-04-20 17:42:06 +02:00
Hue 7f9fcfe3cc Merge pull request #36 from HueByte/dev_fix_notification_sound
fix: error when playing notification sounds in quick succession
2026-04-20 17:42:06 +02:00
Hue 62c5ab27c5 Merge pull request #39 from HueByte/dev_load_more_messages_on_scroll
feat: load more messages on scroll
2026-04-20 17:42:05 +02:00
Stone_Red 240892495b chore: add notification bug fix to changelog 2026-04-20 17:42:05 +02:00
Stone_Red 6e7cbf39f0 chore: add channel history loading and refactor GetChannelHistory with offset to changelog 2026-04-20 17:42:05 +02:00
Stone_Red 83d257591e fix: error when playing notification sounds in quick succession 2026-04-20 17:42:05 +02:00
Stone_Red e42f1a0965 feat: load more message history when scrolling to top 2026-04-20 17:42:05 +02:00
Hue fe6dfd3d4d Merge pull request #40 from HueByte/dev_search_dialog
feat: add search dialog for quick navigation
2026-04-20 17:42:04 +02:00
Stone_Red d6282885e3 refactor: move search dialog handling to AppOrchestrator 2026-04-20 17:42:04 +02:00
Stone_Red bd8b88add2 chore: add command palette with Ctrl+K for quick navigation and actions to changelog 2026-04-20 17:42:04 +02:00
Stone_Red ff9a9e3dd0 chore: mark search bar/modal as completed in the todo list 2026-04-20 17:42:04 +02:00
Stone_Red aa6599a4e0 feat: search dialog to navigate app 2026-04-20 17:42:04 +02:00
Hue 3091a146eb Merge pull request #41 from HueByte/dev_move_cursor_to_end_on_autocomplete
fix: set input field insertion point to end after autocomplete
2026-04-20 17:42:03 +02:00
HueByte 000764fdb4 chore: update version to 0.2.10 and enhance installation scripts and changelog 2026-04-20 17:42:03 +02:00
Stone_Red 56fccf5cfb chore: add cursor position fix to changelog 2026-04-20 17:42:03 +02:00
Stone_Red b98673f8c5 fix: set input field insertion point to end after autocomplete 2026-04-20 17:42:03 +02:00
Stone_Red 335cfcc28a Merge pull request #42 from HueByte/dev_fix_update_service
fix: update service
2026-04-20 17:41:23 +02:00
HueByte 6aef6890cf fix: ensure progress dialog updates on the UI thread 2026-04-20 17:41:22 +02:00
Stone_Red 5b8df9d505 fix: remove unnecessary _app.Invoke calls around update progress dialog updates 2026-04-20 17:41:22 +02:00
Stone_Red 6dbc29818c fix: error when trying to zip open log file 2026-04-20 17:41:22 +02:00
HueByte 6758aceb5d chore: add v0.2.9 release notes to changelog and table of contents 2026-04-07 15:41:18 +02:00
Hue 06aba16303 Merge pull request #35 from HueByte/dev
Merge dev branch
2026-04-07 14:51:14 +02:00
Hue 89b028cf06 Merge branch 'master' into dev 2026-04-07 14:46:58 +02:00
Stone_Red eb4aac9861 Merge pull request #37 from HueByte/dev_fix_create_dialog_checkbox
fix: when users clicks public -> private -> public checkbox in the channel creation, it ends up creating the channel on 3rd check switch
2026-02-25 22:30:13 +01:00
Stone_Red 38d63d844d chore: mark checkbox issue in channel creation as completed in todo list 2026-02-25 22:29:06 +01:00
Stone_Red 3f2211f42a fix: double click on public checkbox creates channel by setting set CommandsToBubbleUp in dialog 2026-02-25 22:29:06 +01:00
Stone_Red 0606394af2 Merge pull request #38 from HueByte/dev_fix_choco_path
fix: choco install path by separating directory and file joins
2026-02-25 18:07:20 +01:00
Stone_Red ee18743721 fix: choco install path by separating directory and file joins 2026-02-25 18:04:13 +01:00
HueByte 0aaa371488 chore: update release process for single-file publish and enhance installer script 2026-02-25 10:08:50 +01:00
HueByte 8689dc2a01 fix: add TODO for incorrect embed color display 2026-02-24 23:08:32 +01:00
HueByte 5d61266fd7 docs: add configuration guide and update related documentation 2026-02-24 23:07:41 +01:00
HueByte ae342381e0 fix: update release workflow and README badges; reorganize table of contents 2026-02-24 22:50:29 +01:00
Stone_Red ba536c6f9c Merge pull request #33 from HueByte/dev_fix_docs_urls
fix: documentation URLs in echohub.nuspecch
2026-02-24 22:47:27 +01:00
Stone_Red 92b89fa8a0 fix: github repo URLs in echohub.nuspecch 2026-02-24 22:46:35 +01:00
Stone_Red 9029d63e54 fix: documentation URLs in echohub.nuspecch 2026-02-24 22:42:07 +01:00
HueByte 023e62dea6 fix: update release workflow to correctly handle version checks and Chocolatey publishing conditions 2026-02-24 22:27:30 +01:00
HueByte 36e1ea0dfa fix: update Chocolatey publish condition to check for source changes 2026-02-24 22:18:39 +01:00
Hue be18cf88df Merge pull request #32 from HueByte/dev
chore: update release workflow to include Chocolatey package publishing
2026-02-24 22:14:25 +01:00
Hue 595475c436 Merge branch 'master' into dev 2026-02-24 22:13:31 +01:00
HueByte 1b6c27247f chore: update release workflow to include Chocolatey package publishing 2026-02-24 22:12:26 +01:00
Hue 4a598aa8a8 Merge pull request #31 from HueByte/dev
Dev merge
2026-02-24 22:01:59 +01:00
Hue ed7d0a73a3 Merge branch 'master' into dev 2026-02-24 22:00:48 +01:00
HueByte 6af85559cb chore: update todo list with completed tasks 2026-02-24 21:59:42 +01:00
Stone_Red 43f1b97244 Merge pull request #26 from HueByte/todo_list
Add  todo list
2026-02-24 21:51:42 +01:00
Stone_Red 9baa3ff566 chore: fix lint errors in todo.md 2026-02-24 21:50:44 +01:00
Stone_Red 0330f24e45 Add todo list 2026-02-24 21:48:12 +01:00
Stone_Red ac98845c69 Merge pull request #28 from HueByte/dev_fix_ghost_channel_on_join
fix: ghost channel when trying to join a channel that doesn't exist
2026-02-24 21:47:53 +01:00
Stone_Red d4f540285b chore: add ghost channel fixes to changelog 2026-02-24 21:45:33 +01:00
Stone_Red 3896640ac7 refactor: Move JoinChannelResult to the correct position in ChatDtos.cs 2026-02-24 21:45:33 +01:00
Stone_Red 29bfd87d8b fix: ghost channel when trying to join a channel that doesn't exist 2026-02-24 21:45:33 +01:00
Stone_Red b0adb55b8f refactor: Move JoinChannelResult to the correct position in ChatDtos.cs 2026-02-24 21:45:33 +01:00
Stone_Red 24a5bdb9a1 fix: ghost channel when trying to join a channel that doesn't exist 2026-02-24 21:45:33 +01:00
HueByte 5391d2cb1c fix: improve role tag formatting and handle empty role cases in user status display 2026-02-24 21:44:43 +01:00
HueByte 62996f85e9 fix: add space between role tag and username in status display 2026-02-24 21:15:41 +01:00
Hue ac5348dbb0 Merge pull request #30 from HueByte/dev_bugs_attack
Dev bugs attack
2026-02-24 21:05:11 +01:00
Hue ad5627a4bb Merge branch 'dev' into dev_bugs_attack 2026-02-24 21:03:23 +01:00
Hue c7a5829fac Merge pull request #29 from HueByte/dev_pipelines
Dev pipelines
2026-02-24 21:02:00 +01:00
HueByte a0d9d86956 fix: update markdownlint configuration to allow compact table pipe style 2026-02-24 21:00:51 +01:00
HueByte 0c16f44db6 feat: enhance user presence and channel interaction features
- Fix userlist not refreshing after creating a new channel.
- Implement unmute timer with a background job to automatically unmute users.
- Improve userlist display by filtering invisible users and ensuring proper transitions between statuses.
- Add clickable usernames, @mentions, and #channels for easier navigation.
- Embed theme colors from source sites for a more cohesive UI.
- Introduce a stateful userlist that updates incrementally via SignalR events.
- Restrict auto-opening of files to safe types only, enhancing security.
- Refactor user management into a dedicated service to reduce code duplication.
- Add a MuteExpirationService to handle timed mutes.
- Update documentation with Mermaid diagrams for major flows.
2026-02-24 20:56:40 +01:00
HueByte fb4f6c34ed feat: add detailed documentation for authentication, connection, messaging, channels, moderation, and media flows 2026-02-24 20:50:20 +01:00
Hue 8647b05c12 Merge pull request #27 from HueByte/dev_distribution_update
Dev distribution update
2026-02-24 19:51:11 +01:00
Hue e2bab3d0d9 Merge branch 'dev' into dev_distribution_update 2026-02-24 19:50:11 +01:00
HueByte 6295831045 feat: update nuspec metadata and add PATH setup functionality for terminal access 2026-02-24 19:44:55 +01:00
HueByte 045515369c feat: add Chocolatey package and Linux/macOS install script with automated publishing 2026-02-24 19:44:46 +01:00
Stone_Red fe0e31f9a0 Merge pull request #25 from HueByte/dev_improve_word_wrapping
feat: improve word wrapping in ChatLine
2026-02-24 13:54:36 +01:00
Stone_Red 3273e62b37 feat: improve word wrapping in ChatLine 2026-02-24 13:53:40 +01:00
Hue cf5fca5772 Merge pull request #24 from HueByte/dev_update_adjustments
Dev update adjustments
2026-02-23 19:46:03 +01:00
HueByte 38df05df41 feat: implement rollback functionality with pre-update backup and recovery options | Rebase 2026-02-23 18:15:16 +01:00
Stone_Red 760693befe Merge pull request #22 from HueByte/dev_manual_update_entry
feat: Add "Check for Updates" menu item
2026-02-23 17:50:12 +01:00
Stone_Red 241fee67e8 chore: update AlwaysUpToDate package to version 2.0.2.20250223 2026-02-23 17:20:38 +01:00
Stone_Red 0e3dd932af feat: Add "Check for Updates" menu item 2026-02-23 17:20:38 +01:00
Hue 81b09b1af7 Merge pull request #23 from HueByte/dev_user_irc_improvements
Dev user irc improvements
2026-02-23 15:58:37 +01:00
HueByte 94b31b6056 refactor: replace FakeChatService with FakeUserService for user authentication and profile handling in tests 2026-02-23 15:52:18 +01:00
HueByte 6e76065dcb fix: correct markdown table formatting and specify code block type in Docker documentation 2026-02-23 14:53:58 +01:00
HueByte bdcff74ad5 Refactor user management: Extract IUserService and UserService, consolidate user registration, authentication, and profile management. Fix memory leaks in ApiClient, enhance connection management, and improve error handling in AuthController and UsersController. Update IRC command handling to utilize IUserService for user operations. 2026-02-23 14:51:24 +01:00
HueByte 27a25b1b43 feat: implement user registration and update SASL authentication handling 2026-02-23 14:51:04 +01:00
HueByte 28c4b2993f feat: handle SASL authentication abort scenario in IrcCommandHandler 2026-02-23 10:57:12 +01:00
HueByte 3c760b0bd8 feat: improve IRC command handling with debug logging for incoming messages 2026-02-23 10:54:02 +01:00
HueByte c2a8e9cfc8 feat: enhance SASL authentication logging for better traceability 2026-02-23 10:40:56 +01:00
HueByte 45fd382b13 feat: add Docker support with environment configuration and entrypoint script 2026-02-22 18:12:06 +01:00
HueByte 039390c8b0 feat: add Docker workflow for building and pushing multi-arch server image to GHCR 2026-02-22 18:03:25 +01:00
HueByte c5be63db25 feat: add Docker support for EchoHub.Server with Dockerfile and docker-compose.yml 2026-02-22 17:58:18 +01:00
HueByte 993bb1f973 refactor: enhance audio playback service with semaphore locking for thread safety 2026-02-22 16:43:05 +01:00
Hue 94f968ccf9 Merge pull request #21 from HueByte/dev
Dev
2026-02-22 16:31:37 +01:00
HueByte 4f96b8d986 refactor: consolidate and reorganize code structure; extract classes and improve readability 2026-02-22 16:25:06 +01:00
HueByte fbb2958fdd refactor: remove Terminal.Gui submodule and switch to NuGet package; update version to 0.2.7 2026-02-22 16:23:19 +01:00
HueByte 62e9d7fff5 fix: optimize user fetching after connection failure handling 2026-02-22 16:02:15 +01:00
Hue be4d686d4d Merge pull request #19 from HueByte/dev_cleanups
Dev cleanups
2026-02-22 15:32:03 +01:00
HueByte c8ea124198 Add hex color parsing helper and implement custom list sources for channels and users
- Introduced HexColorHelper for parsing hex color strings to Terminal.Gui Attributes and Colors.
- Created ChannelListSource for managing and rendering a list of channels with unread counts and active indicators.
- Developed UserListSource for displaying online users with per-user nickname colors.
- Refactored MainWindow to utilize ChatMessageManager for handling messages and channel state.
- Updated project references and removed unused content from the server project.
- Adjusted tests to reflect changes in namespaces and structure.
2026-02-22 08:08:21 +01:00
HueByte c5d8e25a86 Add dialogs for connection, channel creation, profile editing, and status management
- Implemented ConnectDialog for server connection and authentication.
- Added CreateChannelDialog for creating new channels with name and topic.
- Developed ProfileEditDialog for editing user profiles including display name, bio, and avatar.
- Created ProfileViewDialog for viewing user profiles with options to edit or set status.
- Introduced StatusDialog for setting user status and status message.
- Added UpdateConfirmDialog for confirming updates and UpdateProgressDialog for showing update progress.
- Removed obsolete hue_icon.ico file from the server.
2026-02-22 08:07:53 +01:00
373 changed files with 43747 additions and 2907 deletions
+93
View File
@@ -0,0 +1,93 @@
# EchoHub Server Configuration
# Copy this file to .env and customize as needed: cp .env.example .env
# These override appsettings.json via ASP.NET Core's configuration hierarchy.
# ── Server ───────────────────────────────────────────────────────────
Server__Name=My EchoHub Server
Server__Description=A self-hosted EchoHub chat server
Server__PublicServer=false
# Registration mode: open (default), invite (codes via /invite, Admin+), or closed.
# Server__Registration=open
# Hostnames advertised to the EchoHubSpace directory. Index per entry.
# Server__PublicHosts__0=echohub.example.com
# Server__PublicHosts__1=alias.example.com
# Topic tags surfaced in the EchoHubSpace browser. Index per entry.
# Server__Tags__0=community
# Server__Tags__1=gaming
# Server__Admins__0=adminUsername
# Override where the EchoHubSpace claim token file is stored (defaults next to the DB).
# Server__DirectoryClaimPath=
# ── JWT ──────────────────────────────────────────────────────────────
# Auto-generated on first run if left empty. Only set if you need a stable secret across containers.
# Jwt__Secret=
# Jwt__Issuer=EchoHub.Server
# Jwt__Audience=EchoHub.Client
# ── Encryption ───────────────────────────────────────────────────────
# Auto-generated on first run if left empty.
# Encryption__Key=
# Encryption__EncryptDatabase=false
# ── Storage ──────────────────────────────────────────────────────────
# Defaults are set in the Dockerfile to use /app/data for persistence.
# Storage__Path=
# Storage__CleanupIntervalHours=1
# Storage__RetentionDays=30
# ── Uploads ──────────────────────────────────────────────────────────
# Uploads__MaxFileSizeMB=100
# Uploads__MaxImageSizeMB=10
# Uploads__MaxAudioSizeMB=10
# Uploads__MaxAvatarSizeMB=2
# Uploads__MaxAttachmentsPerMessage=10
# ── Spam protection ──────────────────────────────────────────────────
# Per-user flood/duplicate limits with auto-mute escalation; Mods and above are exempt.
# Spam__Enabled=true
# Spam__MaxMessagesPerWindow=8
# Spam__WindowSeconds=5
# Spam__MaxDuplicateMessages=3
# Spam__AutoMuteMinutes=5
# Spam__ViolationThreshold=5
# Spam__ViolationWindowMinutes=5
# Only first-time channel joins count — keep this above your public channel count.
# Spam__MaxJoinsPerWindow=25
# Spam__JoinWindowSeconds=30
# Spam__MaxChannelCreatesPerWindow=3
# Spam__ChannelCreateWindowMinutes=10
# ── IRC Gateway ──────────────────────────────────────────────────────
Irc__Enabled=false
# Irc__Port=6667
# Irc__TlsEnabled=false
# Irc__TlsPort=6697
# Irc__TlsCertPath=
# Irc__TlsCertPassword=
# Irc__ServerName=echohub
# Irc__Motd=Welcome to EchoHub IRC Gateway!
# Public base URL used to make attachment links absolute for IRC clients.
# Irc__PublicBaseUrl=https://chat.example.com
# ── CORS ─────────────────────────────────────────────────────────────
# Leave unset to allow all origins. Index per entry to whitelist.
# Cors__AllowedOrigins__0=https://chat.example.com
# ── Logging ──────────────────────────────────────────────────────────
# Serilog__MinimumLevel__Default=Information
# ── Server logs room ─────────────────────────────────────────────────
# Read-only system channel that live-streams Serilog events to Mod+ users.
# ServerLogs__Enabled=true
# ServerLogs__RoomName=server-logs
# ServerLogs__MinRole=Mod
# ServerLogs__MinLevel=Information
# ServerLogs__BacklogLines=100
# ServerLogs__LogDirectory=logs
# ServerLogs__LogFilePattern=echohub-server-*.log
# ── Periodic stats report ────────────────────────────────────────────
# Aggregate activity snapshot logged as JSON and persisted to the DB.
# Stats__Enabled=true
# Stats__IntervalHours=6
# Stats__RetentionDays=90
+12 -12
View File
@@ -14,6 +14,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install pinned linter
run: npm ci
- name: Run markdownlint
run: bash scripts/lint-markdown.sh
@@ -22,12 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# TEMPORARY: Terminal.Gui submodule's nuget.config breaks restore — remove until PR #4234 is merged
- name: Remove submodule NuGet config
run: rm -f src/Terminal.Gui/nuget.config
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
@@ -35,20 +38,17 @@ jobs:
dotnet-version: '10.0.x'
- name: Check formatting
run: dotnet format src/EchoHub.slnx --verify-no-changes --verbosity diagnostic --exclude src/Terminal.Gui/
run: dotnet format src/EchoHub.slnx --verify-no-changes --verbosity diagnostic
build-and-test:
name: Build & Test
# Run the cheap, fast checks first; only spend build/test compute if they pass.
needs: [format-check, lint-markdown]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
# TEMPORARY: Terminal.Gui submodule's nuget.config breaks restore — remove until PR #4234 is merged
- name: Remove submodule NuGet config
run: rm -f src/Terminal.Gui/nuget.config
- name: Check for src/ changes
id: changes
+102
View File
@@ -0,0 +1,102 @@
name: Docker
# Runs only after the CI workflow completes, so an image is never pushed for a commit
# whose formatting, lint, build, or tests failed.
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [master]
workflow_dispatch:
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/huebyte/echohub-server
jobs:
docker:
name: Build & Push Docker Image
# Proceed only for a successful CI run on a master push, or a manual dispatch.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# The exact commit CI tested (workflow_run), or the current tip (manual dispatch).
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Check for src/ changes
id: changes
run: |
if git rev-parse HEAD~1 >/dev/null 2>&1; then
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/' | wc -l)
else
CHANGED=1
fi
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
- name: Read version
if: steps.changes.outputs.src_changed == 'true'
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- name: Check if image tag exists
if: steps.changes.outputs.src_changed == 'true'
id: check_image
run: |
TAG="${{ steps.version.outputs.tag }}"
if docker manifest inspect "${{ env.IMAGE }}:${TAG}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: ./src
file: ./src/EchoHub.Server/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE }}:latest
${{ env.IMAGE }}:${{ steps.version.outputs.tag }}
labels: |
org.opencontainers.image.title=EchoHub Server
org.opencontainers.image.description=Self-hosted IRC-style chat server
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Skip notice
if: steps.changes.outputs.src_changed != 'true' || steps.check_image.outputs.exists == 'true'
run: echo "⏭️ Skipped — no src/ changes or image tag already exists."
-6
View File
@@ -16,12 +16,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# TEMPORARY: Terminal.Gui submodule's nuget.config breaks restore — remove until PR #4234 is merged
- name: Remove submodule NuGet config
run: rm -f src/Terminal.Gui/nuget.config
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
+60
View File
@@ -0,0 +1,60 @@
name: Release Checklist
on:
pull_request:
branches: [master]
workflow_dispatch:
jobs:
release-checklist:
name: Release Checklist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
if [ -z "$VERSION" ]; then
echo "::error file=src/Directory.Build.props::Could not read version from Directory.Build.props"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Check version was bumped from master
run: |
BRANCH_VERSION="${{ steps.version.outputs.version }}"
git fetch origin master --depth=1
MASTER_VERSION=$(git show origin/master:src/Directory.Build.props | grep -oP '(?<=<Version>)[^<]+')
echo "Branch: $BRANCH_VERSION | Master: $MASTER_VERSION"
if [ "$BRANCH_VERSION" = "$MASTER_VERSION" ]; then
echo "::error file=src/Directory.Build.props::Version $BRANCH_VERSION was not bumped from master. Update <Version> in src/Directory.Build.props."
exit 1
fi
- name: Check changelog file exists
run: |
VERSION="${{ steps.version.outputs.version }}"
FILE="docs/changelog/v${VERSION}.md"
if [ ! -f "$FILE" ]; then
echo "::error::Missing changelog file: $FILE"
exit 1
fi
echo "Found: $FILE"
- name: Check changelog TOC
run: |
VERSION="${{ steps.version.outputs.version }}"
if ! grep -q "v${VERSION}.md" docs/changelog/toc.yml; then
echo "::error file=docs/changelog/toc.yml::v${VERSION} not found in changelog TOC. Add it to docs/changelog/toc.yml."
exit 1
fi
if ! grep -q "v${VERSION}" docs/changelog/index.md; then
echo "::error file=docs/changelog/index.md::v${VERSION} not found in changelog index. Add it to docs/changelog/index.md."
exit 1
fi
echo "toc.yml and index.md: OK"
+86 -20
View File
@@ -1,7 +1,11 @@
name: Release
# Runs only after the CI workflow completes, so a release is never published on a commit
# whose formatting, lint, build, or tests failed.
on:
push:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [master]
workflow_dispatch:
@@ -11,31 +15,36 @@ permissions:
jobs:
release:
name: Create Release
# Proceed only for a successful CI run on a master push, or a manual dispatch.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master')
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false' }}
release_exists: ${{ steps.check_release.outputs.exists }}
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
# TEMPORARY: Terminal.Gui submodule's nuget.config breaks restore — remove until PR #4234 is merged
- name: Remove submodule NuGet config
run: rm -f src/Terminal.Gui/nuget.config
# The exact commit CI tested (workflow_run), or the current tip (manual dispatch).
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Check for src/ changes
id: changes
env:
BEFORE: ${{ github.event.before }}
run: |
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
echo "src_changed=true" >> "$GITHUB_OUTPUT"
if git rev-parse HEAD~1 >/dev/null 2>&1; then
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/' | wc -l)
else
CHANGED=$(git diff --name-only "$BEFORE" HEAD -- 'src/' | wc -l)
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
CHANGED=1
fi
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
- name: Read version
if: steps.changes.outputs.src_changed == 'true'
id: version
run: |
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
@@ -43,7 +52,6 @@ jobs:
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- name: Check if release exists
if: steps.changes.outputs.src_changed == 'true'
id: check_release
run: |
if gh release view "${{ steps.version.outputs.tag }}" &>/dev/null; then
@@ -58,7 +66,7 @@ jobs:
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
dotnet-version: "10.0.x"
- name: Publish Server win-x64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
@@ -76,21 +84,29 @@ jobs:
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-arm64 --self-contained true -o publish/server-osx-arm64
- name: Publish Server linux-arm64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r linux-arm64 --self-contained true -o publish/server-linux-arm64
- name: Publish Client win-x64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -o publish/client-win-x64
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-win-x64
- name: Publish Client linux-x64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-x64 --self-contained true -o publish/client-linux-x64
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-linux-x64
- name: Publish Client osx-x64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-x64 --self-contained true -o publish/client-osx-x64
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-osx-x64
- name: Publish Client osx-arm64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -o publish/client-osx-arm64
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-osx-arm64
- name: Publish Client linux-arm64
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-linux-arm64
- name: Zip artifacts
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
@@ -100,10 +116,12 @@ jobs:
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
zip -r ../EchoHub-Server-osx-arm64.zip server-osx-arm64/
zip -r ../EchoHub-Server-linux-arm64.zip server-linux-arm64/
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
zip -r ../EchoHub-Client-osx-arm64.zip client-osx-arm64/
zip -r ../EchoHub-Client-linux-arm64.zip client-linux-arm64/
- name: Build release notes
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
@@ -144,9 +162,57 @@ jobs:
EchoHub-Server-linux-x64.zip \
EchoHub-Server-osx-x64.zip \
EchoHub-Server-osx-arm64.zip \
EchoHub-Server-linux-arm64.zip \
EchoHub-Client-win-x64.zip \
EchoHub-Client-linux-x64.zip \
EchoHub-Client-osx-x64.zip \
EchoHub-Client-osx-arm64.zip
EchoHub-Client-osx-arm64.zip \
EchoHub-Client-linux-arm64.zip
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
choco:
name: Publish to Chocolatey
needs: release
if: needs.release.outputs.release_exists == 'true' || needs.release.outputs.should_release == 'true'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Check if version already published
id: choco_check
shell: pwsh
run: |
$version = "${{ needs.release.outputs.version }}"
$result = choco search echohub --version $version --exact --source https://community.chocolatey.org/api/v2/ 2>&1
if ($result -match "echohub $version") {
echo "exists=true" >> $env:GITHUB_OUTPUT
Write-Host "Chocolatey package echohub $version already published — skipping."
} else {
echo "exists=false" >> $env:GITHUB_OUTPUT
}
- name: Download release asset
if: steps.choco_check.outputs.exists == 'false'
run: gh release download "v${{ needs.release.outputs.version }}" --pattern "EchoHub-Client-win-x64.zip"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Pack and push
if: steps.choco_check.outputs.exists == 'false'
shell: pwsh
run: |
$version = "${{ needs.release.outputs.version }}"
$checksum = (Get-FileHash EchoHub-Client-win-x64.zip -Algorithm SHA256).Hash.ToLower()
# Stamp version and checksum into package templates
(Get-Content packaging/choco/echohub.nuspec) -replace '__VERSION__', $version | Set-Content packaging/choco/echohub.nuspec
(Get-Content packaging/choco/tools/chocolateyInstall.ps1) -replace '__VERSION__', $version | Set-Content packaging/choco/tools/chocolateyInstall.ps1
(Get-Content packaging/choco/tools/chocolateyInstall.ps1) -replace '__CHECKSUM64__', $checksum | Set-Content packaging/choco/tools/chocolateyInstall.ps1
cd packaging/choco
choco pack echohub.nuspec --output-directory $env:TEMP
$pkg = Get-ChildItem "$env:TEMP\echohub.*.nupkg" | Select-Object -First 1
choco push $pkg.FullName --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY
env:
CHOCO_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
-3
View File
@@ -1,3 +0,0 @@
[submodule "src/Terminal.Gui"]
path = src/Terminal.Gui
url = https://github.com/HueByte/Terminal.Gui.git
+6 -1
View File
@@ -6,10 +6,14 @@
"MD013": false,
// Allow duplicate headings in different sections
"MD024": { "siblings_only": true },
// Allow files to open with a centered HTML header block instead of an H1 (README)
"MD041": false,
// Allow inline HTML (docfx uses it)
"MD033": false,
// Allow bare URLs
"MD034": false
"MD034": false,
// Allow compact table pipe style (flow docs use compact tables)
"MD060": false
},
"globs": ["**/*.md"],
@@ -18,6 +22,7 @@
".dev/**",
"docs/_site/**",
"docs/api/**",
"docs/auriondocs/**",
"node_modules/**",
"**/bin/**",
"**/obj/**",
+128 -304
View File
@@ -1,39 +1,23 @@
# EchoHub
<div align="center">
<p align="center">
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub Logo" width="120" />
</p>
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub" width="112" />
<h1 align="center">EchoHub</h1>
<h1>EchoHub</h1>
<p align="center">
No tracking. No subscriptions. No "enhanced AI features". Just chat.
</p>
**No tracking. No subscriptions. No "enhanced AI features". Just chat.**
<p align="center">
<a href="#what-is-this">What</a> •
<a href="#getting-started">Setup</a> •
<a href="#irc-gateway">IRC</a> •
<a href="#deployment-with-nginx">Deploy</a> •
<a href="#client-commands">Commands</a> •
<a href="#configuration">Config</a> •
<a href="#license">License</a>
</p>
Self-hosted, IRC-inspired chat with an open API. Use the terminal, a desktop app, IRC, or a client you build yourself.
<p align="center">
<a href="https://echohub.voidcube.cloud/">Website</a> •
<a href="https://echohub.voidcube.cloud/servers">Public Servers</a> •
<a href="https://huebyte.github.io/EchoHub/">Documentation</a>
</p>
[Website](https://echohub.voidcube.cloud/) · [Public servers](https://echohub.voidcube.cloud/servers) · [Documentation](https://huebyte.github.io/EchoHub/) · [Changelog](docs/changelog/index.md)
<p align="center">
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
<img alt="SignalR" src="https://img.shields.io/badge/SignalR-Real--time-0078D4?style=flat-square" />
<img alt="SQLite" src="https://img.shields.io/badge/SQLite-EF%20Core-003B57?style=flat-square&logo=sqlite&logoColor=white" />
<img alt="License" src="https://img.shields.io/badge/License-MIT-green?style=flat-square" />
<img alt="Terminal.Gui" src="https://img.shields.io/badge/TUI-Terminal.Gui%20v2-yellow?style=flat-square" />
<img alt="Electron" src="https://img.shields.io/badge/Electron-None-red?style=flat-square" />
</p>
<a href="https://github.com/HueByte/EchoHub/actions/workflows/ci.yml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/HueByte/EchoHub/ci.yml?branch=master&style=flat-square&logo=github&label=Build" /></a>
<a href="https://github.com/HueByte/EchoHub/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/HueByte/EchoHub?style=flat-square&logo=github&label=Release" /></a>
<a href="https://community.chocolatey.org/packages/echohub"><img alt="Chocolatey" src="https://img.shields.io/chocolatey/v/echohub?style=flat-square&logo=chocolatey&label=Chocolatey" /></a>
<a href="https://github.com/HueByte/EchoHub/pkgs/container/echohub-server"><img alt="Docker" src="https://img.shields.io/badge/Docker-GHCR-2496ED?style=flat-square&logo=docker&logoColor=white" /></a>
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
<a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/HueByte/EchoHub?style=flat-square" /></a>
</div>
---
@@ -41,313 +25,153 @@
Chat apps used to be simple. You connected to a server, joined a channel, and talked to people. No one was mining your messages for ad targeting, no one was selling your "engagement metrics", and the app didn't need 2GB of RAM to display text.
EchoHub is a return to that. Self-hosted, IRC-inspired chat that runs in your terminal. You own the server, you own the data, and the client won't try to upsell you on a premium tier.
EchoHub is a return to that. Self-hosted, IRC-inspired chat where you own the server and the data, and the client won't try to upsell you on a premium tier.
Each server is fully independent — no central authority, no account federation, no corporate overlord. Just spin one up and go.
Each server is fully independent. No central authority, no account federation, no corporate overlord. Just spin one up and go.
The terminal client in this repo is the main way in: a proper native app that runs right in your terminal, no browser or Electron in sight. But you're not locked to it. The server speaks an open API (REST and SignalR) plus native IRC, so you can reach it however you like. There's already [**decho**](https://github.com/Stone-Red-Code/Decho), any IRC client works fine, and if none of those suit you, writing your own is fair game.
<!--
Screenshots / demo GIF go here once ready. Drop them in docs/images/ and reference like:
<p align="center"><img src="docs/images/demo.gif" alt="EchoHub in action" width="820" /></p>
-->
## Architecture
One server process speaks two protocols: SignalR for native clients and raw IRC for everything else. Both land on the same `ChatService`, so a message from irssi and one from the terminal client look identical by the time they hit the database.
```mermaid
graph TD
subgraph Server["Server"]
ChatSvc["ChatService"]
Hub["SignalR ChatHub"]
IRC["IRC Gateway :6667"]
graph LR
subgraph Clients
direction TB
TUI["Terminal client<br/>Terminal.Gui v2"]
Desk["decho · your app<br/>REST + SignalR"]
IRCC["IRC clients<br/>irssi · WeeChat · …"]
end
subgraph Server["EchoHub Server · ASP.NET Core"]
direction TB
Hub["SignalR Hub"]
GW["IRC Gateway<br/>:6667"]
Chat["ChatService"]
Auth["JWT Auth"]
DB["SQLite DB (EF Core)"]
Files["File Storage"]
DB[("SQLite · EF Core")]
Files[("File store")]
end
subgraph Clients["Clients"]
TUI["Terminal GUI (TUI)"]
IRCClient["IRC Client (irssi, WeeChat, ...)"]
end
TUI == "WebSocket" ==> Hub
TUI -. "REST" .-> Auth
Desk == "WebSocket" ==> Hub
Desk -. "REST" .-> Auth
IRCC == "TCP" ==> GW
Hub --> Chat
GW --> Chat
Auth --> Chat
Chat --> DB
Chat --> Files
TUI -- "WebSocket" --> Hub
TUI -- "REST" --> Auth
IRCClient -- "TCP" --> IRC
Hub --> ChatSvc
IRC --> ChatSvc
ChatSvc --> DB
Auth --> DB
Files --> DB
classDef core fill:#512BD4,stroke:#c3b5ff,color:#ffffff,stroke-width:1.5px;
classDef io fill:#1f6feb,stroke:#9dc1ff,color:#ffffff;
classDef data fill:#0f7b8a,stroke:#7fd3de,color:#ffffff;
classDef client fill:#22272e,stroke:#768390,color:#e6edf3;
class Chat core
class Hub,GW,Auth io
class DB,Files data
class TUI,Desk,IRCC client
```
## What you get
## Highlights
### Server
| | |
| --- | --- |
| 🔒 **Actually private** | Self-hosted, no telemetry. Password rooms are end-to-end encrypted with a key derived from the passphrase on your own machine, so not even the server owner can read them. |
| 🖥️ **Native client, open API** | A real terminal client ships in the box. The REST + SignalR API is open too, so you're never stuck with it: there's the [**decho**](https://github.com/Stone-Red-Code/Decho) desktop app, any IRC client, or roll your own. |
| 💬 **IRC still works** | A built-in gateway drops irssi and WeeChat users into the same rooms as everyone else, live. Actions, replies and presence all carry across. |
| 🛡️ **Moderation, built in** | Four roles with ban, kick, mute and invite-only signup. Spam and flooding earn an automatic timed mute, no plugins to install. |
| 📎 **Files, images, audio** | Uploads are checked by their real bytes, not the file extension. Images even render as ASCII in the terminal, because why not. |
| 📤 **Take your data and go** | One command exports everything the server knows about you. Another deletes the account for good. |
- **Self-hostable** — your server, your rules, your data
- **Real-time messaging** via SignalR WebSockets
- **IRC gateway** — native IRC clients connect alongside TUI users, full cross-protocol messaging
- **JWT auth** with short-lived access tokens and 30-day refresh tokens
- **Channels** — create, set topics, delete (no 47-step permission wizard required)
- **File & image uploads** with actual validation (magic bytes, not just trusting the extension)
- **Image-to-ASCII** — because images in a terminal is objectively cool
- **Presence tracking** — online/away/DND/invisible with custom status messages
- **Rate limiting** — in case someone gets too excited
- **Auto-restart** on crash with exponential backoff — it picks itself back up
- **Serilog logging** — console + rolling file, because `Console.WriteLine` isn't a logging strategy
- **Zero config first run** — generates its own JWT secret and config on launch
Full feature tour in the [documentation](https://huebyte.github.io/EchoHub/articles/getting-started.html).
### Client
## Clients
- **Runs in your terminal** — no browser, no Electron, no 500MB of bundled Chromium
- **13 built-in themes** — including `hacker` for when you want to feel like you're in a movie
- **Slash commands** — `/join`, `/send`, `/status`, `/theme`, etc.
- **Colored nicknames** — pick your hex color, express yourself
- **File/image sharing** — local files or URLs
- **Multi-server** — save and switch between servers
- **Auto-reconnect** — drops happen, it rejoins your channels automatically
- **Message history** on join — you won't miss context
The terminal client is the main, native interface. Beyond that the API and the IRC gateway are open, so you can bring whatever you like.
## Getting Started
| Client | Platform | Notes |
| --- | --- | --- |
| **[Terminal client](src/EchoHub.Client)** | Windows · macOS · Linux | The main, native client (Terminal.Gui v2), shipped as a self-contained binary |
| **[decho](https://github.com/Stone-Red-Code/Decho)** | Desktop | A community desktop client |
| **Any IRC client** | Everywhere | irssi, WeeChat, TheLounge, and friends, through the built-in gateway |
| **Your own** | anything | Build against the open REST + SignalR API. See the [documentation](https://huebyte.github.io/EchoHub/) |
### Download
## Quick start
Grab a self-contained binary from [Releases](../../releases) — no runtime needed, just run it.
### Prerequisites (for development)
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
### Run the Server
### Install the client
```bash
dotnet run --project src/EchoHub.Server
# Windows (Chocolatey)
choco install echohub
# Linux / macOS
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
```
First run does everything for you:
Or grab a self-contained binary from [Releases](../../releases). No runtime required.
1. Creates `appsettings.json` from the example config
2. Generates a secure JWT secret
3. Creates the database with a `#general` channel
### Run the Client
### Host a server
```bash
dotnet run --project src/EchoHub.Client
cp .env.example .env
docker compose up -d
```
Connect, register, chat. That's the whole onboarding flow.
Pre-built multi-arch images live on [GHCR](https://github.com/HueByte/EchoHub/pkgs/container/echohub-server). Prefer running from source? `dotnet run --project src/EchoHub.Server` (needs the [.NET 10 SDK](https://dotnet.microsoft.com/download)). The first launch writes its config, generates a JWT secret, and creates the database with a `#general` channel. No manual setup.
### Build from Source
**[Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html)** · **[Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html)** · **[Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html)**
## Documentation
Everything lives at **[huebyte.github.io/EchoHub](https://huebyte.github.io/EchoHub/)**:
| Guide | What's inside |
| --- | --- |
| [Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html) | Install, first connection, the onboarding flow |
| [TUI guide](https://huebyte.github.io/EchoHub/articles/tui-guide.html) | Slash commands, themes, keybindings, message actions |
| [Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html) | Every `appsettings.json` / env key explained |
| [IRC gateway](https://huebyte.github.io/EchoHub/articles/irc-gateway.html) | Connecting IRC clients, what maps to what, TLS |
| [Encrypted rooms](https://huebyte.github.io/EchoHub/articles/encrypted-rooms.html) | How zero-knowledge password channels work |
| [Moderation](https://huebyte.github.io/EchoHub/articles/moderation.html) | Roles, bans, mutes, invite codes |
| [Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html) | Compose, reverse proxy, TLS (see also [`examples/nginx.conf`](examples/nginx.conf)) |
| [Architecture](https://huebyte.github.io/EchoHub/articles/architecture.html) | How the pieces above actually fit together |
## Building from source
```bash
dotnet build src/EchoHub.slnx
dotnet build src/EchoHub.slnx # build everything
dotnet test src/EchoHub.Tests # run the test suite
```
## IRC Gateway
Contributions are welcome. Open an issue to discuss larger changes first.
EchoHub includes a built-in IRC protocol gateway. Any standard IRC client can connect to the same server and chat alongside TUI users — messages flow both ways in real time.
## Contributors
### Enable It
<div align="center">
In the server's `appsettings.json`:
<sub>The people building EchoHub.</sub>
```json
{
"Irc": {
"Enabled": true,
"Port": 6667,
"ServerName": "echohub",
"Motd": "Welcome to EchoHub IRC Gateway!"
}
}
```
<p align="center">
<a href="https://github.com/HueByte"><img src="https://wsrv.nl/?url=github.com/HueByte.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="HueByte" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/Stone-Red-Code"><img src="https://wsrv.nl/?url=github.com/Stone-Red-Code.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="Stone_Red" /></a>
</p>
### Connect
<p align="center">
<a href="https://github.com/HueByte"><b>HueByte</b></a>
&nbsp;&nbsp;·&nbsp;&nbsp;
<a href="https://github.com/Stone-Red-Code"><b>Stone_Red</b></a>
<br />
<sub>Creators &amp; maintainers</sub>
</p>
```bash
# irssi
irssi -c your-server.com -p 6667 -w <password> -n <username>
# WeeChat
/server add echohub your-server.com/6667 -password=<password> -nicks=<username>
/connect echohub
```
IRC users must have an existing EchoHub account (no registration via IRC). Auth works via `PASS`/`NICK`/`USER` or SASL PLAIN.
### What Works
| Feature | How it maps to IRC |
| ------- | ------------------ |
| Text messages | Standard `PRIVMSG` (long messages split at ~400 byte chunks) |
| Images | `[Image: filename]` + download URL + ASCII art line-by-line |
| File uploads | `[File: filename] /api/files/{id}` |
| Channels | `JOIN`, `PART`, `NAMES`, `TOPIC`, `LIST` |
| Presence | `AWAY`, `WHO`, `WHOIS` |
| Status | Maps to IRC away/here |
### TLS
If running behind **nginx** (recommended), let nginx handle TLS -- see [Deployment with nginx](#deployment-with-nginx) below.
For direct TLS without a reverse proxy, the IRC gateway can terminate TLS itself:
```json
{
"Irc": {
"TlsEnabled": true,
"TlsPort": 6697,
"TlsCertPath": "/path/to/cert.pfx",
"TlsCertPassword": "your-password"
}
}
```
## Client Commands
| Command | Description |
| ------- | ----------- |
| `/join <channel>` | Join a channel |
| `/leave` | Leave current channel |
| `/topic <text>` | Set channel topic (creator only) |
| `/send <file or URL>` | Upload a file or image |
| `/status <online\|away\|dnd\|invisible>` | Set your status |
| `/status <message>` | Set a status message |
| `/nick <name>` | Set display name |
| `/color <#hex>` | Set nickname color |
| `/theme <name>` | Switch theme |
| `/profile` | Open profile editor |
| `/users` | List online users in channel |
| `/servers` | Manage saved servers |
| `/help` | Show help |
| `/quit` | Exit |
## Themes
`/theme <name>` to switch:
| Theme | Vibe |
| ----- | ---- |
| `default` | Gray on black — clean and quiet |
| `transparent` | White on black — for fancy transparent terminals |
| `classic` | White on blue — IRC nostalgia |
| `light` | Black on white — for the brave |
| `hacker` | Green on black — *I'm in* |
| `solarized` | Cyan/yellow on dark gray — for the refined |
| `dracula` | Purple accents on black — the classic dark theme |
| `monokai` | Yellow highlights on black — warm and familiar |
| `nord` | Cool blues — arctic vibes |
| `gruvbox` | Earthy yellows on black — retro warmth |
| `ocean` | Cyan on deep blue — underwater aesthetics |
| `highcontrast` | Bright yellow on black — maximum readability |
| `rosepine` | Muted pinks on black — cozy and soft |
## Configuration
`appsettings.json` is auto-generated on first run. Tweak what you need:
| Key | Default | Description |
| --- | ------- | ----------- |
| `Urls` | `http://0.0.0.0:5000` | Listen address and port |
| `ConnectionStrings:DefaultConnection` | *(empty — app directory)* | SQLite connection string |
| `Jwt:Secret` | *(auto-generated)* | JWT signing key |
| `Server:Name` | `My EchoHub Server` | Server display name |
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
| `Server:PublicServer` | `false` | List on the [public directory](https://echohub.voidcube.cloud/servers) |
| `Server:PublicHost` | *(empty)* | Public hostname for directory listing |
| `Irc:Enabled` | `false` | Enable the IRC gateway |
| `Irc:Port` | `6667` | IRC listen port |
| `Irc:TlsEnabled` | `false` | Enable TLS for IRC |
| `Irc:TlsPort` | `6697` | IRC TLS port |
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
| `Cors:AllowedOrigins` | *(all origins)* | CORS whitelist |
Logging uses Serilog — console + daily rolling files with 14-day retention. Configure in the `Serilog` section.
## Deployment with nginx
Most production deployments run behind nginx. Here's a config that handles both the HTTP/WebSocket server and the IRC gateway:
```nginx
# HTTP + WebSocket (EchoHub Server API + SignalR)
server {
listen 443 ssl;
server_name echohub.example.com;
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for SignalR WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Increase max upload size for file sharing
client_max_body_size 10m;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name echohub.example.com;
return 301 https://$host$request_uri;
}
# IRC TLS (port 6697 → plain IRC on 6667)
stream {
upstream irc_backend {
server 127.0.0.1:6667;
}
server {
listen 6697 ssl;
proxy_pass irc_backend;
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
}
}
```
With this setup, keep the EchoHub IRC gateway's `TlsEnabled` set to `false` — nginx terminates TLS. See the full example at [`examples/nginx.conf`](examples/nginx.conf).
## Project Structure
```text
src/
├── EchoHub.Core/ # Shared models, DTOs, contracts, validation
│ ├── Constants/ # ValidationConstants, HubConstants
│ ├── Contracts/ # IChatService, IChatBroadcaster, IEchoHubClient
│ ├── DTOs/ # Record DTOs
│ └── Models/ # Entity models
├── EchoHub.Server/ # ASP.NET Core server
│ ├── Auth/ # JWT token service
│ ├── Controllers/ # REST API endpoints
│ ├── Data/ # EF Core DbContext + migrations
│ ├── Hubs/ # SignalR ChatHub
│ ├── Services/ # ChatService, presence, file storage, image processing
│ └── Setup/ # First-run setup, DB initialization
├── EchoHub.Server.Irc/ # IRC protocol gateway
│ ├── IrcGatewayService # TCP listener (BackgroundService)
│ ├── IrcCommandHandler # IRC command dispatch (JOIN, PRIVMSG, etc.)
│ ├── IrcBroadcaster # Fans chat events to IRC connections
│ └── IrcMessageFormatter # MessageDto → IRC PRIVMSG lines
├── EchoHub.Client/ # Terminal.Gui TUI client
│ ├── Config/ # Client configuration
│ ├── Services/ # API client, SignalR connection
│ ├── Themes/ # 13 built-in themes
│ └── UI/ # MainWindow, dialogs, chat renderer
└── EchoHub.slnx # Solution file
```
## License
[MIT](LICENSE) — do whatever you want with it.
</div>
+19
View File
@@ -0,0 +1,19 @@
services:
echohub-server:
build:
context: ./src
dockerfile: EchoHub.Server/Dockerfile
# image: ghcr.io/huebyte/echohub-server:latest # use this instead of build for pre-built images
container_name: echohub-server
restart: unless-stopped
ports:
- "5000:5000"
# - "6667:6667" # IRC (plain, no encryption)
# - "6697:6697" # IRC (TLS encrypted, preferred)
volumes:
- echohub-data:/app/data
env_file:
- .env
volumes:
echohub-data:
+1 -1
View File
@@ -52,7 +52,7 @@ Terminal.Gui v2 TUI application:
- **UI**: Main window, dialogs, chat renderer with ANSI color support
- **Services**: API client with automatic token refresh, SignalR connection wrapper, audio playback (NetCoreAudio), automatic update checker (AlwaysUpToDate)
- **Themes**: 13 built-in color themes (including transparent theme with true terminal transparency)
- **Themes**: 14 built-in color themes (including transparent dark/light themes with true terminal transparency)
- **Config**: Client configuration management with session persistence ("Remember Me" refresh tokens)
## Communication
+210
View File
@@ -0,0 +1,210 @@
# Configuration
EchoHub Server generates an `appsettings.json` with sensible defaults on first run (including a random JWT secret), so you can launch and start chatting immediately. Tweak things later when you feel like it.
> [!NOTE]
> Under the hood, EchoHub Server is built on ASP.NET Core, so it inherits the standard .NET configuration system. If you're familiar with that, everything works exactly as you'd expect. If not — no worries, this page covers everything you need.
## How It Works
EchoHub Server loads settings from multiple sources. Each source **overrides** the previous one, so you can layer defaults with environment-specific values:
```text
1. appsettings.json (base defaults)
2. appsettings.{Environment}.json (e.g. appsettings.Production.json)
3. Environment variables (great for Docker / CI)
4. Command-line arguments (highest priority)
```
The last one wins. If `appsettings.json` says `"Irc:Port": 6667` but you pass `--Irc:Port=7000` on the command line, port 7000 is what you get.
In practice this means you can leave `appsettings.json` alone and override just the settings you care about using environment variables or CLI flags — no need to edit JSON files if that's not your thing.
### Environment Variable Mapping
Environment variables use **double underscores** (`__`) in place of the JSON nesting. The rule is simple — replace every `:` (or each level of JSON nesting) with `__`:
| appsettings.json path | Environment variable |
| --- | --- |
| `Server:Name` | `Server__Name` |
| `Irc:Enabled` | `Irc__Enabled` |
| `Jwt:Secret` | `Jwt__Secret` |
| `Serilog:MinimumLevel:Default` | `Serilog__MinimumLevel__Default` |
| `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` |
Arrays use numeric indices: `Server:Admins:0` becomes `Server__Admins__0`, `Server:Admins:1` becomes `Server__Admins__1`, and so on.
This is why the Docker `.env` file uses `Server__Name=My Server` instead of JSON — Docker passes these as environment variables, and the server picks them up automatically.
### Examples
All three of these achieve the same thing — use whichever fits your setup.
**appsettings.json** (direct editing):
```json
{
"Server": {
"Name": "My EchoHub Server",
"PublicServer": true
}
}
```
**Environment variables** (Docker, systemd, shell export):
```bash
export Server__Name="My EchoHub Server"
export Server__PublicServer=true
```
**Command-line arguments** (quick overrides, highest priority):
```bash
./EchoHub.Server --Server:Name="My EchoHub Server" --Irc:Enabled=true
```
## Configuration Reference
The full `appsettings.json` is auto-generated on first run from the [example config](https://github.com/HueByte/EchoHub/blob/master/src/EchoHub.Server/appsettings.example.json). Here's every option:
### General
| Key | Default | Description |
| --- | --- | --- |
| `Urls` | `http://0.0.0.0:5000` | Listen address and port |
| `AllowedHosts` | `*` | Allowed host headers (leave `*` unless you need host filtering) |
### Database
| Key | Default | Description |
| --- | --- | --- |
| `ConnectionStrings:DefaultConnection` | *(empty)* | SQLite connection string. Empty = `echohub.db` in the app directory |
### Authentication
| Key | Default | Description |
| --- | --- | --- |
| `Jwt:Secret` | *(auto-generated)* | Signing key (min 32 chars). Auto-generated on first run |
| `Jwt:Issuer` | `EchoHub.Server` | JWT issuer claim |
| `Jwt:Audience` | `EchoHub.Client` | JWT audience claim |
Access tokens expire after 15 minutes, refresh tokens after 30 days with rotation on each use.
### Server Identity
| Key | Default | Description |
| --- | --- | --- |
| `Server:Name` | `My EchoHub Server` | Display name shown to clients |
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
| `Server:PublicServer` | `false` | Register on the [public directory](https://echohub.voidcube.cloud/servers) |
| `Server:PublicHosts` | `[]` | Hostnames advertised to the directory (array, e.g. `["chat.example.com"]`). Required when `PublicServer` is `true` |
| `Server:Tags` | `[]` | Topic tags surfaced in the directory browser (array, e.g. `["community", "gaming"]`) |
| `Server:Admins` | `[]` | Array of admin usernames (e.g. `["alice", "bob"]`) |
| `Server:Registration` | `open` | Registration mode: `open`, `invite` (codes via `/invite`, Admin+), or `closed` |
| `Server:DirectoryClaimPath` | *(empty)* | Overrides where the directory claim token file is stored. Empty = `directory-claim.json` next to the SQLite database. Treat it as a secret and back it up with the database |
### Uploads
Per-attachment size limits by kind (in megabytes) and the per-message attachment cap. An
absent or partial `Uploads` section keeps the built-in defaults. See
[Messages & Attachments](messages-and-attachments.md) for how kinds are detected.
| Key | Default | Description |
| --- | --- | --- |
| `Uploads:MaxImageSizeMB` | `10` | Max size for one image attachment |
| `Uploads:MaxAudioSizeMB` | `10` | Max size for one audio attachment |
| `Uploads:MaxFileSizeMB` | `100` | Max size for any other attachment |
| `Uploads:MaxAvatarSizeMB` | `2` | Max avatar upload size |
| `Uploads:MaxAttachmentsPerMessage` | `10` | Attachments allowed on a single message |
The server sizes its request-body limits from these values, so raising a limit here is all
that's needed — no separate Kestrel tuning.
### Spam Protection
Per-user flood, duplicate, join, and channel-create limits with auto-mute escalation. Mods and
above are always exempt, and the defaults are lenient enough that a fast typist never trips them.
An absent or partial `Spam` section keeps these defaults.
| Key | Default | Description |
| --- | --- | --- |
| `Spam:Enabled` | `true` | Master switch for all spam protection |
| `Spam:MaxMessagesPerWindow` | `8` | Max messages per `WindowSeconds` before a send is rejected |
| `Spam:WindowSeconds` | `5` | Sliding window (seconds) for the message-rate check |
| `Spam:MaxDuplicateMessages` | `3` | Identical messages in a row tolerated before rejection (E2E rooms are exempt — ciphertext differs each time) |
| `Spam:AutoMuteMinutes` | `5` | Auto-mute duration once a user hits the violation threshold. `0` disables auto-mute (rejections still apply) |
| `Spam:ViolationThreshold` | `5` | Rejected sends within `ViolationWindowMinutes` that trigger an auto-mute |
| `Spam:ViolationWindowMinutes` | `5` | Window (minutes) over which violations accumulate |
| `Spam:MaxJoinsPerWindow` | `25` | Max first-time channel joins per `JoinWindowSeconds` — keep this above your public channel count |
| `Spam:JoinWindowSeconds` | `30` | Sliding window (seconds) for the join-rate check |
| `Spam:MaxChannelCreatesPerWindow` | `3` | Max channel creations per `ChannelCreateWindowMinutes` |
| `Spam:ChannelCreateWindowMinutes` | `10` | Window (minutes) for the channel-create check |
### Encryption
| Key | Default | Description |
| --- | --- | --- |
| `Encryption:Key` | *(auto-generated)* | AES key for message encryption in transit |
| `Encryption:EncryptDatabase` | `false` | Also encrypt message content at rest in SQLite |
### Storage
| Key | Default | Description |
| --- | --- | --- |
| `Storage:Path` | *(empty)* | Directory for uploaded file blobs (attachments, avatars). Empty = `uploads/` in the app directory; the Docker image sets it to `/app/data/uploads` on the persistent volume |
| `Storage:CleanupIntervalHours` | `1` | How often the cleanup job runs (hours) |
| `Storage:RetentionDays` | `30` | Days to keep uploaded files before cleanup |
### IRC Gateway
| Key | Default | Description |
| --- | --- | --- |
| `Irc:Enabled` | `false` | Enable the IRC protocol gateway |
| `Irc:Port` | `6667` | IRC plain-text listen port |
| `Irc:TlsEnabled` | `false` | Enable TLS termination for IRC |
| `Irc:TlsPort` | `6697` | IRC TLS listen port |
| `Irc:TlsCertPath` | *(empty)* | Path to a PKCS#12 (`.pfx`) certificate |
| `Irc:TlsCertPassword` | *(empty)* | Password for the certificate file |
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
### Server Logs Room
When enabled, EchoHub auto-creates a read-only system channel and streams log events to it live,
so operators can watch the server from inside the app. Log lines are **never stored as messages**
the rolling Serilog files remain the only persistence, and the room replays recent lines from those
files when someone opens it.
| Key | Default | Description |
| --- | --- | --- |
| `ServerLogs:Enabled` | `true` | Master switch for the live log room |
| `ServerLogs:RoomName` | `server-logs` | Name of the auto-created channel (reserved — users can't create a channel with this name) |
| `ServerLogs:MinRole` | `Mod` | Minimum server role that can see and join the room (`Member`, `Mod`, `Admin`, `Owner`) |
| `ServerLogs:MinLevel` | `Information` | Minimum log level streamed to the room (`Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`) — affects only the room, not the file/console sinks |
| `ServerLogs:BacklogLines` | `100` | Recent log lines replayed from file when someone opens the room |
| `ServerLogs:LogDirectory` | `logs` | Directory holding the rolling log files (must match the Serilog file sink path below) |
| `ServerLogs:LogFilePattern` | `echohub-server-*.log` | Filename glob for the rolling log files inside `LogDirectory` |
### Logging
EchoHub uses [Serilog](https://serilog.net/) for structured logging — console output + daily rolling files with 14-day retention by default.
| Key | Default | Description |
| --- | --- | --- |
| `Serilog:MinimumLevel:Default` | `Information` | Global log level (`Debug`, `Information`, `Warning`, `Error`) |
| `Serilog:MinimumLevel:Override:Microsoft` | `Warning` | Suppress noisy framework logs |
| `Serilog:MinimumLevel:Override:Microsoft.AspNetCore` | `Warning` | Suppress request pipeline logs |
| `Serilog:MinimumLevel:Override:Microsoft.EntityFrameworkCore` | `Warning` | Suppress database query logs |
Log files are written to `logs/echohub-server-YYYY-MM-DD.log`. To change the path or retention, edit the `Serilog:WriteTo` section in `appsettings.json`.
Want more verbose output for debugging? Set the minimum level to `Debug`:
```bash
# via environment variable
export Serilog__MinimumLevel__Default=Debug
# or command line
./EchoHub.Server --Serilog:MinimumLevel:Default=Debug
```
+121
View File
@@ -0,0 +1,121 @@
# Docker
## Quick Start
```bash
cp .env.example .env # create your config
docker compose up -d # start the server
```
On first run the server automatically generates JWT and encryption keys, creates the database, and seeds a `#general` channel. Connect with the EchoHub client to `http://localhost:5000`.
### Using a Pre-built Image
Instead of building locally, you can pull from GHCR. In `docker-compose.yml`, replace the `build` block:
```yaml
services:
echohub-server:
image: ghcr.io/huebyte/echohub-server:latest
# build:
# context: ./src
# dockerfile: EchoHub.Server/Dockerfile
```
## Configuration
All settings are configured through the `.env` file. These are environment variables that override `appsettings.json` — the `__` (double underscore) maps to JSON nesting levels. For example, `Server__Name` overrides the `Server:Name` key in `appsettings.json`.
See the [Configuration](configuration.md) guide for the full reference of all available settings and how the override hierarchy works.
Common Docker-relevant variables:
| Variable | Default | Description |
| --- | --- | --- |
| `Server__Name` | My EchoHub Server | Display name for your server |
| `Server__Admins__0` | *(empty)* | Admin username (use `__1`, `__2` for more) |
| `Irc__Enabled` | `false` | Enable the IRC gateway |
| `Serilog__MinimumLevel__Default` | `Information` | Log level (`Debug`, `Warning`, etc.) |
## Persistent Data
All server state lives in a single Docker volume mounted at `/app/data`:
```text
/app/data/
├── appsettings.json # generated config with JWT/encryption keys
├── echohub.db # SQLite database
├── uploads/ # uploaded files and avatars
└── logs/ # rolling log files (14-day retention)
```
### Backup
```bash
# stop the server to ensure a consistent snapshot
docker compose stop
# copy the data volume to a local directory
docker cp echohub-server:/app/data ./backup
docker compose start
```
## IRC Gateway
To enable IRC, set these in your `.env`:
```env
Irc__Enabled=true
```
Then uncomment the port in `docker-compose.yml`:
```yaml
ports:
- "5000:5000"
- "6697:6697" # IRC (TLS encrypted, preferred)
```
For TLS, also set:
```env
Irc__TlsEnabled=true
Irc__TlsCertPath=/app/data/cert.pfx
Irc__TlsCertPassword=your_password
```
Mount your certificate into the data volume or bind-mount it directly.
IRC users must have an existing EchoHub account. See [Getting Started](getting-started.md#connect-via-irc) for client connection examples.
## Updating
```bash
# if using pre-built images
docker compose pull
docker compose up -d
# if building locally
docker compose build
docker compose up -d
```
Data persists across updates since it lives in the named volume.
## Troubleshooting
**Port already in use** -- Another process is using port 5000. Change the host port in `docker-compose.yml`:
```yaml
ports:
- "8080:5000" # access via http://localhost:8080
```
**Permission denied on volume** -- The container runs as a non-root `echohub` user (UID 999). If using bind mounts instead of named volumes, ensure the directory is writable.
**View logs** -- Check the container output:
```bash
docker compose logs -f echohub-server
```
File-based logs are also available inside the volume at `/app/data/logs/`.
+160
View File
@@ -0,0 +1,160 @@
# Encrypted Rooms (Password-Protected Channels)
An **encrypted room** is a channel whose entire content — every message and every file — is
end-to-end encrypted with a key derived from a shared passphrase. Only people who know the
passphrase can read the room. **Not even the server owner can read the content**, yet the server
can still gate who joins, and it can count and measure what's stored (message count, file sizes,
timestamps) without ever seeing the plaintext.
This is a stronger guarantee than the [transport and at-rest encryption](encryption.md) described
elsewhere, where the server decrypts every message to process it. Here the server is treated as
*untrusted* for content: it holds only ciphertext and wrapped keys.
> **The passphrase is the only key.** There is no recovery. If everyone who knows a room's
> passphrase forgets it, that room's history is permanently unreadable — by design.
## What the server can and cannot see
| The server **can** see | The server **cannot** see |
| --- | --- |
| That the channel is encrypted | Message text |
| Message count and timestamps | File contents |
| Who sent each message (sender identity) | Image previews (ASCII art) |
| Each attachment's **file name** and byte size | The passphrase, the room key, or the key-encryption key |
| The estimated total size (via `/meta`) | Anything that would let it decrypt the above |
File **names are stored in plaintext** so the file list stays usable — treat a file name itself as
non-secret. Everything *inside* the file is encrypted.
## Key hierarchy
Three keys are derived from one passphrase. The passphrase, the key-encryption key, and the room
content key **never leave the client**.
```mermaid
flowchart TD
P[Passphrase] -->|PBKDF2-SHA256, 210k iterations, per-room salt| OKM[64-byte output]
OKM --> AK["Auth key (first 32 bytes)"]
OKM --> KEK["Key-encryption key / KEK (last 32 bytes)"]
AK -->|sent as lowercase hex| SRV1["Server: BCrypt-hash as the join gate"]
RCK["Room Content Key (random 256-bit)"] -->|encrypts all content| CONTENT[Messages + files + previews]
RCK -->|AES-256-GCM wrap under KEK| WRAP["Wrapped room key"]
WRAP -->|stored| SRV2["Server: stores wrapped key + salt only"]
```
- **Auth key** — the join credential. Derived from the passphrase, sent to the server as hex, and
stored only as a **BCrypt hash**. Proving knowledge of it is what lets you join; it reveals
nothing about the content key.
- **Key-encryption key (KEK)** — never sent. Used locally to *wrap* (encrypt) and *unwrap* the room
content key.
- **Room Content Key (RCK)** — a random 256-bit key generated once, at room creation. It encrypts
every message and file. The server stores it only in wrapped form, so it can hand the wrapped key
to a joiner but can never unwrap it itself.
All content encryption is **AES-256-GCM** with a random 12-byte nonce and a 16-byte authentication
tag per item, so identical inputs never produce identical ciphertext, and any tampering is detected.
Room-encrypted text carries a self-describing prefix so clients and the server can tell it apart
from plaintext:
```text
$RC1$base64(nonce || tag || ciphertext)
```
## Creating a room
The client does all the cryptography locally, then hands the server only what it needs to gate joins
and store (but not read) the content.
```mermaid
sequenceDiagram
participant Client
participant Server
Client->>Client: generate salt + random Room Content Key (RCK)
Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
Client->>Client: wrap RCK under KEK
Client->>Server: create channel { authKey(hex), salt, wrappedRoomKey }
Server->>Server: BCrypt-hash auth key, store salt + wrapped key
Note over Server: Server never receives passphrase, KEK, or RCK
```
## Joining a room
```mermaid
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /crypto → { isEncrypted, salt }
Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
Client->>Server: join { authKey(hex) }
Server->>Server: BCrypt-verify against stored hash
alt correct passphrase
Server->>Client: history (ciphertext) + wrapped room key
Client->>Client: unwrap RCK with KEK, then decrypt everything locally
else wrong passphrase
Server->>Client: rejected (join gate fails)
end
```
A wrong passphrase fails the BCrypt gate, so the server never even hands out the wrapped key. Even if
it did, an attacker without the KEK cannot unwrap it.
## What gets encrypted
When you send a message or attach files to an encrypted room, the client encrypts each part with the
room content key **before** uploading:
- **Message text** → `$RC1$…` ciphertext.
- **Files** (any kind) → the whole blob is AES-256-GCM encrypted client-side; the server stores an
opaque ciphertext blob.
- **Image ASCII previews** → rendered on the client, then room-encrypted. The server never sees the
rendered art.
The server records each attachment's **kind**, **file name**, and **byte size** (of the ciphertext
blob) as metadata, and broadcasts the ciphertext to other members, who decrypt locally.
## Changing the passphrase
`/passwd <old> <new>` rotates the passphrase. Because only the *wrapping* of the room content key
changes — not the RCK itself — **all existing history stays readable**:
1. The client proves knowledge of the old passphrase (old auth key).
2. It unwraps the RCK with the old KEK, then re-wraps it under the new KEK (new salt).
3. It uploads the new auth key + salt + re-wrapped key. The content is never re-encrypted.
## Inspecting a room
Use `/meta` in any channel to see what the server knows about it, including encrypted rooms:
```text
Room info for #private-room:
Room ID 3f2a…-…-…
Created 7/16/2026 2:31 PM
Messages 128
Unique users 4
Est. size 42.5 MB
Protection end-to-end encrypted
```
`Est. size` is the sum of stored attachment blob sizes plus message text length — an estimate of the
room's footprint, computed entirely from metadata the server holds without reading any content.
## Limitations & security notes
- **No recovery.** A lost passphrase means unrecoverable history. Keep it safe; there is no reset.
- **File names are plaintext.** They stay readable so the file list works — don't put secrets in a
file name.
- **IRC is disabled for encrypted rooms.** The IRC gateway forwards plaintext and cannot participate
in the room's key scheme, so encrypted channels are not bridged to IRC.
- **Metadata is visible.** Message counts, timestamps, sender identities, file names, and sizes are
intentionally readable so the server can moderate at the metadata level and report `/meta`.
- **Endpoint trust.** End-to-end encryption protects content from the server and the network, not
from a compromised client device that already holds the passphrase.
## Related
- [Message Encryption](encryption.md) — transport (`$ENC$v1$`) and optional at-rest database
encryption, where the server *does* decrypt content for processing. Encrypted rooms are a separate,
stronger layer that sits on top.
+1 -1
View File
@@ -118,7 +118,7 @@ If you need to recover old encrypted messages, restore the original key from a b
### Limitations
- **TLS-inspecting proxies** — if a corporate proxy terminates TLS with a trusted root CA, it can intercept the key exchange (`GET /api/server/encryption-key`) and read all traffic. A future upgrade to ECDH key exchange would address this.
- **Server has full access** — the server decrypts all messages for processing. This is not end-to-end encryption between users; it's transport encryption between client and server.
- **Server has full access** — the server decrypts all messages for processing. This is not end-to-end encryption between users; it's transport encryption between client and server. For true end-to-end encryption where the server cannot read content, use [encrypted rooms](encrypted-rooms.md).
- **IRC clients receive plaintext** — IRC is an open protocol and third-party clients cannot participate in the encryption scheme.
## Troubleshooting
+44 -13
View File
@@ -1,29 +1,60 @@
# Getting Started
## Prerequisites
## Install the Client
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
### Windows (Chocolatey)
Or grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
```bash
choco install echohub
```
## Run the Server
### Linux / macOS
```bash
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
```
To install a specific version or to a custom directory:
```bash
curl -sSfL .../install.sh | sh -s -- --version 0.2.14
curl -sSfL .../install.sh | sh -s -- --install-dir /opt/echohub
```
### Manual Download
Grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
## Host a Server
### Docker
The quickest way to host a server:
```bash
cp .env.example .env
docker compose up -d
```
See the [Docker guide](docker.md) for configuration, pre-built images, and more.
### From Source
```bash
dotnet run --project src/EchoHub.Server
```
Requires [.NET 10 SDK](https://dotnet.microsoft.com/download).
On first run, the server automatically:
1. Creates `appsettings.json` from the example config
2. Generates a secure JWT secret
3. Creates the SQLite database with a `#general` channel
## Run the Client
```bash
dotnet run --project src/EchoHub.Client
```
## Usage
After installing the client, run `echohub` (or `dotnet run --project src/EchoHub.Client` from source).
Connect to a server, register an account, and start chatting.
## Connect via IRC
@@ -45,17 +76,17 @@ Then connect with any standard IRC client:
irssi -c localhost -p 6667 -w <password> -n <username>
```
IRC users must have an existing EchoHub account. Authentication works via `PASS`/`NICK`/`USER` or SASL PLAIN. Messages flow bidirectionally between IRC and TUI clients.
Your nick is your EchoHub username and the server password is your account password (`PASS`/`NICK`/`USER` or SASL PLAIN). Connecting with a new username registers the account. Messages flow bidirectionally between IRC and TUI clients.
For TLS, set `TlsEnabled: true`, `TlsPort: 6697`, and provide a PKCS#12 certificate path.
See the [Architecture](architecture.md) page for details on how the IRC gateway integrates with the chat service.
See the [IRC Gateway guide](irc-gateway.md) for command mapping, attachment rendering, and limitations, or [Architecture](architecture.md) for how the gateway integrates with the chat service.
## Configuration
Server configuration is in `appsettings.json` (auto-generated on first run). See the [example config](https://github.com/HueByte/EchoHub/blob/master/src/EchoHub.Server/appsettings.example.json) for all available options.
Server configuration is in `appsettings.json` (auto-generated on first run). You can also use environment variables or command-line arguments to override settings.
To list your server on the [public directory](https://echohub.voidcube.cloud/servers), set `Server:PublicServer` to `true` and `Server:PublicHost` to your server's public address.
See the [Configuration](configuration.md) guide for the full reference and how it all works.
## Build from Source
+103
View File
@@ -0,0 +1,103 @@
# IRC Gateway
Every EchoHub server can expose a second door: a built-in **IRC gateway** that speaks the
classic IRC protocol on port 6667. Any standard IRC client — irssi, WeeChat, HexChat,
Halloy — can join the same channels as TUI users, see the same messages, and chat with the
same accounts. Under the hood both protocols call the same chat service, so a message sent
from IRC appears instantly in the TUI and vice versa (see [Architecture](architecture.md)).
## Enabling the gateway
The gateway is off by default. Enable it in `appsettings.json` (or `Irc__Enabled=true` as an
environment variable):
```json
{
"Irc": {
"Enabled": true,
"Port": 6667,
"TlsEnabled": false,
"TlsPort": 6697,
"TlsCertPath": "",
"ServerName": "echohub",
"Motd": "Welcome to EchoHub IRC Gateway!"
}
}
```
The plaintext listener always starts on `Port`. The TLS listener on `TlsPort` starts only
when `TlsEnabled` is `true` **and** `TlsCertPath` points to a PKCS#12 (`.pfx`) certificate.
See the [configuration reference](configuration.md#irc-gateway) for every option.
## Connecting & authentication
Your IRC **nick is your EchoHub username** and your server password is your **account
password**. Two flows are supported:
```bash
# classic PASS/NICK/USER — most clients call this the "server password"
irssi -c chat.example.com -p 6667 -w <password> -n <username>
```
or **SASL PLAIN** (advertised via `CAP LS`), where the SASL username/password are the account
credentials.
A few things worth knowing:
- **Connecting auto-registers.** If the username doesn't exist yet, the gateway creates the
account with that password (usernames: 350 chars of `a-z 0-9 _ -`; passwords: 6+ chars).
The very first account ever created on a server becomes the **Owner**.
- Because of that, a typo'd password for an *existing* account fails with
`Username is already taken` — the gateway tried to log in, couldn't, then tried to register
the name. If you see that error, re-check your password.
- Connecting without a password is rejected: `Password required. Use PASS command or SASL PLAIN.`
## What maps to what
| IRC | EchoHub |
| --- | --- |
| `JOIN #room` | Join a channel (history is replayed on join) |
| `JOIN #room <key>` | Join a password-protected (`+k`) channel |
| `PART` / `QUIT` | Leave channel / disconnect |
| `LIST` | Public channels only (password-protected ones show a `[+k]` hint) |
| `TOPIC` | Read or set the channel topic (permission-checked) |
| `NAMES` / `WHO` | Online users in the channel |
| `WHOIS` | Profile: display name, channels, idle time, away status |
| `AWAY [message]` | Sets your EchoHub status to Away / back to Online |
| `MODE #room +k <key>` / `-k` | Set / clear the channel password |
Private (unlisted) channels don't appear in `LIST`, but members who know the exact name can
still `JOIN` them. Channels are not auto-created from IRC — create them from the TUI first.
## How messages look
- **Attachments** arrive as labeled link lines — `[Image: photo.png] https://…`,
`♪ [Audio: song.mp3] https://…`, `[File: report.pdf] https://…` — and image attachments
additionally render their **ASCII-art preview** using truecolor ANSI escapes, so a modern
terminal IRC client shows actual picture previews.
- **Link embeds** are appended as `│`-prefixed text lines.
- Long messages are split at word boundaries into IRC-safe lines (~400 bytes each);
incoming messages may be up to 2,000 characters like any EchoHub message.
- Your own messages aren't echoed back (standard IRC convention).
- Moderation actions surface natively: kicks arrive as `KICK`, bans and channel nukes as
server `NOTICE`s.
## Limitations
The gateway bridges what IRC can express — and deliberately refuses what it can't:
- **No end-to-end encrypted rooms.** Joining an [encrypted room](encrypted-rooms.md) fails
with *"Cannot join channel — end-to-end encrypted, use the EchoHub client."* Bridging one
would require the server to hold the room key, breaking the zero-knowledge design.
- **No private messages.** `PRIVMSG` to a nick is rejected; EchoHub is channel-based.
- **Usernames, not display names.** Messages are attributed to the account username;
a user's display name is visible via `WHOIS`/`WHO` (realname field).
- **No client features.** Uploading attachments, profiles, themes, and reactions to status
changes are TUI-client features. Other users' status changes aren't pushed to IRC —
discover them with `WHOIS`/`WHO`.
## How IRC users appear to TUI users
Users connected *only* through the gateway are tagged `[irc]` in the users panel — a hint
that they can't receive encrypted content or use client-side features. Someone connected
with both an IRC client and the TUI shows untagged.
+109
View File
@@ -0,0 +1,109 @@
# Messages & Attachments
An EchoHub message is **text content plus up to 10 attachments**, Discord-style. A plain chat
line is just a message with no attachments; a photo dump is one message with several files and
an optional caption. This page explains how to attach files, what happens to them on the way to
the server, and how other clients receive them.
## Message basics
| Limit | Value |
| --- | --- |
| Max message length | 2,000 characters |
| Max newlines per message | 30 (no blank-line runs) |
| Max attachments per message | 10 |
| Link embeds per message | first 3 URLs |
Multiline messages are written with `Ctrl+N` for a newline; `Enter` sends. URLs in a message
get link embeds (title, description, theme color) fetched by the server.
## Attaching files
All of these end up in the same place — the **staging tray** — and are sent together as one
message the next time you press `Enter`, with whatever you've typed as the caption:
- **Paste a copied file** — copy one *or several* files in your file manager and press
`Ctrl+V` in the input. All of them are staged at once.
- **Paste an image from the clipboard** — copy an image in a browser (right-click → *Copy
image*), take a screenshot (`Win+Shift+S`), or copy from an image editor, then `Ctrl+V`.
The image is attached directly as a PNG named `image.png` — no saving to disk first.
On Linux this uses `wl-paste` or `xclip`; on macOS it requires
[`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`).
- **Drag & drop** — drop a file onto the terminal window; the client recognizes the dropped
path and stages the file.
- **`/send <filepath>`** — stage a file by path (quote paths containing spaces).
The input frame's title shows what's currently staged. `/clear` drops all staged attachments
without sending. Sending with an empty input is fine — the message is just the attachments.
```text
┌ Message (2 attached: report.pdf, image.png) ──────────────┐
│ here's the summary and a screenshot_ │
└────────────────────────────────────────────────────────────┘
```
**URL sends are different:** `/send <https://…>` sends an image URL immediately as its own
message — nothing is staged, and it isn't available in end-to-end encrypted rooms (the server
would have to fetch the image, which would defeat the encryption).
## Attachment kinds
The kind is detected per attachment, not per message:
| Kind | Detected by | Renders as | Default size limit |
| --- | --- | --- | --- |
| **Image** | Magic bytes: JPEG, PNG, GIF, WebP | ASCII-art preview in chat | 10 MB |
| **Audio** | Extension: `.mp3` `.wav` `.ogg` `.flac` `.aac` `.m4a` `.wma` | Playable row (▶) | 10 MB |
| **File** | Everything else | Downloadable row | 100 MB |
Limits are per file and server-configurable — see the `Uploads` section in the
[configuration guide](configuration.md) (`MaxImageSizeMB`, `MaxAudioSizeMB`, `MaxFileSizeMB`,
`MaxAttachmentsPerMessage`).
## Image previews (ASCII art)
Images are rendered in chat as half-block ASCII art. You pick the rendering size:
| Flag | Size | Feel |
| --- | --- | --- |
| `-s` / `/size s` | 40 × 40 | compact |
| `-m` / `/size m` | 80 × 80 | default |
| `-l` / `/size l` | 120 × 120 | detailed |
`/size` with no argument opens a picker; the choice persists as your default. A one-off
`-s|-m|-l` flag on `/send` applies to that message.
## Receiving attachments
Right-click a message (or press `F6` to select one with the arrow keys) for actions:
- **Images** → save to disk
- **Audio** → play (in-client playback)
- **Files** → download
Downloads go to your configured download folder — set it with `/downloadpath` (no argument
opens a native folder picker, or pass a path directly).
## Attachments in encrypted rooms
In an [end-to-end encrypted room](encrypted-rooms.md) every attachment is encrypted
client-side **before** upload:
```mermaid
flowchart LR
F[File bytes] -->|AES-256-GCM with room key| B[Ciphertext blob]
F -->|if image: render ASCII locally| A[ASCII preview]
A -->|room-encrypt| AP["$RC1$… preview"]
B --> S[Server stores blob + name + size]
AP --> S
```
The server never sees the file contents or the rendered preview — it stores an opaque blob and
broadcasts it to members, who decrypt locally. File **names and sizes remain visible** to the
server so the file list stays usable; don't put secrets in a file name. Pasted clipboard
images go through exactly the same pipeline.
## Deleting messages with attachments
Deleting a message also removes its uploaded attachment files from the server. You can always
delete your own messages; moderators can delete others' — see [Moderation & Roles](moderation.md).
+71
View File
@@ -0,0 +1,71 @@
# Moderation & Roles
Every EchoHub server has a four-tier role hierarchy. Moderation is **strictly hierarchical**:
acting on another user requires outranking them — equal rank is never enough — and a few
invariants protect the server owner from lockouts.
## Roles
| Role | Rank | Users panel glyph | How it's granted |
| --- | --- | --- | --- |
| **Owner** | 3 | ★ | The first account ever registered on the server |
| **Admin** | 2 | ♦ | Assigned by the Owner |
| **Mod** | 1 | ❀ | Assigned by an Admin or the Owner |
| **Member** | 0 | — | Everyone else |
Assign roles with `/role <user> <admin|mod|member>`. Two rules apply:
- You can only assign roles **strictly below your own** — an Admin can promote to Mod but
cannot create another Admin; only the Owner can.
- **Owner is not assignable and not demotable.** There is exactly one Owner (the first
account), nobody can be promoted to it, and the Owner's role can't be changed.
## Actions
| Command | Minimum role | Effect |
| --- | --- | --- |
| `/kick <user> [reason]` | Mod | Disconnects the user. Not persistent — they can reconnect immediately. |
| `/ban <user> [reason]` | Admin | Persistent: flags the account banned and disconnects it. Banned accounts are rejected at login. |
| `/unban <user>` | Admin | Lifts a ban. |
| `/mute <user> [minutes]` | Mod | Blocks the user from sending messages or uploading files. Without a duration the mute is **indefinite**; with one it auto-expires (checked every ~15 seconds). |
| `/unmute <user>` | Mod | Lifts a mute early. |
| `/role <user> <role>` | Admin | Assign a role (see rules above). |
| `/nuke` | Mod | Deletes the **entire history of the current channel**, including all attachment files on disk. Channel-wide — no per-user check. |
Kick, ban, and mute all enforce the hierarchy: the target's role must be **strictly lower**
than yours. A Mod cannot kick another Mod; nobody can kick, ban, mute, or demote the Owner.
## Deleting messages
Deletion has its own, slightly different rule set:
- **Your own messages** — always deletable, whatever your role. Right-click a message →
*Delete message*, or press `F6`, pick the message, and hit `Delete`.
- **Someone else's messages** — requires **Mod or higher** *and* strictly outranking the
author. A Mod can delete a Member's message, but not another Mod's.
Deleting a message also purges its uploaded attachment blobs from the server's disk, and the
removal is broadcast live — the message disappears from everyone's chat immediately.
## How actions surface
Everyone in the channel sees moderation happen:
- **TUI clients** show system messages — *"alice was kicked (reason)"*, *"bob was banned"*,
*"Channel history has been cleared by a moderator."* The kicked or banned user themselves
gets a dialog with the reason, then the client disconnects.
- **IRC clients** get native protocol events: kicks arrive as a real `KICK` command, bans as
a server `NOTICE`. (See the [IRC Gateway guide](irc-gateway.md).)
Muted users aren't announced; they simply receive *"You are muted and cannot send messages."*
when they try to speak.
## Design notes
- All checks run server-side in the moderation API — the client commands are conveniences,
and the same rules bind IRC users and any direct API caller.
- Bans are account-level, not IP-level. A banned person can register a fresh account; pair
bans with registration hygiene on public servers.
- In [end-to-end encrypted rooms](encrypted-rooms.md) moderation still works at the metadata
level — messages can be deleted and users muted/kicked by identity — but no moderator can
*read* the content, including the Owner.
+22 -4
View File
@@ -1,8 +1,26 @@
- name: Getting Started
- name: Guides
items:
- name: Getting Started
href: getting-started.md
- name: Architecture
- name: TUI Guide
href: tui-guide.md
- name: Messages & Attachments
href: messages-and-attachments.md
- name: Moderation & Roles
href: moderation.md
- name: IRC Gateway
href: irc-gateway.md
- name: Docker
href: docker.md
- name: Architecture
href: architecture.md
- name: Encryption
- name: Configuration
href: configuration.md
- name: Encryption
href: encryption.md
- name: Notification Sounds
- name: Encrypted Rooms
href: encrypted-rooms.md
- name: Notification Sounds
href: notification-sounds.md
- name: Flows
href: ../flows/toc.yml
+116
View File
@@ -0,0 +1,116 @@
# TUI Guide
Everything you can do in the EchoHub terminal client: keyboard shortcuts, mouse actions,
slash commands, themes, and the everyday behaviors (unread markers, auto-join, scrollback)
that make it feel like a proper IRC-era client with modern comforts.
## Layout
```text
┌ Menu bar ──────────────────────────────────────────────────┐
│ ┌ Channels ─┐ ┌ Messages ────────────────────┐ ┌ Users ──┐ │
│ │ #general 3│ │ 12:01 <alice> hi │ │ ★ alice │ │
│ │ #dev │ │ ── new messages ── │ │ ❀ bob │ │
│ │ #random*~ │ │ 12:04 <bob> anyone around? │ │ carol │ │
│ └───────────┘ └──────────────────────────────┘ │ d [irc] │ │
│ ┌ Message │ Enter=send │ Tab=complete │ … ────┐ └─────────┘ │
│ │ _ │ │
│ └─────────────────────────────────────────────┘ │
│ Status: Connected │ v0.2.14 │ alice │ Act: #dev │
└─────────────────────────────────────────────────────────────┘
```
Channel list markers: `*` = password-protected, `~` = private (unlisted), plus unread counts
(orange when you were @mentioned). Users panel glyphs: `★` Owner, `♦` Admin, `❀` Mod,
`[irc]` for IRC-gateway-only users; status icons `●`/`○`/`◐`/`◌` for online/offline/away/dnd.
## Keyboard shortcuts
### In the message input
| Key | Action |
| --- | --- |
| `Enter` | Send the message (also sends staged attachments with the text as caption) |
| `Ctrl+N` | Insert a newline (multiline message) |
| `Tab` | Autocomplete a slash command (`/th``/theme`) |
| `Ctrl+V` (or `Ctrl+Y`) | Paste — copied files and images become attachments, text pastes normally ([details](messages-and-attachments.md)) |
| `Ctrl+C` / `Ctrl+X` | Copy / cut in the input |
| `Ctrl+W` | Delete the word left of the cursor |
| `Ctrl+K` | Open the search palette |
| `F6` | Move focus into the message list |
### In the message list (after `F6`)
| Key | Action |
| --- | --- |
| `↑` / `↓` | Select a message |
| `Enter` | Activate: play/download/save an attachment, open an `@mention`'s profile, join a `#channel`, or open the sender's profile |
| `Delete` / `Backspace` | Delete the selected message (with confirmation; [permission rules](moderation.md)) |
| `F6` | Return focus to the input |
### Anywhere
| Key | Action |
| --- | --- |
| `F2` | Toggle the users panel |
| `Ctrl+K` | Search palette |
| `Alt+Q` | Quit |
## The search palette (`Ctrl+K`)
A command-palette that searches **channels and app actions** — type to filter, `↓` to
navigate, `Enter` to jump. Actions include Connect, Disconnect, Logout, My Profile,
Set Status, Create/Delete Channel, Saved Servers, Toggle Users Panel, Check for Updates,
and Quit. `Ctrl+K` again closes it.
## Mouse
- **Right-click a message** for the context menu: save image / play audio / download file
(depending on the attachment), *Mention @user*, *View profile*, *Copy text*,
*Copy message ID*, *Delete message*.
- **Left-click a message** does the most useful thing for that line: attachments
play/download/save, `@mentions` and the sender open profiles, `#channel` references join
that channel.
- **Click a user** in the users panel to open their profile; **click a channel** to switch.
## Slash commands
Type `/help` in any channel for the full list. The highlights:
| Command | What it does |
| --- | --- |
| `/status <online\|away\|dnd\|invisible>` or `/status <message>` | Presence / status message |
| `/nick <name>`, `/color <#hex>`, `/avatar <url or path>` | Display name, nick color, avatar |
| `/theme <name>` | Switch theme |
| `/send`, `/clear`, `/size`, `/downloadpath` | Attachments — see [Messages & Attachments](messages-and-attachments.md) |
| `/join <channel> [password]`, `/leave`, `/topic <text>` | Channel membership and topic |
| `/passwd <old> <new>` | Rotate an encrypted room's passphrase |
| `/profile [user]`, `/users`, `/meta` | Profiles, online users, room info |
| `/kick`, `/ban`, `/mute`, `/role`, `/nuke`, … | [Moderation](moderation.md) |
| `/servers`, `/quit` | Saved servers, exit |
Emoji shortcodes (`:smile:` style) are replaced live as you type.
## Themes
14 built-in themes: **Default, Transparent, TransparentLight, Classic, Light, Hacker,
Solarized, Dracula, Monokai, Nord, Gruvbox, Ocean, HighContrast, RosePine** — switch from
the User menu or `/theme <name>`. The two *Transparent* themes use no background color at
all, so your terminal's own background (and any blur/acrylic) shows through.
You can add your own: drop a theme JSON into `~/.echohub/themes/` and it appears in the list
(names that collide with a built-in are skipped).
## Everyday behaviors
- **Unread markers** — a `── new messages ──` rule marks where you left off in each channel,
irssi-style. Read positions are **persisted per server**, so the marker survives
reconnects and restarts. The status bar's `Act:` segment lists channels with activity
(orange when you were @mentioned), and day boundaries draw a date rule.
- **Auto-join** — connecting joins `#general` plus every channel you're a member of, so
unread counts and mentions accumulate everywhere. Channels you `/leave` stay left, and
password-protected or [encrypted rooms](encrypted-rooms.md) are never auto-prompted —
join those explicitly. `#general` is the home channel and can't be left or deleted.
- **Scrollback** — history loads 100 messages at a time; scrolling to the top of a channel
fetches the next page and keeps your position (no jump).
- **Drag & drop** — dropping a file onto the window stages it as an attachment.
+18
View File
@@ -0,0 +1,18 @@
# Agent Context Pack — HueByte/EchoHub
Machine-oriented context for AI coding agents, generated deterministically from
the repository's symbol graph (no LLM involved). Reading this pack instead of
scanning the source tree saves tokens: one map read plus targeted doc lookups
replaces repeated file reads and greps.
| File | What it is | When to read it |
|---|---|---|
| [repo-map](repo-map.md) | Subsystem map + most-connected symbols | First — orientation |
| [symbol-graph.json](symbol-graph.json) | Full queryable symbol graph (kind, file, subsystem, complexity, dependencies, doc path) | To trace exact dependencies or find a symbol |
| `../Code/**` | Per-file generated documentation | For the behaviour of a specific file |
| `../Synthesis/Architecture.md` | Cross-cutting architecture narrative + diagram | For the big picture |
Suggested workflow: read `repo-map.md`; locate the symbols relevant to your task in
`symbol-graph.json` (each carries a `doc` path and `dependsOn` edges); open only
those docs/sources. The pack is regenerated on every full documentation run, so it
matches the commit recorded in the graph's `commit` field.
+187
View File
@@ -0,0 +1,187 @@
# Repo Map — HueByte/EchoHub
> Deterministic structural map generated for AI agents. Read this file for
> orientation, query [`symbol-graph.json`](symbol-graph.json) for exact
> dependencies, and open the linked per-file docs for behaviour — instead of
> scanning the source tree.
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.Client/Services · AppOrchestrator
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`
- [`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
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`
- *…and 14 more (see symbol-graph.json)*
### src/EchoHub.Core/DTOs · ApiClient
19 symbols across 11 files. Key symbols (by connectivity):
- [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) (class) — `src/EchoHub.Client/Services/ApiClient.cs`
- [`ModerationController`](../Code/src/EchoHub.Server/Controllers/ModerationController.cs.md) (class) — `src/EchoHub.Server/Controllers/ModerationController.cs`
- [`ServerStatsCollector`](../Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md) (class) — `src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs`
- [`RefreshRequest`](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/AuthDtos.cs`
- [`AssignRoleRequest`](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ModerationDtos.cs`
- [`UpdateProfileRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
- [`AvatarHelper`](../Code/src/EchoHub.Client/Services/AvatarHelper.cs.md) (class) — `src/EchoHub.Client/Services/AvatarHelper.cs`
- [`OutgoingAttachment`](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) (record) — `src/EchoHub.Client/Services/OutgoingAttachment.cs`
- *…and 11 more (see symbol-graph.json)*
### src/EchoHub.Server · User
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`
- [`EchoHubDbContext`](../Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md) (class) — `src/EchoHub.Server/Data/EchoHubDbContext.cs`
- [`ServerRole`](../Code/src/EchoHub.Core/Models/ServerRole.cs.md) (enum) — `src/EchoHub.Core/Models/ServerRole.cs`
- [`AuthController`](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md) (class) — `src/EchoHub.Server/Controllers/AuthController.cs`
- [`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 11 more (see symbol-graph.json)*
### src/EchoHub.Core/DTOs · ChannelService
13 symbols across 9 files. Key symbols (by connectivity):
- [`ChannelService`](../Code/src/EchoHub.Server/Services/ChannelService.cs.md) (class) — `src/EchoHub.Server/Services/ChannelService.cs`
- [`IrcCommandHandler`](../Code/src/EchoHub.Server.Irc/IrcCommandHandler.cs.md) (class) — `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
- [`IChannelService`](../Code/src/EchoHub.Core/Contracts/IChannelService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChannelService.cs`
- [`ChannelOperationResult`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
- [`FakeChannelService`](../Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md) (class) — `src/EchoHub.Tests/Irc/TestHelpers.cs`
- [`ValidationConstants`](../Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md) (class) — `src/EchoHub.Core/Constants/ValidationConstants.cs`
- [`ChannelCryptoDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`ChannelError`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (enum) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
- *…and 5 more (see symbol-graph.json)*
### src/EchoHub.Server · Program
13 symbols across 12 files. Key symbols (by connectivity):
- [`Program`](../Code/src/EchoHub.Server/Program.cs.md) (file) — `src/EchoHub.Server/Program.cs`
- [`HubConstants`](../Code/src/EchoHub.Core/Constants/HubConstants.cs.md) (class) — `src/EchoHub.Core/Constants/HubConstants.cs`
- [`PresenceTracker`](../Code/src/EchoHub.Server/Services/PresenceTracker.cs.md) (class) — `src/EchoHub.Server/Services/PresenceTracker.cs`
- [`ImageToAsciiService`](../Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md) (class) — `src/EchoHub.Core/Services/ImageToAsciiService.cs`
- [`UploadLimits`](../Code/src/EchoHub.Server/Config/UploadLimits.cs.md) (class) — `src/EchoHub.Server/Config/UploadLimits.cs`
- [`DatabaseSetup`](../Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md) (class) — `src/EchoHub.Server/Setup/DatabaseSetup.cs`
- [`DirectoryClaimStore`](../Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md) (class) — `src/EchoHub.Server/Services/DirectoryClaimStore.cs`
- [`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.Core/Contracts · ChannelDto
9 symbols across 9 files. Key symbols (by connectivity):
- [`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
7 symbols across 1 files. Key symbols (by connectivity):
- [`ServerDirectoryService`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`ErrorDetail`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`Response`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`DirectoryProtocol`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`DirectoryRegistrationErrors`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`RegisterServerDto`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
- [`RegisterServerResult`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
### src/EchoHub.Client/UI · Channel
6 symbols across 4 files. Key symbols (by connectivity):
- [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) (class) — `src/EchoHub.Core/Models/Channel.cs`
- [`SearchDialog`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
- [`SearchResultType`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (enum) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
- [`SearchListSource`](../Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md) (class) — `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
- [`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):
- [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) (class) — `src/EchoHub.Server/Controllers/ChannelsController.cs`
- [`RekeyChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`FileValidationHelper`](../Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md) (class) — `src/EchoHub.Core/Services/FileValidationHelper.cs`
- [`CreateChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`SendUrlRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
- [`UpdateTopicRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
### src/EchoHub.Core/Models
6 symbols across 6 files. Key symbols (by connectivity):
- [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) (class) — `src/EchoHub.Core/Models/Message.cs`
- [`DataMigrationService`](../Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md) (class) — `src/EchoHub.Server/Setup/DataMigrationService.cs`
- [`Attachment`](../Code/src/EchoHub.Core/Models/Attachment.cs.md) (class) — `src/EchoHub.Core/Models/Attachment.cs`
- [`AsyncRunner`](../Code/src/EchoHub.Client/Services/AsyncRunner.cs.md) (class) — `src/EchoHub.Client/Services/AsyncRunner.cs`
- [`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 11 smaller subsystems (see symbol-graph.json).*
## Most connected symbols
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 | 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` |
| [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) | class | 29 | 1 | `src/EchoHub.Core/Models/Channel.cs` |
| [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) | class | 2 | 25 | `src/EchoHub.Client/UI/MainWindow.cs` |
| [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) | class | 21 | 4 | `src/EchoHub.Core/Models/Message.cs` |
| [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) | class | 0 | 25 | `src/EchoHub.Server/Controllers/ChannelsController.cs` |
| [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) | class | 1 | 22 | `src/EchoHub.Client/Services/ConnectionManager.cs` |
| [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) | record | 20 | 3 | `src/EchoHub.Core/DTOs/ChatDtos.cs` |
*Regenerated on every full documentation run; see [README](README.md) for how to use this pack.*
File diff suppressed because it is too large Load Diff
+229
View File
@@ -0,0 +1,229 @@
# HueByte/EchoHub — Documentation
*Generated from branch `master`*
*Commit: `40aea9a0`*
## Languages
- **C#**: 128 files
## Statistics
- **Files processed:** 128
- **Lines of code:** 22,133
- **Symbols documented:** 617
- **Validation retries:** 69
## Documentation Index
### src/EchoHub.Client
- [AppOrchestrator](src/EchoHub.Client/AppOrchestrator.cs.md)
- [Program](src/EchoHub.Client/Program.cs.md)
### src/EchoHub.Client/Commands
- [CommandHandler](src/EchoHub.Client/Commands/CommandHandler.cs.md)
### src/EchoHub.Client/Config
- [ClientConfig](src/EchoHub.Client/Config/ClientConfig.cs.md)
- [ConfigManager](src/EchoHub.Client/Config/ConfigManager.cs.md)
### src/EchoHub.Client/Services
- [ApiClient](src/EchoHub.Client/Services/ApiClient.cs.md)
- [AsyncRunner](src/EchoHub.Client/Services/AsyncRunner.cs.md)
- [AudioPlaybackService](src/EchoHub.Client/Services/AudioPlaybackService.cs.md)
- [AvatarHelper](src/EchoHub.Client/Services/AvatarHelper.cs.md)
- [ClientEncryptionService](src/EchoHub.Client/Services/ClientEncryptionService.cs.md)
- [ClipboardFiles](src/EchoHub.Client/Services/ClipboardFiles.cs.md)
- [ClipboardImage](src/EchoHub.Client/Services/ClipboardImage.cs.md)
- [ConnectionManager](src/EchoHub.Client/Services/ConnectionManager.cs.md)
- [EchoHubConnection](src/EchoHub.Client/Services/EchoHubConnection.cs.md)
- [NativeFolderPicker](src/EchoHub.Client/Services/NativeFolderPicker.cs.md)
- [NotificationSoundService](src/EchoHub.Client/Services/NotificationSoundService.cs.md)
- [OutgoingAttachment](src/EchoHub.Client/Services/OutgoingAttachment.cs.md)
- [PathSetup](src/EchoHub.Client/Services/PathSetup.cs.md)
- [RoomKeyProtector](src/EchoHub.Client/Services/RoomKeyProtector.cs.md)
- [RoomKeyStore](src/EchoHub.Client/Services/RoomKeyStore.cs.md)
- [UpdateBackupService](src/EchoHub.Client/Services/UpdateBackupService.cs.md)
- [UpdateChecker](src/EchoHub.Client/Services/UpdateChecker.cs.md)
- [UserSession](src/EchoHub.Client/Services/UserSession.cs.md)
### src/EchoHub.Client/Themes
- [Theme](src/EchoHub.Client/Themes/Theme.cs.md)
- [ThemeManager](src/EchoHub.Client/Themes/ThemeManager.cs.md)
### src/EchoHub.Client/UI
- [MainWindow](src/EchoHub.Client/UI/MainWindow.cs.md)
### src/EchoHub.Client/UI/Chat
- [ChatColors](src/EchoHub.Client/UI/Chat/ChatColors.cs.md)
- [ChatLine](src/EchoHub.Client/UI/Chat/ChatLine.cs.md)
- [ChatListSource](src/EchoHub.Client/UI/Chat/ChatListSource.cs.md)
- [ChatMessageManager](src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md)
- [ChatSegment](src/EchoHub.Client/UI/Chat/ChatSegment.cs.md)
- [RenderHelpers](src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md)
- [WelcomeBanner](src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md)
### src/EchoHub.Client/UI/Dialogs
- [AudioPlayerDialog](src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md)
- [ChannelPasswordDialog](src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md)
- [ConnectDialog](src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md)
- [CreateChannelDialog](src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md)
- [ProfileEditDialog](src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md)
- [ProfileViewDialog](src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md)
- [SearchDialog](src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md)
- [StatusDialog](src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md)
- [UpdateConfirmDialog](src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md)
### src/EchoHub.Client/UI/Helpers
- [DroppedFileParser](src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md)
- [EmojiHelper](src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md)
- [HexColorHelper](src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md)
- [NickColorHelper](src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md)
### src/EchoHub.Client/UI/ListSources
- [ChannelListSource](src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md)
- [SearchListSource](src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md)
- [UserListSource](src/EchoHub.Client/UI/ListSources/UserListSource.cs.md)
### src/EchoHub.Core/Constants
- [HubConstants](src/EchoHub.Core/Constants/HubConstants.cs.md)
- [MessageConventions](src/EchoHub.Core/Constants/MessageConventions.cs.md)
- [ValidationConstants](src/EchoHub.Core/Constants/ValidationConstants.cs.md)
### src/EchoHub.Core/Contracts
- [IChannelService](src/EchoHub.Core/Contracts/IChannelService.cs.md)
- [IChatBroadcaster](src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md)
- [IChatService](src/EchoHub.Core/Contracts/IChatService.cs.md)
- [IEchoHubClient](src/EchoHub.Core/Contracts/IEchoHubClient.cs.md)
- [IMessageEncryptionService](src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)
- [IUserService](src/EchoHub.Core/Contracts/IUserService.cs.md)
### src/EchoHub.Core/DTOs
- [AccountDtos](src/EchoHub.Core/DTOs/AccountDtos.cs.md)
- [AuthDtos](src/EchoHub.Core/DTOs/AuthDtos.cs.md)
- [ChatDtos](src/EchoHub.Core/DTOs/ChatDtos.cs.md)
- [CommonDtos](src/EchoHub.Core/DTOs/CommonDtos.cs.md)
- [InviteDtos](src/EchoHub.Core/DTOs/InviteDtos.cs.md)
- [ModerationDtos](src/EchoHub.Core/DTOs/ModerationDtos.cs.md)
- [ProfileDtos](src/EchoHub.Core/DTOs/ProfileDtos.cs.md)
- [ServerDtos](src/EchoHub.Core/DTOs/ServerDtos.cs.md)
### src/EchoHub.Core/Models
- [Attachment](src/EchoHub.Core/Models/Attachment.cs.md)
- [AttachmentKind](src/EchoHub.Core/Models/AttachmentKind.cs.md)
- [Channel](src/EchoHub.Core/Models/Channel.cs.md)
- [ChannelMembership](src/EchoHub.Core/Models/ChannelMembership.cs.md)
- [InviteCode](src/EchoHub.Core/Models/InviteCode.cs.md)
- [Message](src/EchoHub.Core/Models/Message.cs.md)
- [MessageType](src/EchoHub.Core/Models/MessageType.cs.md)
- [RefreshToken](src/EchoHub.Core/Models/RefreshToken.cs.md)
- [ServerRole](src/EchoHub.Core/Models/ServerRole.cs.md)
- [ServerStatsReport](src/EchoHub.Core/Models/ServerStatsReport.cs.md)
- [User](src/EchoHub.Core/Models/User.cs.md)
- [UserStatus](src/EchoHub.Core/Models/UserStatus.cs.md)
### src/EchoHub.Core/Security
- [RoomCrypto](src/EchoHub.Core/Security/RoomCrypto.cs.md)
### src/EchoHub.Core/Services
- [AsciiBannerService](src/EchoHub.Core/Services/AsciiBannerService.cs.md)
- [FileValidationHelper](src/EchoHub.Core/Services/FileValidationHelper.cs.md)
- [ImageToAsciiService](src/EchoHub.Core/Services/ImageToAsciiService.cs.md)
### src/EchoHub.Server
- [Program](src/EchoHub.Server/Program.cs.md)
### src/EchoHub.Server.Irc
- [IrcBroadcaster](src/EchoHub.Server.Irc/IrcBroadcaster.cs.md)
- [IrcClientConnection](src/EchoHub.Server.Irc/IrcClientConnection.cs.md)
- [IrcCommandHandler](src/EchoHub.Server.Irc/IrcCommandHandler.cs.md)
- [IrcGatewayService](src/EchoHub.Server.Irc/IrcGatewayService.cs.md)
- [IrcMessage](src/EchoHub.Server.Irc/IrcMessage.cs.md)
- [IrcMessageFormatter](src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md)
- [IrcNumericReply](src/EchoHub.Server.Irc/IrcNumericReply.cs.md)
- [IrcOptions](src/EchoHub.Server.Irc/IrcOptions.cs.md)
- [IrcServiceExtensions](src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md)
### src/EchoHub.Server/Auth
- [JwtTokenService](src/EchoHub.Server/Auth/JwtTokenService.cs.md)
### src/EchoHub.Server/Config
- [ServerLogsOptions](src/EchoHub.Server/Config/ServerLogsOptions.cs.md)
- [SpamOptions](src/EchoHub.Server/Config/SpamOptions.cs.md)
- [StatsOptions](src/EchoHub.Server/Config/StatsOptions.cs.md)
- [UploadLimits](src/EchoHub.Server/Config/UploadLimits.cs.md)
### src/EchoHub.Server/Controllers
- [AuthController](src/EchoHub.Server/Controllers/AuthController.cs.md)
- [ChannelsController](src/EchoHub.Server/Controllers/ChannelsController.cs.md)
- [FilesController](src/EchoHub.Server/Controllers/FilesController.cs.md)
- [InvitesController](src/EchoHub.Server/Controllers/InvitesController.cs.md)
- [ModerationController](src/EchoHub.Server/Controllers/ModerationController.cs.md)
- [ServerController](src/EchoHub.Server/Controllers/ServerController.cs.md)
- [UsersController](src/EchoHub.Server/Controllers/UsersController.cs.md)
### src/EchoHub.Server/Data
- [EchoHubDbContext](src/EchoHub.Server/Data/EchoHubDbContext.cs.md)
### src/EchoHub.Server/Hubs
- [ChatHub](src/EchoHub.Server/Hubs/ChatHub.cs.md)
### src/EchoHub.Server/Services
- [ChannelService](src/EchoHub.Server/Services/ChannelService.cs.md)
- [ChatService](src/EchoHub.Server/Services/ChatService.cs.md)
- [DirectoryClaimStore](src/EchoHub.Server/Services/DirectoryClaimStore.cs.md)
- [FileCleanupService](src/EchoHub.Server/Services/FileCleanupService.cs.md)
- [FileStorageService](src/EchoHub.Server/Services/FileStorageService.cs.md)
- [LinkEmbedService](src/EchoHub.Server/Services/LinkEmbedService.cs.md)
- [MessageEncryptionService](src/EchoHub.Server/Services/MessageEncryptionService.cs.md)
- [MuteExpirationService](src/EchoHub.Server/Services/MuteExpirationService.cs.md)
- [PresenceTracker](src/EchoHub.Server/Services/PresenceTracker.cs.md)
- [ServerDirectoryService](src/EchoHub.Server/Services/ServerDirectoryService.cs.md)
- [SignalRBroadcaster](src/EchoHub.Server/Services/SignalRBroadcaster.cs.md)
- [SpamGuard](src/EchoHub.Server/Services/SpamGuard.cs.md)
- [UserService](src/EchoHub.Server/Services/UserService.cs.md)
### src/EchoHub.Server/Services/ServerLogs
- [ServerLogsService](src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md)
- [ServerLogsSink](src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md)
- [ServerLogsStreamService](src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md)
### src/EchoHub.Server/Services/Stats
- [ServerStatsCollector](src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md)
- [ServerStatsReportService](src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md)
### src/EchoHub.Server/Setup
- [DataMigrationService](src/EchoHub.Server/Setup/DataMigrationService.cs.md)
- [DatabaseSetup](src/EchoHub.Server/Setup/DatabaseSetup.cs.md)
- [FirstRunSetup](src/EchoHub.Server/Setup/FirstRunSetup.cs.md)
### src/EchoHub.Tests/Irc
- [TestHelpers](src/EchoHub.Tests/Irc/TestHelpers.cs.md)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
# ClientConfig.cs
> **Source:** `src/EchoHub.Client/Config/ClientConfig.cs`
## Contents
- [AccountPreset](#accountpreset)
- [ClientConfig](#clientconfig)
- [NotificationConfig](#notificationconfig)
- [SavedServer](#savedserver)
---
## AccountPreset
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class AccountPreset
```
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
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
- Null properties indicate that the corresponding attribute should not override any existing value when applying the preset to an existing account.
---
## ClientConfig
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class ClientConfig
```
ClientConfig is a simple data container that groups the clients preferences and runtime settings into a single object. It includes the list of configured servers (`SavedServers`), the default account preset (`DefaultPreset`), the currently selected theme (`ActiveTheme`), and the notification configuration (`Notifications`). It also carries optional application paths and rendering settings: `DownloadPath` specifies where attachments are saved (null means use the OS Downloads folder), and `DefaultAsciiSize` selects the ASCII-art rendering size for attached images (values 's', 'm', or 'l', defaulting to 'm').
## Remarks
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.
---
## NotificationConfig
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class NotificationConfig
```
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
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.
---
## SavedServer
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class SavedServer
```
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
The `SavedServer` acts as a simple data container that binds together server identity, user identity (when supplied), and user-driven state that enhances the reconnect experience. It sits at the boundary between the persistence layer and the networking layer: serialization of this object enables quick restoration of a user session without re-issuing authentication or resynchronizing channel state. The `ChannelKeys` field, in particular, represents sensitive data tied to end-to-end encrypted channels and is kept on the client; its lifecycle is intentionally scoped to the users device and is managed with the same care prescribed for the `RefreshToken`.
## Example
```csharp
var server = new SavedServer
{
Name = "EchoHub",
Url = "https://echo.example",
Username = "alice",
RememberMe = true,
LastConnected = DateTimeOffset.UtcNow,
ChannelKeys = new Dictionary<string, string>
{
{ "general", "base64encryptedKeyHere" }
},
LeftChannels = new List<string> { "old-channel" },
LastReadMessages = new Dictionary<string, string>
{
{ "general", "12345" }
}
};
```
## Notes
- Treat `ChannelKeys` as sensitive data: avoid logging them or exposing them to the UI; ensure at-rest encryption via the clients security model. The keys are stored only on the client device and are not sent to `server` endpoints.
- This class is intended as a plain data carrier (DTO) used by the persistence and connection layers; do not embed domain logic here. When upgrading or migrating fields, consider versioning in the surrounding storage layer to preserve compatibility.
---
@@ -0,0 +1,25 @@
# ConfigManager
> **File:** `src/EchoHub.Client/Config/ConfigManager.cs`
> **Kind:** class
```csharp
public static class ConfigManager
```
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
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
- 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.
@@ -0,0 +1,15 @@
# Program
> **File:** `src/EchoHub.Client/Program.cs`
> **Kind:** file
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
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
- Rolling back can terminate startup early because `UpdateBackupService.RestoreBackup()` or subsequent error paths invoke `Environment.Exit`.
- Unix permission checks are best-effort and any failures are swallowed to avoid blocking startup on platform quirks.
- The initial configuration may be sourced from an embedded resource (`EchoHub.Client.appsettings.example.json`) if `appsettings.json` is absent, providing a safe fallback during first-run scenarios.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
# AsyncRunner
> **File:** `src/EchoHub.Client/Services/AsyncRunner.cs`
> **Kind:** class
```csharp
public static class AsyncRunner
```
Runs the provided asynchronous work on a background thread and routes exceptions to the UI, eliminating boilerplate like `Task.Run`/try/catch/`app.Invoke(ShowError)`.
`AsyncRunner.Run` takes an `IApplication` (`app`), a `Func<Task>` representing the work, an `Action<string>` (`showError`), a string (`errorPrefix`) used in the user-facing error, and an optional `string? logContext` to enrich logs; if an exception occurs, it logs with `Log.Error` and invokes the UI thread to display the error via `showError`.
This pattern centralizes background execution and UI-error reporting, so callers need only supply the work and error message components and can rely on consistent logging and user feedback.
## Remarks
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: 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.
@@ -0,0 +1,19 @@
# AudioPlaybackService
> **File:** `src/EchoHub.Client/Services/AudioPlaybackService.cs`
> **Kind:** class
```csharp
public class AudioPlaybackService
```
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
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
- 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.
@@ -0,0 +1,14 @@
# AvatarHelper
> **File:** `src/EchoHub.Client/Services/AvatarHelper.cs`
> **Kind:** class
```csharp
internal static class AvatarHelper
```
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
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.
@@ -0,0 +1,31 @@
# ClientEncryptionService
> **File:** `src/EchoHub.Client/Services/ClientEncryptionService.cs`
> **Kind:** class
```csharp
public sealed class ClientEncryptionService : IMessageEncryptionService
```
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 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 usage of client-side encryption
var encryption = new ClientEncryptionService();
string base64Key = "<32-byte-base64-key>";
encryption.SetKey(base64Key);
string plaintext = "Secret message";
string ciphertext = encryption.Encrypt(plaintext);
string decrypted = encryption.Decrypt(ciphertext);
```
## Notes
- 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]".
@@ -0,0 +1,27 @@
# ClipboardFiles
> **File:** `src/EchoHub.Client/Services/ClipboardFiles.cs`
> **Kind:** class
```csharp
public static class ClipboardFiles
```
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
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))
{
foreach (var path in files)
Console.WriteLine(path);
}
```
## Notes
- 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.
@@ -0,0 +1,22 @@
# ClipboardImage
> **File:** `src/EchoHub.Client/Services/ClipboardImage.cs`
> **Kind:** class
```csharp
public static class ClipboardImage
```
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
`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.
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
- 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.
@@ -0,0 +1,65 @@
# ConnectionManager.cs
> **Source:** `src/EchoHub.Client/Services/ConnectionManager.cs`
## Contents
- [ConnectionManager](#connectionmanager)
- [ConnectResult](#connectresult)
---
## ConnectionManager
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
> **Kind:** class
```csharp
internal sealed class ConnectionManager : IAsyncDisposable
```
Owns the full client-side connection lifecycle: authenticating via the `ApiClient`, establishing and wiring an `EchoHubConnection` (SignalR) for realtime events, enabling end-to-end encryption via the `ClientEncryptionService`, and tracking channel membership in `RoomKeyStore` and `_joinedChannels`. Reach for `ConnectionManager` when you want a single, high-level component to manage connection setup, token refresh handling, event forwarding, and channel join/leave logic instead of manipulating `ApiClient` and `EchoHubConnection` directly.
## Remarks
`ConnectionManager` is the orchestration point between the networking primitives (`ApiClient` and `EchoHubConnection`) and the UI layer. It centralizes responsibility for: authenticating (including login, register, and refresh-token flows), persisting rotated refresh tokens via `OnTokensRefreshed`/`HandleTokensRefreshed`, attempting to establish an E2E encryption key with `ClientEncryptionService`, and forwarding SignalR events to consumers through its public events (for example `MessageReceived`, `UserJoined`, `ChannelUpdated`, and `ConnectionStatusChanged`). By exposing `IsConnected`, `IsAuthenticated`, `Api`, and `RoomKeys`, it gives callers enough state to update UI and perform API operations without needing to manage low-level connection state or event wiring.
## Notes
- `ConnectAsync` throws on authentication failure — callers are expected to handle saved-session expiry and related UI flows. See the `ConnectAsync` progress messages for how the method reports intermediate status.
- The class disposes and replaces the internal `ApiClient` during `ConnectAsync` (it calls `_apiClient?.Dispose()`), and implements `IAsyncDisposable`; callers should ensure `DisposeAsync` is invoked when the manager is no longer needed to avoid resource leaks.
- Encryption is best-effort: if fetching the encryption key fails (`GetEncryptionKeyAsync`), the manager logs a warning and continues with an unencrypted session — consumers should not assume messages are always encrypted.
- The implementation mutates internal fields like `_apiClient`, `_connection`, and `_joinedChannels` without visible synchronization. The class appears intended for single-threaded/UI-thread usage; consumers that access it from multiple threads should serialize calls externally to avoid race conditions.
---
## ConnectResult
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
> **Kind:** record
```csharp
internal record ConnectResult(
LoginResponse Login,
List<ChannelDto> Channels,
Dictionary<string, List<MessageDto>> Histories,
ServerStatusDto? ServerInfo = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Login` | `LoginResponse` | — |
| `Channels` | `List<ChannelDto>` | — |
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
| `ServerInfo` | `ServerStatusDto?` | `null` |
ConnectResult is an internal, immutable `record` that represents the successful outcome of establishing a connection and is returned to the `AppOrchestrator` to drive UI updates. It bundles the login information (`Login`) of type `LoginResponse`, the joined channels (`Channels`) as `List<ChannelDto>`, the initial per-channel histories (`Histories`) as `Dictionary<string, List<MessageDto>>`, and optional server status (`ServerInfo`) as `ServerStatusDto?`. The `Histories` dictionary maps channel names to their corresponding history lists and always includes the default channel.
## Remarks
ConnectResult acts as a single, UI-facing snapshot of the connected state. It collects authentication results, channel roster, initial per-channel histories, and optional server health/status so the `AppOrchestrator` can immediately render the connected view without issuing further requests.
## Notes
- ConnectResult is immutable; use a `with` expression to derive a modified copy rather than mutating the existing instance.
- `ServerInfo` may be null; callers should handle absence gracefully.
---
@@ -0,0 +1,110 @@
# EchoHubConnection.cs
> **Source:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
## Contents
- [ChannelPasswordRequiredException](#channelpasswordrequiredexception)
- [EchoHubConnection](#echohubconnection)
- [RoomLockedException](#roomlockedexception)
- [JoinOutcome](#joinoutcome)
---
## ChannelPasswordRequiredException
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class ChannelPasswordRequiredException : Exception
```
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
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
- 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.
---
## EchoHubConnection
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class EchoHubConnection : IAsyncDisposable
```
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` 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
// 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
- 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.
---
## RoomLockedException
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class RoomLockedException : Exception
```
`RoomLockedException` is thrown when attempting to send into an end-to-end encrypted channel whose room key isnt cached. Without the key, the operation would emit plaintext, which must never happen, so the exception blocks the send. The `ChannelName` property exposes which channel is locked, and the constructor formats the failure message to include `#{channelName}` to guide unlocking.
## Remarks
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.
---
## JoinOutcome
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** record
```csharp
public sealed record JoinOutcome(List<MessageDto> History, string? EncryptionSalt, string? WrappedRoomKey)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `History` | `List<MessageDto>` | — |
| `EncryptionSalt` | `string?` | — |
| `WrappedRoomKey` | `string?` | — |
Represents the result of joining a channel: the decrypted message history and, for end-to-end encrypted rooms, the key envelope needed to unlock the room content key. `History` is a `List<MessageDto>` containing the decrypted messages, and `WrappedRoomKey` (with optional `EncryptionSalt`) provides the cryptographic envelope when encryption is in play.
## Remarks
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` are nullable; guard for nulls and only attempt decryption when these values are provided.
---
@@ -0,0 +1,109 @@
# NativeFolderPicker.cs
> **Source:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
## Contents
- [NativeFolderPicker](#nativefolderpicker)
- [FolderPickResult](#folderpickresult)
- [PickerOutcome](#pickeroutcome)
---
## NativeFolderPicker
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** class
```csharp
public static class NativeFolderPicker
```
Opens the OS-native folder chooser by shelling out to platform-specific dialogs (Windows Explorer, macOS Finder, Linux GTK/KDE), allowing the TUI to remain GUI-toolkit agnostic. It dispatches to the appropriate platform helper at runtime and returns a `FolderPickResult` with a `PickerOutcome` of `Unavailable` when no native dialog can run, so callers can fall back to a configured path. Failures are caught and logged to avoid crashing the UI, and the dialog title is a fixed prompt guiding the user to select EchoHubs download folder.
## Remarks
By shielding native dialogs behind `NativeFolderPicker`, the rest of the application stays decoupled from platform GUI toolkits, improving portability and testability. The abstraction also centralizes crossplatform quirks (Windows PowerShell quoting, AppleScript invocation, and GTK/KDialog fallbacks) in one place, reducing duplication and ensuring a consistent user experience across environments.
## Notes
- 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.
---
## FolderPickResult
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** record
```csharp
public sealed record FolderPickResult(PickerOutcome Outcome, string? Path)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Outcome` | `PickerOutcome` | — |
| `Path` | `string?` | — |
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
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` is nullable; validate before use and prefer accessing `Path` only when `Outcome` indicates a successful result.
---
## PickerOutcome
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** enum
```csharp
public enum PickerOutcome
{
Chosen,
Cancelled,
Unavailable,
}
```
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
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
- Do not access `FolderPickResult.Path` when outcome is not `PickerOutcome.Chosen`.
---
@@ -0,0 +1,23 @@
# NotificationSoundService
> **File:** `src/EchoHub.Client/Services/NotificationSoundService.cs`
> **Kind:** class
```csharp
public class NotificationSoundService
```
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
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
- 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.
@@ -0,0 +1,30 @@
# OutgoingAttachment
> **File:** `src/EchoHub.Client/Services/OutgoingAttachment.cs`
> **Kind:** record
```csharp
public sealed record OutgoingAttachment(
Stream Stream,
string FileName,
string? DeclaredKind = null,
string? EncryptedPreview = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Stream` | `Stream` | — |
| `FileName` | `string` | — |
| `DeclaredKind` | `string?` | `null` |
| `EncryptedPreview` | `string?` | `null` |
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
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
- If `DeclaredKind` is provided for an encrypted attachment, ensure `EncryptedPreview` is also supplied to avoid inconsistent previews.
@@ -0,0 +1,26 @@
# PathSetup
> **File:** `src/EchoHub.Client/Services/PathSetup.cs`
> **Kind:** class
```csharp
public static class PathSetup
```
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
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
- 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.
@@ -0,0 +1,16 @@
# RoomKeyProtector
> **File:** `src/EchoHub.Client/Services/RoomKeyProtector.cs`
> **Kind:** class
```csharp
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 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 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.
@@ -0,0 +1,19 @@
# RoomKeyStore
> **File:** `src/EchoHub.Client/Services/RoomKeyStore.cs`
> **Kind:** class
```csharp
public sealed class RoomKeyStore
```
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` 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
- `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).
@@ -0,0 +1,73 @@
# UpdateBackupService.cs
> **Source:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
## Contents
- [BackupJsonContext](#backupjsoncontext)
- [UpdateBackupService](#updatebackupservice)
- [BackupInfo](#backupinfo)
---
## BackupJsonContext
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** class
```csharp
[System.Text.Json.Serialization.JsonSerializable(typeof(BackupInfo))]
internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSerializerContext
```
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
By centralizing the JSON metadata in ``BackupJsonContext``, the codebase gains a single, version-stable contract for serializing ``BackupInfo``. The generated ```JsonTypeInfo<BackupInfo>``` exposed as ``BackupJsonContext.Default.BackupInfo`` is consumed by ``JsonSerializer`` overloads that accept type metadata, reducing runtime reflection and enabling better inlining and optimization. This scope-limited context also makes it straightforward to extend serialization support to additional related types by extending the same context without changing call-sites.
---
## UpdateBackupService
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** class
```csharp
public static class UpdateBackupService
```
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.
---
## BackupInfo
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** record
```csharp
public record BackupInfo(
string Version,
string AppDirectory,
DateTimeOffset CreatedAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Version` | `string` | — |
| `AppDirectory` | `string` | — |
| `CreatedAt` | `DateTimeOffset` | — |
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
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
- 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.
---
@@ -0,0 +1,37 @@
# UpdateChecker
> **File:** `src/EchoHub.Client/Services/UpdateChecker.cs`
> **Kind:** class
```csharp
public sealed class UpdateChecker : IDisposable
```
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
`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 checker = new UpdateChecker(app);
checker.Start(); // starts periodic checks in RELEASE builds
// Trigger a manual check from UI or command handler
await checker.CheckNowAsync();
// After the Terminal.Gui main loop exits, the host should run any pending update
if (checker.PendingUpdate != null)
{
await checker.PendingUpdate(); // will download/extract and may restart the process
}
```
## Notes
- [`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.
@@ -0,0 +1,14 @@
# UserSession
> **File:** `src/EchoHub.Client/Services/UserSession.cs`
> **Kind:** class
```csharp
internal sealed class UserSession
```
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
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.
@@ -0,0 +1,57 @@
# Theme.cs
> **Source:** `src/EchoHub.Client/Themes/Theme.cs`
## Contents
- [Theme](#theme)
- [ThemeColors](#themecolors)
---
## Theme
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
> **Kind:** class
```csharp
public class Theme
```
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
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
};
```
---
## ThemeColors
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
> **Kind:** class
```csharp
public class ThemeColors
```
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
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
- 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.
---
@@ -0,0 +1,559 @@
# ThemeManager.cs
> **Source:** `src/EchoHub.Client/Themes/ThemeManager.cs`
## Contents
- [ThemeManager](#thememanager)
- [ApplyTheme](#applytheme)
- [BuildColorScheme](#buildcolorscheme)
- [GetAvailableThemes](#getavailablethemes)
- [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)
---
## ThemeManager
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** class
```csharp
public static class ThemeManager
```
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
Theme definitions live as [`Theme`](Theme.cs.md) instances inside the manager, with a fixed set of built-ins (e.g. `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, `RosePineTheme`) and a mechanism to discover additional user themes from the directory located at `ThemeDir`. `GetAvailableThemes()` merges these sources while skipping duplicates by name and ignoring malformed theme files; if the theme directory cannot be read, it gracefully falls back to the built-ins. The color wiring happens in `BuildColorScheme(ThemeColors colors)` to ensure the editor surfaces—such as `TextView` and `TextField`—are pinned to the themes colors so transparency is preserved (e.g. transparent themes do not render an opaque input background). `ApplyTheme(Theme theme)` applies the chosen look to UI chrome like frame borders and titles, while `SaveTheme(Theme theme)` persists changes to disk as a best-effort operation.
## Notes
- 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.
---
### ApplyTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static void ApplyTheme(Theme theme)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `theme` | [`Theme`](Theme.cs.md) | — |
**Returns:** `void`
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
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
- 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.
---
### BuildColorScheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
private static Scheme BuildColorScheme(ThemeColors colors)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `colors` | [`ThemeColors`](Theme.cs.md) | — |
**Returns:** `Scheme`
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
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
- 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.
---
### GetAvailableThemes
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static List<Theme> GetAvailableThemes()
```
**Returns:** `List<Theme>`
GetAvailableThemes collects the available themes by starting with the built-in set (`BuiltInThemes`), then augmenting it with user-provided themes discovered as JSON files in `ThemeDir`. It reads each `*.json` file, deserializes the content into a [`Theme`](Theme.cs.md) using `JsonSerializer` with `JsonOptions`, and, if the resulting theme has a non-empty `Name` and isn't already present (checked by name using `StringComparison.OrdinalIgnoreCase`), adds it to the list. If the theme directory can't be read or a file is malformed, those items are skipped and the method returns the built-in themes as a fallback. The result is a `List<Theme>` that callers can present to the user.
## Remarks
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
- 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.
---
### GetTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static Theme GetTheme(string name)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `name` | `string` | — |
**Returns:** [`Theme`](Theme.cs.md)
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.
---
### ParseColor
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
private static Color ParseColor(string colorName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `colorName` | `string` | — |
**Returns:** `Color`
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
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 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.
---
### SaveTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static void SaveTheme(Theme theme)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `theme` | [`Theme`](Theme.cs.md) | — |
**Returns:** `void`
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
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 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.
---
### BuiltInThemes
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly List<Theme> BuiltInThemes =
[
DefaultTheme,
TransparentTheme,
TransparentLightTheme,
ClassicTheme,
LightTheme,
HackerTheme,
SolarizedTheme,
DraculaTheme,
MonokaiTheme,
NordTheme,
GruvboxTheme,
OceanTheme,
HighContrastTheme,
RosePineTheme
]
```
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
This private, static collection centralizes the built-in theme catalog used by the theming system. The `readonly` modifier prevents reassigning the field, but the underlying `List<Theme>` can still be mutated by internal code, which means changes to the set of built-ins could affect any UI that relies on them. If true immutability is required, consider exposing a read-only wrapper or a dedicated API surface.
## Notes
- The `List<Theme>` is mutable even though the field is `readonly`; external code cannot access it, but internal code can modify its contents. If you need to guarantee immutability, replace with a read-only wrapper such as `ReadOnlyCollection<Theme>` and expose a safe accessor.
---
### ClassicTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme ClassicTheme = new()
```
ClassicTheme is a privately scoped, statically initialized [`Theme`](Theme.cs.md) instance that serves as the built-in look-and-feel blueprint used by the UI. It defines color mappings for the `Base`, `Menu`, `Dialog`, and `Status` surfaces, establishing a cohesive appearance across the application. Because it is declared as `private static readonly`, the instance is created once during type initialization and is shared for the lifetime of the process, acting as a default theme reference for the `ThemeManager`.
## Remarks
By centralizing the palette in a single, private field, the `ThemeManager` can apply a consistent Classic style across all major surfaces without requiring external configuration. The private visibility keeps the default theme encapsulated within the theming code, making it straightforward to introduce additional themes or swap them by adding alternative static fields or exposing a configuration mechanism in the future.
---
### DefaultTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DefaultTheme = new()
```
Represents the canonical default color theme used by the theming subsystem. As a private static readonly [`Theme`](Theme.cs.md) named `Default`, it seeds the color configuration for core surfaces (`Base`, `Menu`, `Dialog`, `Status`) so the UI maintains a consistent palette when no user-provided theme is supplied.
## Remarks
This value acts as the internal seed for all theming operations within the `ThemeManager`. Centralizing the default colors in a single `DefaultTheme` instance ensures consistent visuals across surfaces and avoids duplicating color choices. Note that while the field is `readonly`, its nested [`ThemeColors`](Theme.cs.md) objects may still be mutable at runtime, depending on their mutability; consuming code should not rely on deep immutability unless enforced by the type definitions. The arrangement guarantees uniform behavior for the `Base`, `Menu`, `Dialog`, and `Status` color states (foreground, background, and focus states).
## Notes
- Although the field is `readonly` at the top level, the nested [`ThemeColors`](Theme.cs.md) instances may be mutated; treat this as a potential mutation point.
---
### DraculaTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DraculaTheme = new()
```
DraculaTheme is a preconfigured [`Theme`](Theme.cs.md) instance that encodes the Dracula color palette for the UI. Declared as a private static readonly field named `DraculaTheme`, it defines a single, shared palette used by the application to color the core surfaces — `Base`, `Menu`, `Dialog`, and `Status` — with per-surface mappings such as foregrounds, backgrounds, and focus colors that collectively establish a cohesive, dark interface with magenta accents on focus. With `Name` set to Dracula, this theme provides a consistent Dracula aesthetic across the application.
## Remarks
Centralizes the Dracula color choices in one place to ensure visual consistency across surfaces and to simplify theme swapping by the `ThemeManager` without recalculating colors at render time. The per-surface [`ThemeColors`](Theme.cs.md) definitions govern how content appears on the main areas (`Base`), the navigation (`Menu`), popups (`Dialog`), and status indicators (`Status`).
## Notes
- The nested [`ThemeColors`](Theme.cs.md) objects may be mutable; treat DraculaTheme as effectively immutable only if those types are immutable, or clone before modification if variations are needed.
- Because the field is private, external code cannot reference it directly; expose an accessor or copy if you need to reuse this theme outside its containing class.
---
### GruvboxTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme GruvboxTheme = new()
```
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
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
- Private field scope means external code cannot reference `GruvboxTheme` directly; use the public theming API (e.g., `ThemeManager`) to switch or retrieve themes.
---
### HackerTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HackerTheme = new()
```
HackerTheme is a private static readonly instance of [`Theme`](Theme.cs.md) that defines the 'Hacker' color scheme used by the UI. It centralizes the color configuration for the core regions—`Base`, `Menu`, `Dialog`, and `Status`—by specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to deliver a cohesive hacker aesthetic across the interface, and is reused internally rather than rebuilt for each component.
## Remarks
By housing the entire color palette in a single static field, the code ensures visual consistency across all UI surfaces that adopt this theme. The `HackerTheme` instance is created once at class initialization and referenced wherever a [`Theme`](Theme.cs.md) is needed within the theme system, promoting reuse and reducing the risk of divergent color values. Keeping this configuration private reinforces encapsulation: external code cannot mutate the theme inadvertently, preserving the intended appearance.
---
### HighContrastTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HighContrastTheme = new()
```
Defines a preconfigured [`Theme`](Theme.cs.md) instance named `HighContrast` that drives a high-contrast UI palette. It is exposed internally as a private static readonly field `HighContrastTheme` and initializes the `Base`, `Menu`, `Dialog`, and `Status` surfaces with a dark background (`Black`) and bright foreground (`BrightYellow`), while tuning region-specific focus colors to preserve legibility. Because it is static and readonly, the theme is constructed once and reused by the UI theming system rather than rebuilt at runtime.
## Remarks
This field acts as a canonical, immutable high-contrast palette for the theming subsystem. By centralizing the color choices for `Base`, `Menu`, `Dialog`, and `Status`, it ensures consistent accessibility-friendly visuals across the application and prevents drift between components. Its private visibility indicates it is an internal implementation detail of the theme infrastructure, intended to be consumed by the theme-management logic rather than by consumer code directly.
## Notes
- The `HighContrastTheme` is immutable after initialization due to `readonly`; runtime theme switching would require a separate mechanism to swap themes.
---
### JsonOptions
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly JsonSerializerOptions JsonOptions = new()
```
Defines a shared `JsonSerializerOptions` instance named `JsonOptions` used by the `ThemeManager` to serialize theme data with consistent formatting. It configures pretty-printed JSON by setting `WriteIndented` to true and enforces camelCase property names by using `PropertyNamingPolicy` via `JsonNamingPolicy.CamelCase`.
## Remarks
By making the field `static` and `readonly`, the class ensures a single, immutable source of serialization configuration for all calls within the ThemeManager, reducing duplication and the risk of inconsistent formatting. This centralization also minimizes drift if multiple serialization sites exist in the class.
## Notes
- Do not mutate `JsonOptions` after initialization; although `JsonSerializerOptions` properties are mutable, the field is intended to be consumed as a fixed configuration.
- If a one-off operation requires a different formatting (e.g., a different naming policy or indentation), create and use a separate `JsonSerializerOptions` instance instead of modifying this field.
---
### LightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme LightTheme = new()
```
The `LightTheme` field provides a concrete, immutable light color scheme used by the theming system. It centralizes color definitions for the main UI surfaces: `Base`, the `Menu`, `Dialog`, and `Status` areas, ensuring consistent foreground/background combinations across the application and predictable focus states.
With `Name` set to `Light` and color pairs like `Foreground`/`Background` and `FocusForeground`/`FocusBackground` defined per surface, it enables the ThemeManager to apply the light theme quickly without reconstructing the palette each time.
## Remarks
By keeping the field `private static readonly`, the code guarantees a single, shared instance of the light theme that cannot be modified at runtime, avoiding drift between components. This centralization also clarifies the intended visual identity for the light mode and reduces duplication whenever a light theme is needed.
---
### MonokaiTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme MonokaiTheme = new()
```
MonokaiTheme is a private static readonly field that encapsulates the internal Monokai color palette used by the UI. It defines a single [`Theme`](Theme.cs.md) named `Monokai` with dedicated [`ThemeColors`](Theme.cs.md) for `Base`, `Menu`, `Dialog`, and `Status`, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to ensure the interface presents a cohesive look.
## Remarks
MonokaiTheme centralizes the Monokai palette for the UI, providing a single source of truth for the [`Theme`](Theme.cs.md) the `ThemeManager` applies across components. Its private static readonly scope ensures a stable, class-wide instance isn't exposed or replaced by external code, preserving the intended appearance. If internal code mutates the nested [`ThemeColors`](Theme.cs.md) objects, the look could drift, so treat the instance as effectively immutable after initialization.
## Notes
- `readonly` prevents reassigning the field, but nested color objects may still be mutated; ensure internal code avoids mutating the theme after initialization or consider making the color data immutable.
---
### NordTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme NordTheme = new()
```
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
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
- 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.
---
### OceanTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme OceanTheme = new()
```
The private static readonly field `OceanTheme` is a [`Theme`](Theme.cs.md) instance configured with a named palette Ocean and dedicated [`ThemeColors`](Theme.cs.md) for its `Base`, `Menu`, `Dialog`, and `Status` sections. It is initialized inline with specific color tokens such as `BrightCyan`, `DarkBlue`, and `DarkCyan` to ensure a cohesive, visually distinct look across the UI. Being `static readonly` means this instance is created once at type initialization and cannot be reassigned, serving as an internal, consistent theme blueprint for the `ThemeManager`.
## Remarks
This field encapsulates a concrete theme configuration that `ThemeManager` uses internally, without exposing mutable defaults to consumers. Centralizing color mappings for `Base`, `Menu`, `Dialog`, and `Status` in a single private field reduces duplication and promotes visual consistency across the UI. Because the field is private, external code cannot reference or alter it directly; changes must go through the public theming API, preserving encapsulation.
---
### RosePineTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme RosePineTheme = new()
```
RosePineTheme is a private static readonly [`Theme`](Theme.cs.md) instance named `RosePine` that encodes a RosePine color palette for the UI. It defines color roles for `Base`, `Menu`, `Dialog`, and `Status` via nested [`ThemeColors`](Theme.cs.md) objects, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values. This single, prebuilt object lets the rest of the UI apply a cohesive RosePine appearance without reconstructing a [`Theme`](Theme.cs.md) from scratch.
## Remarks
Centralizes 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
- Although the field is `readonly`, the nested [`ThemeColors`](Theme.cs.md) instances may still be mutable if their properties have setters. Treat the object as immutable; avoid mutating to preserve a consistent RosePine theme.
- The field is private, so external consumers cannot reference `RosePineTheme` directly; changes to the theme would require a public accessor or method in `ThemeManager`.
---
### SolarizedTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme SolarizedTheme = new()
```
This field defines the pre-defined Solarized color theme as a private, static, readonly [`Theme`](Theme.cs.md) instance named `SolarizedTheme`. It bundles color roles for the base chrome, menus, dialogs, and status areas, providing a centralized Solarized palette that the theming subsystem can apply to the UI. The `private static readonly` designation ensures a single, immutable instance is created at startup, guaranteeing consistent visuals across the application.
## Remarks
Having a single [`Theme`](Theme.cs.md) instance for Solarized encapsulates the palette in one place, reducing duplication of color literals across UI surfaces. By separating the colors into `Base`, `Menu`, `Dialog`, and `Status` groups, the theme clearly communicates how each UI surface should appear and simplifies future tweaks. This private field serves as an internal canonical source for the Solarized look within the codebase and is consumed by the theming pipeline without exposing implementation details publicly.
---
### ThemeDir
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly string ThemeDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes")
```
ThemeDir stores the path to the per-user themes directory for the EchoHub client. It is initialized once at type initialization by combining the user's home directory (obtained via `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`) with the relative path `".echohub/themes"`, yielding a stable, user-scoped base for reading or enumerating theme assets.
## Remarks
- By centralizing the path construction, this private static readonly field reduces duplication and ensures all theme IO uses the same base directory.
- It encodes the assumption that themes are stored under the user's profile, which keeps user-specific customization isolated from system-wide resources.
- The static readonly nature means the value is fixed after initialization, simplifying reasoning about its value and caching theme metadata.
## Notes
- If the environment lacks a user profile directory, `Environment.GetFolderPath` may return an empty string, which would yield an invalid `ThemeDir`. Calling code should validate the path before attempting IO.
- It is a private field; external code cannot rely on this path and must use public APIs provided by the class for theme access.
---
### TransparentLightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentLightTheme = new()
```
Defines a concrete [`Theme`](Theme.cs.md) named `TransparentLight` with per-surface color rules for `Base`, `Menu`, `Dialog`, `Status`, and `Border` via [`ThemeColors`](Theme.cs.md). Each surface is configured with `Foreground`, `Background`, and `FocusForeground`/`FocusBackground` values to yield a light, nearly transparent appearance on the host UI: most surfaces use `Background = "None"`, while `Dialog` uses a light gray background and blue focus accents. This field is `private static readonly`, initialized once and used internally by the theming system to provide the `TransparentLight` theme.
## Remarks
By centralizing the color definitions for a light, semi-transparent appearance, this field enables consistent theming across the UI without scattering color literals throughout the code. Because it is `private`, external code cannot directly reference it; the surrounding theme infrastructure can expose higher-level theme switching that pulls from this internal variant. The immutable reference helps ensure the theme is not accidentally replaced at runtime, though the nested [`ThemeColors`](Theme.cs.md) instances may still be mutated if their properties are writable.
## Notes
- The `readonly` modifier prevents reassignment of the field, but the nested [`ThemeColors`](Theme.cs.md) objects could still be mutated if their properties have setters; avoid mutating them at runtime to preserve theme consistency.
---
### TransparentTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentTheme = new()
```
TransparentTheme is a private, static readonly instance of [`Theme`](Theme.cs.md) that encodes the glassy, transparent UI aesthetic named 'Transparent' and is intended for internal use by the theming system rather than as a public theme. It defines color settings for `Base`, `Menu`, `Dialog`, `Status`, and `Border` to deliver a cohesive appearance, with muted `Border` colors to preserve the translucent look.
## Remarks
TransparentTheme centralizes the palette for the glassy style in a single immutable object, reducing duplication across components. As a private field, it serves as an internal predefined palette that the theming system can apply without exposing a public API. This encapsulation makes it easy to tweak the look in one place while keeping the public surface stable.
---
@@ -0,0 +1,29 @@
# ChatColors
> **File:** `src/EchoHub.Client/UI/Chat/ChatColors.cs`
> **Kind:** class
```csharp
public static partial class ChatColors
```
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 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 [email protected]";
var segments = ChatColors.SplitMentions(message, defaultColor: null);
foreach (var seg in segments)
Console.WriteLine($"[{seg.Color}] {seg.Text}");
```
## Notes
- `SplitMentions` treats the optional `defaultColor` as the fallback attribute for non-special text; passing `null` means segment `Color` values may be `null` and the caller must handle that when rendering.
- The `MentionRegex` uses `(?<!\w)@...` to avoid matching emails, and the `ChannelRefRegex` requires at least one ASCII letter to avoid matching hex colors or bare numbers (so some international or non-ASCII usernames/channels may not match).
- Mentions take precedence: because `SplitMentions` colors `@`-matches in the first pass, any `#` inside an already-colored mention will not be reprocessed in the second pass.
@@ -0,0 +1,100 @@
# ChatLine.cs
> **Source:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
## Contents
- [ChatLine](#chatline)
- [AttachmentActionSpan](#attachmentactionspan)
- [AttachmentAction](#attachmentaction)
---
## ChatLine
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** class
```csharp
public partial class ChatLine
```
A single visual line in the chat view composed of one or more colored [`ChatSegment`](ChatSegment.cs.md)s. Use `ChatLine` when preparing data for rendering or layout (wrapping, separators, attachments, and reply jump targets) rather than when working with raw message text; it carries both the display segments and metadata the view needs (attachment info, rule labels, continuation/indent hints, and navigation markers).
## Remarks
`ChatLine` models what the UI actually renders: a sequence of colored segments in `Segments` plus a small set of rendering hints and metadata. It centralizes information the view needs for word-wrapping (`TextLength`, `Wrap`, `ContinuationIndent`, `ContinuationPrefixSegments`), special-line rendering (`RuleLabel`, `RuleAttr`, `IsUnreadMarker`), and attachment/interactivity (`AttachmentUrl`, `AttachmentFileName`, [`AttachmentKind`](../../../EchoHub.Core/Models/AttachmentKind.cs.md), `ActionSpans`). The `Wrap` method produces multiple `ChatLine` instances that fit a given column `width`, and `JumpToMessageId` links reply-quote lines back to their source message when present in the loaded history.
## Notes
- `TextLength` is computed once in the constructors (via `GetColumns()` on the provided text/segments). Because `Segments` is a mutable `List<ChatSegment>`, mutating `Segments` after construction will not update `TextLength`; keep them consistent or recreate the `ChatLine`.
- If `ContinuationPrefixSegments` is set it takes precedence over `ContinuationIndent` when computing the indent for continuation lines; the prefix's column width is used instead of the plain-space indent.
- `ActionSpans` columns are relative to the unwrapped line, so only the first wrapped line preserves clickable sub-line targets; setting `ActionSpans` to `null` means the whole line should use the kind's default action.
---
## AttachmentActionSpan
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** record
```csharp
public readonly record struct AttachmentActionSpan(int StartCol, int EndCol, AttachmentAction Action)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `StartCol` | `int` | — |
| `EndCol` | `int` | — |
| `Action` | `AttachmentAction` | — |
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
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
- 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.
---
## AttachmentAction
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** enum
```csharp
public enum AttachmentAction
{
OpenImage,
SaveImage,
}
```
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
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 = AttachmentAction.OpenImage;
if (action == AttachmentAction.OpenImage)
{
// Open the image for viewing
}
else if (action == AttachmentAction.SaveImage)
{
// Persist the image to disk
}
```
## Notes
- 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.
---
@@ -0,0 +1,19 @@
# ChatListSource
> **File:** `src/EchoHub.Client/UI/Chat/ChatListSource.cs`
> **Kind:** class
```csharp
public class ChatListSource : IListDataSource
```
A list data source that stores [`ChatLine`](ChatLine.cs.md) instances and renders them into a UI list with per-segment coloring and mention highlighting. Reach for `ChatListSource` when you need an `IListDataSource` implementation that maintains chat-specific layout state (like `MaxItemLength`) and performs per-grapheme drawing of `ChatLine.Segments` so segment colors and mention backgrounds are respected during rendering.
## Remarks
`ChatListSource` maintains an internal `List<ChatLine>` (`_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
- `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).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
# ChatSegment
> **File:** `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
> **Kind:** record
```csharp
public record ChatSegment(string Text, Attribute? Color)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Text` | `string` | — |
| `Color` | `Attribute?` | — |
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
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.
@@ -0,0 +1,19 @@
# RenderHelpers
> **File:** `src/EchoHub.Client/UI/Chat/RenderHelpers.cs`
> **Kind:** class
```csharp
static class RenderHelpers
```
RenderHelpers is a small static utility class that centralizes rendering concerns for `IListDataSource` implementations. It currently provides a single method, `WriteText`, which writes text to a `ListView` grapheme by grapheme, respecting a maximum width. It returns the updated drawn-columns count, enabling callers to track horizontal placement as multiple fields are rendered on a single line.
## Remarks
RenderHelpers abstracts the grapheme-aware rendering logic so all list-rendering code shares the same boundary checks and column accounting. It couples the `GraphemeHelper.GetGraphemes` iteration with a safe width calculation, reducing the chance of off-by-one errors when composing UI rows. In short, its the single place responsible for safe, width-bound text rendering to a `ListView` in this UI layer.
## Notes
- The width of each grapheme is determined by `grapheme.GetColumns()`, clamped to at least 1 with `Math.Max(grapheme.GetColumns(), 1)`.
- Rendering stops when adding the next grapheme would exceed `maxWidth`; partial graphemes are not drawn.
- The method delegates actual drawing to `ListView.AddStr`, so callers should ensure the `ListView` state is appropriate for incremental writes.
@@ -0,0 +1,13 @@
# WelcomeBanner
> **File:** `src/EchoHub.Client/UI/Chat/WelcomeBanner.cs`
> **Kind:** class
```csharp
internal static class WelcomeBanner
```
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.
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.
@@ -0,0 +1,24 @@
# AudioPlayerDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs`
> **Kind:** class
```csharp
public sealed class AudioPlayerDialog
```
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
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
AudioPlayerDialog.Show(app, audioService, "/path/to/song.mp3", "song.mp3");
```
## Notes
- 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.
@@ -0,0 +1,21 @@
# ChannelPasswordDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs`
> **Kind:** class
```csharp
public sealed class ChannelPasswordDialog
```
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
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 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.
@@ -0,0 +1,67 @@
# ConnectDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
## Contents
- [ConnectDialog](#connectdialog)
- [ConnectDialogResult](#connectdialogresult)
---
## ConnectDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
> **Kind:** class
```csharp
public sealed class ConnectDialog
```
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.
---
## ConnectDialogResult
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
> **Kind:** record
```csharp
public record ConnectDialogResult(
string ServerUrl, string Username, string Password,
bool IsRegister, bool RememberMe, string? SavedRefreshToken,
string? DisplayName = null, string? InviteCode = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ServerUrl` | `string` | — |
| `Username` | `string` | — |
| `Password` | `string` | — |
| `IsRegister` | `bool` | — |
| `RememberMe` | `bool` | — |
| `SavedRefreshToken` | `string?` | — |
| `DisplayName` | `string?` | `null` |
| [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md) | `string?` | `null` |
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
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
- 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.
---
@@ -0,0 +1,53 @@
# CreateChannelDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
## Contents
- [CreateChannelDialog](#createchanneldialog)
- [CreateChannelResult](#createchannelresult)
---
## CreateChannelDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
> **Kind:** class
```csharp
public sealed class CreateChannelDialog
```
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
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 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.
---
## CreateChannelResult
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
> **Kind:** record
```csharp
public record CreateChannelResult(string Name, string? Topic, bool IsPublic, string? Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsPublic` | `bool` | — |
| `Password` | `string?` | — |
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.
---
@@ -0,0 +1,70 @@
# ProfileEditDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
## Contents
- [ProfileEditDialog](#profileeditdialog)
- [ProfileEditResult](#profileeditresult)
---
## ProfileEditDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
> **Kind:** class
```csharp
public sealed class ProfileEditDialog
```
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 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
- 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.
---
## ProfileEditResult
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
> **Kind:** record
```csharp
public record ProfileEditResult(string? DisplayName, string? Bio, string? NicknameColor, string? AvatarPath, bool? NotificationSoundEnabled, byte? NotificationVolume)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `DisplayName` | `string?` | — |
| `Bio` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `AvatarPath` | `string?` | — |
| `NotificationSoundEnabled` | `bool?` | — |
| `NotificationVolume` | `byte?` | — |
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 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
- Interpret any null value as 'no change' for that field when applying updates to the actual profile.
---
@@ -0,0 +1,49 @@
# ProfileViewDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
## Contents
- [ProfileViewDialog](#profileviewdialog)
- [ProfileAction](#profileaction)
---
## ProfileViewDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
> **Kind:** class
```csharp
public sealed class ProfileViewDialog
```
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
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 `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.
---
## ProfileAction
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
> **Kind:** enum
```csharp
public enum ProfileAction
{
Close,
EditProfile,
SetStatus
}
```
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.
---
@@ -0,0 +1,120 @@
# SearchDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
## Contents
- [SearchDialog](#searchdialog)
- [SearchResult](#searchresult)
- [SearchResultType](#searchresulttype)
---
## SearchDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** class
```csharp
public static class SearchDialog
```
## Source Code
Static class `SearchDialog` provides a Ctrl+K-activated, command-palette style dialog for navigating channels and triggering app actions. It merges the current `IReadOnlyList<string>` of `channels` with a fixed set of default `SearchResult` actions into a single searchable list presented in a `Dialog` consisting of a `Label` hint, a `TextField` input, and a `ListView` of results; typing filters the list and Enter selects. The `Show` method returns the selected `SearchResult` or `null` if canceled, communicating through the provided `IApplication` instance.
## Remarks
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 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`
---
## SearchResult
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** record
```csharp
public record SearchResult(SearchResultType Type, string Key, string Label)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Type` | `SearchResultType` | — |
| `Key` | `string` | — |
| `Label` | `string` | — |
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
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
- The `Key` should be stable and unique within a given `Type` to avoid ambiguity when presenting or selecting results.
---
## SearchResultType
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** enum
```csharp
public enum SearchResultType
{
Channel,
Action
}
```
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
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;
}
```
---
@@ -0,0 +1,55 @@
# StatusDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
## Contents
- [StatusDialog](#statusdialog)
- [StatusDialogResult](#statusdialogresult)
---
## StatusDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
> **Kind:** class
```csharp
public sealed class StatusDialog
```
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 serves as a focused UI primitive that isolates status-edit behavior from the rest of the application. By wiring `OptionSelector<UserStatus>` and a `TextField` to a lightweight `StatusDialogResult`, it provides a predictable, reusable pattern for collecting user input and converting it to a simple value object. This keeps the UI code cohesive while allowing the caller to handle the result without managing Terminal.Gui lifecycle details. The dialog is deliberately minimal and self-contained, relying on the provided `IApplication` to control its lifecycle.
## Notes
- 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`.
---
## StatusDialogResult
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
> **Kind:** record
```csharp
public record StatusDialogResult(UserStatus Status, string? StatusMessage)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Status` | [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
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
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.
---
@@ -0,0 +1,20 @@
# UpdateConfirmDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs`
> **Kind:** class
```csharp
public sealed class UpdateConfirmDialog
```
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
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.
@@ -0,0 +1,19 @@
# DroppedFileParser
> **File:** `src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs`
> **Kind:** class
```csharp
public static class DroppedFileParser
```
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
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
- 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`.
@@ -0,0 +1,19 @@
# EmojiHelper
> **File:** `src/EchoHub.Client/UI/Helpers/EmojiHelper.cs`
> **Kind:** class
```csharp
public static class EmojiHelper
```
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
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 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.
@@ -0,0 +1,17 @@
# HexColorHelper
> **File:** `src/EchoHub.Client/UI/Helpers/HexColorHelper.cs`
> **Kind:** class
```csharp
public static class HexColorHelper
```
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
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
- Leading whitespace before the optional '#' is not trimmed; strings starting with spaces will fail to parse gracefully.
@@ -0,0 +1,11 @@
# NickColorHelper
> **File:** `src/EchoHub.Client/UI/Helpers/NickColorHelper.cs`
> **Kind:** class
```csharp
public static class NickColorHelper
```
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.
@@ -0,0 +1,19 @@
# ChannelListSource
> **File:** `src/EchoHub.Client/UI/ListSources/ChannelListSource.cs`
> **Kind:** class
```csharp
public class ChannelListSource : IListDataSource
```
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 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
- `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`.
@@ -0,0 +1,19 @@
# SearchListSource
> **File:** `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
> **Kind:** class
```csharp
public class SearchListSource(List<SearchResult> items) : IListDataSource
```
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
`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
- `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`.
@@ -0,0 +1,24 @@
# UserListSource
> **File:** `src/EchoHub.Client/UI/ListSources/UserListSource.cs`
> **Kind:** class
```csharp
public class UserListSource : IListDataSource
```
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` 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 backing list and always fires a `Reset` change notification (not incremental add/remove events). Consumers that rely on fine-grained collection changes should account for that.
- `SuspendCollectionChangedEvent` prevents `Update(...)` from raising `CollectionChanged`. This is a simple way to batch updates, but callers are responsible for firing or forcing a refresh later if needed.
- `IsMarked(...)` and `SetMark(...)` are no-ops; `UserListSource` does not track per-item marks. Callers expecting mark semantics must manage marks externally.
- `GetUsername(...)` returns `null` when `index` is out of range; callers should check for `null` before using the result.
- `MaxItemLength` is computed from `Text.GetColumns()` for each item; it reflects display column width rather than raw `string.Length` and becomes `0` when the source is empty.
- `Render(...)` uses `GraphemeHelper.GetGraphemes(...)` and per-grapheme `GetColumns()` calls and will truncate output when `drawnChars + cols > width`. This ensures column-consistent drawing for wide or combining characters but may be relatively expensive if called frequently — consider caching grapheme data or avoiding per-frame allocations if rendering many items each frame.
- When `selected` is `true`, the code uses the `Focus`/`Normal` role mapping (`normalAttr`) for both prefix and username; the `NameColor` is ignored while selected. This is an intentional styling choice but may surprise callers who expect nickname coloring even for selected rows.
- `Dispose()` is empty; there are no unmanaged resources to free. The class is not explicitly thread-safe — concurrent calls to `Update(...)` and `Render(...)` without external synchronization may race.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
# HubConstants
> **File:** `src/EchoHub.Core/Constants/HubConstants.cs`
> **Kind:** class
```csharp
public static class HubConstants
```
HubConstants acts as the single source of truth for the chat hubs configurable limits and defaults. It groups static, compile-time constants that govern where the hub is exposed, how sessions are identified (including the IRC gateway prefix), and the upper bounds for messages, attachments, avatars, and embeds, providing a centralized reference that other components consult for validation and formatting.
## Remarks
HubConstants 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
- 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.
@@ -0,0 +1,25 @@
# MessageConventions
> **File:** `src/EchoHub.Core/Constants/MessageConventions.cs`
> **Kind:** class
```csharp
public static class MessageConventions
```
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
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
string content = MessageConventions.FormatAction("waves");
bool ok = MessageConventions.TryParseAction(content, out var actionText);
// ok == true, actionText == "waves"
```
## Notes
- `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.
@@ -0,0 +1,18 @@
# ValidationConstants
> **File:** `src/EchoHub.Core/Constants/ValidationConstants.cs`
> **Kind:** class
```csharp
public static partial class ValidationConstants
```
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
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-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.
@@ -0,0 +1,108 @@
# IChannelService.cs
> **Source:** `src/EchoHub.Core/Contracts/IChannelService.cs`
*Figure: How IChannelService 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
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
- [IChannelService](#ichannelservice)
- [ChannelListItem](#channellistitem)
---
## IChannelService
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
> **Kind:** interface
```csharp
public interface IChannelService
```
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
`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 then fetch its DTO if creation succeeded
var result = await channelService.CreateChannelAsync(creatorUserId, "general", "General chat", isPublic: true);
if (result.IsSuccess)
{
var channel = await channelService.GetChannelByNameAsync("general");
// use 'channel' (type: ChannelDto) for further operations
}
else
{
// handle failure (inspect result for details provided by the implementation)
}
```
## Notes
- 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.
---
## ChannelListItem
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
> **Kind:** record
```csharp
public record ChannelListItem(string Name, string? Topic, int OnlineCount, bool IsPublic = true, bool IsProtected = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `OnlineCount` | `int` | — |
| `IsPublic` | `bool` | `true` |
| `IsProtected` | `bool` | `false` |
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
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
var item = new ChannelListItem("general", "General discussion", 12);
```
## Notes
- Topic may be null to indicate no topic is set.
- IsPublic defaults to true and IsProtected defaults to false; pass explicit values to override.
---
@@ -0,0 +1,19 @@
# IChatBroadcaster
> **File:** `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
> **Kind:** interface
```csharp
public interface IChatBroadcaster
```
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
`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` 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.
@@ -0,0 +1,78 @@
# IChatService
> **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
```
Provides chat-layer operations for connection lifecycle, channel membership, messaging, presence and cross-process broadcasting. Reach for `IChatService` when implementing or calling the application-level chat logic (for example from controllers, real-time hubs or an IRC gateway) rather than manipulating lower-level transport or persistence APIs directly.
## Remarks
`IChatService` centralizes the domain operations needed by the real-time chat surface: tracking connections ([`UserConnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), joining and leaving channels ([`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`LeaveChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), sending and retrieving messages ([`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), presence ([`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetOnlineUsersAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), and broadcasting channel or message events to other processes ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface is designed for use by controllers and gateway components (the code comments indicate the IRC gateway uses several methods), so it intentionally mixes request/response operations (join, send) with one-way broadcast methods used to propagate state across processes.
## Example
```csharp
// Typical happy-path usage from a controller or hub
var (history, joinError, passwordRequired) = await chatService.JoinChannelAsync(connectionId, userId, username, "general");
if (joinError != null) {
// handle join failure (implementation-specific semantics)
return;
}
// Show the returned history to the user
foreach (var item in history) {
// item is a [`MessageDto`](../DTOs/ChatDtos.cs.md)
}
// Send a message; the returned nullable string has implementation-dependent meaning
var sendResult = await chatService.SendMessageAsync(userId, username, "general", "Hello everyone!");
if (sendResult != null) {
// react to non-null result per the concrete implementation
}
// Broadcast a message instance (e.g. from background processing or another gateway)
// `message` here is a [`MessageDto`](../DTOs/ChatDtos.cs.md) obtained from persistence or constructed by the implementation
// await chatService.BroadcastMessageAsync("general", message);
```
## Notes
- Several methods return `Task<string?>` (for example [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface does not document the exact semantics of a non-null string (error message vs. identifier vs. other). Consumers must consult the concrete implementation or its docs to interpret these values correctly.
- The `originConnectionId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) is used to avoid echoing a broadcast back to the originating connection (IRC-like behavior). Other sessions owned by the same user still receive the message.
- The `replyToMessageId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) must reference a message that exists in the same channel; implementations should validate this constraint.
- [`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns a tuple containing `History`, `Error`, and `PasswordRequired`. Callers should handle the `Error` and `PasswordRequired` flags before assuming `History` contains usable data.
- [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) supports simple pagination via `count` and `offset`; callers should choose `count` and `offset` to limit load and avoid returning excessively large histories in a single call.
- Broadcasting methods ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)) are intentionally one-way primitives used to notify other processes; they do not return operation results and callers should not rely on them for synchronous guarantees.
@@ -0,0 +1,100 @@
# IEchoHubClient
> **File:** `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
> **Kind:** interface
```csharp
public interface IEchoHubClient
```
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 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
using System;
using System.Threading.Tasks;
public class ConsoleEchoClient : IEchoHubClient
{
public Task ReceiveMessage(MessageDto message)
{
Console.WriteLine($"[{message.Channel}] {message.Sender}: {message.Text}");
return Task.CompletedTask;
}
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
{
Console.WriteLine($"User joined {channelName}: {username}");
return Task.CompletedTask;
}
public Task UserLeft(string channelName, string username)
{
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;
}
public Task Error(string message)
{
Console.WriteLine($"Error from server: {message}");
return Task.CompletedTask;
}
}
```
## Notes
- 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.
@@ -0,0 +1,34 @@
# IMessageEncryptionService
> **File:** `src/EchoHub.Core/Contracts/IMessageEncryptionService.cs`
> **Kind:** interface
```csharp
public interface IMessageEncryptionService
```
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 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
// Assume you have an instance of IMessageEncryptionService named `service`
string ciphertext = service.Encrypt("TopSecret");
string plaintext = service.Decrypt(ciphertext); // "TopSecret"
// Decrypting non-encrypted content yields the original value (pass-through)
string passthrough = service.Decrypt("plain-text"); // "plain-text"
string? nullableValue = null;
string? encNullable = service.EncryptNullable(nullableValue); // null
string? decNullable = service.DecryptNullable(encNullable); // null
```
## Notes
- 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.
@@ -0,0 +1,36 @@
# IUserService
> **File:** `src/EchoHub.Core/Contracts/IUserService.cs`
> **Kind:** interface
```csharp
public interface IUserService
```
`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 returning `Task<UserOperationResult>` for mutating operations and `Task<UserProfileDto?>` for profile queries, the interface cleanly models success/failure and optional data. The [`UserOperationResult`](../DTOs/CommonDtos.cs.md) type provides `Success(UserProfileDto user)` and `Fail(UserError error, string message)` helpers, enabling implementations to construct consistent outcomes. The `RegisterUserAsync` method carries a server-policies cue in its comment: when `Server:Registration = "invite"`, an `inviteCode` is required; in `"closed"` mode, new accounts are refused. This centralizes registration policy at the service boundary and avoids scattering policy checks across call sites.
## Example
```csharp
// Example usage of IUserService
public async Task DemoAsync(IUserService userService)
{
var reg = await userService.RegisterUserAsync("alice", "Secret123", inviteCode: "INVITE-42");
if (reg.IsSuccess)
{
var profile = await userService.GetUserProfileAsync("alice");
// Use profile as needed
}
}
```
## Notes
- 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.
@@ -0,0 +1,142 @@
# AccountDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
## Contents
- [DeleteAccountRequest](#deleteaccountrequest)
- [ExportedAttachmentDto](#exportedattachmentdto)
- [ExportedMessageDto](#exportedmessagedto)
- [UserDataExportDto](#userdataexportdto)
---
## DeleteAccountRequest
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record DeleteAccountRequest(string Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Password` | `string` | — |
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
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
- 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.
---
## ExportedAttachmentDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record ExportedAttachmentDto(
string FileName,
string Url,
long FileSize,
string Kind,
string ChannelName,
DateTimeOffset SentAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `FileName` | `string` | — |
| `Url` | `string` | — |
| `FileSize` | `long` | — |
| `Kind` | `string` | — |
| `ChannelName` | `string` | — |
| `SentAt` | `DateTimeOffset` | — |
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
`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 `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.
---
## ExportedMessageDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record ExportedMessageDto(
Guid Id,
string ChannelName,
DateTimeOffset SentAt,
string Content,
Guid? ReplyToMessageId)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `ChannelName` | `string` | — |
| `SentAt` | `DateTimeOffset` | — |
| `Content` | `string` | — |
| `ReplyToMessageId` | `Guid?` | — |
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
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.
---
## UserDataExportDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record UserDataExportDto(
DateTimeOffset ExportedAt,
string ServerName,
UserProfileDto Profile,
List<ExportedMessageDto> Messages,
List<ExportedAttachmentDto> Attachments)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ExportedAt` | `DateTimeOffset` | — |
| [`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md) | `string` | — |
| `Profile` | [`UserProfileDto`](ProfileDtos.cs.md) | — |
| `Messages` | `List<ExportedMessageDto>` | — |
| `Attachments` | `List<ExportedAttachmentDto>` | — |
`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
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
- Large exports can be memory-intensive; plan for streaming or chunked delivery in exporters.
---
@@ -0,0 +1,141 @@
# AuthDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
## Contents
- [LoginRequest](#loginrequest)
- [LoginResponse](#loginresponse)
- [RefreshRequest](#refreshrequest)
- [RegisterRequest](#registerrequest)
---
## LoginRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record LoginRequest(string Username, string Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `Password` | `string` | — |
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
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
- Do not log or serialize the `Password` value; treat `LoginRequest` as sensitive data and ensure transport uses TLS.
---
## LoginResponse
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record LoginResponse(
string Token,
string RefreshToken,
DateTimeOffset ExpiresAt,
string Username,
string? DisplayName,
string? NicknameColor)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md) | `string` | — |
| `RefreshToken` | `string` | — |
| `ExpiresAt` | `DateTimeOffset` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `NicknameColor` | `string?` | — |
`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
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: "defghijklmnopqrstuvwxyz",
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
Username: "alice",
DisplayName: "Alice",
NicknameColor: "#1E90FF"
);
```
## Notes
- 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.
---
## RefreshRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record RefreshRequest(string RefreshToken)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `RefreshToken` | `string` | — |
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
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
- `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.
---
## RegisterRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record RegisterRequest(string Username, string Password, string? DisplayName = null, string? InviteCode = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `Password` | `string` | — |
| `DisplayName` | `string?` | `null` |
| [`InviteCode`](../Models/InviteCode.cs.md) | `string?` | `null` |
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
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 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.
---
@@ -0,0 +1,504 @@
# ChatDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
## Contents
- [AttachmentDto](#attachmentdto)
- [ChannelCryptoDto](#channelcryptodto)
- [ChannelDto](#channeldto)
- [ChannelMetaDto](#channelmetadto)
- [CreateChannelRequest](#createchannelrequest)
- [EmbedDto](#embeddto)
- [JoinChannelResult](#joinchannelresult)
- [MessageDto](#messagedto)
- [RekeyChannelRequest](#rekeychannelrequest)
- [ReplyRefDto](#replyrefdto)
- [SendMessageRequest](#sendmessagerequest)
- [SendUrlRequest](#sendurlrequest)
- [UpdateTopicRequest](#updatetopicrequest)
- [UserDto](#userdto)
---
## AttachmentDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record AttachmentDto(
AttachmentKind Kind,
string Url,
string FileName,
long FileSize,
string? AsciiPreview = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Kind` | [`AttachmentKind`](../Models/AttachmentKind.cs.md) | — |
| `Url` | `string` | — |
| `FileName` | `string` | — |
| `FileSize` | `long` | — |
| `AsciiPreview` | `string?` | `null` |
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
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.
---
## ChannelCryptoDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelCryptoDto(bool IsEncrypted, string? EncryptionSalt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `IsEncrypted` | `bool` | — |
| `EncryptionSalt` | `string?` | — |
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
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.
---
## ChannelDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelDto(
Guid Id,
string Name,
string? Topic,
bool IsPublic,
int MessageCount,
DateTimeOffset CreatedAt,
bool IsProtected = false,
bool IsEncrypted = false,
bool IsSystem = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsPublic` | `bool` | — |
| `MessageCount` | `int` | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `IsProtected` | `bool` | `false` |
| `IsEncrypted` | `bool` | `false` |
| `IsSystem` | `bool` | `false` |
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 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(
Guid.NewGuid(),
"general",
"General discussion",
true,
128,
DateTimeOffset.UtcNow
);
```
## Notes
- Topic is nullable; consumers should handle `null` before displaying a topic, or provide a fallback.
---
## ChannelMetaDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelMetaDto(
Guid Id,
string Name,
string? Topic,
bool IsEncrypted,
bool IsProtected,
int MessageCount,
int UniqueUserCount,
long EstimatedSizeBytes,
DateTimeOffset CreatedAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsEncrypted` | `bool` | — |
| `IsProtected` | `bool` | — |
| `MessageCount` | `int` | — |
| `UniqueUserCount` | `int` | — |
| `EstimatedSizeBytes` | `long` | — |
| `CreatedAt` | `DateTimeOffset` | — |
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
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
- 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.
---
## CreateChannelRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record CreateChannelRequest(
string Name,
string? Topic = null,
bool IsPublic = true,
string? Password = null,
string? EncryptionSalt = null,
string? WrappedRoomKey = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | `null` |
| `IsPublic` | `bool` | `true` |
| `Password` | `string?` | `null` |
| `EncryptionSalt` | `string?` | `null` |
| `WrappedRoomKey` | `string?` | `null` |
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
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
- 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.
---
## EmbedDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record EmbedDto(
string? SiteName,
string? Title,
string? Description,
string? ImageAscii,
string Url,
string? ThemeColor = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `SiteName` | `string?` | — |
| `Title` | `string?` | — |
| `Description` | `string?` | — |
| `ImageAscii` | `string?` | — |
| `Url` | `string` | — |
| `ThemeColor` | `string?` | `null` |
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
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: "EchoHub",
Title: "Welcome",
Description: "A friendly hello from EchoHub.",
ImageAscii: " ___ \n (o o) \n \_/ ",
Url: "https://echohub.example",
ThemeColor: "#4B8BBE"
);
```
## Notes
- `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.
---
## JoinChannelResult
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record JoinChannelResult(
bool Success,
List<MessageDto> History,
string? Error = null,
bool PasswordRequired = false,
string? EncryptionSalt = null,
string? WrappedRoomKey = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| `History` | `List<MessageDto>` | — |
| `Error` | `string?` | `null` |
| `PasswordRequired` | `bool` | `false` |
| `EncryptionSalt` | `string?` | `null` |
| `WrappedRoomKey` | `string?` | `null` |
Represents the outcome of a join-channel operation as a `JoinChannelResult` type. It exposes a `bool` `Success` flag, a `List<MessageDto>` `History` of messages retrieved for the channel, and optional metadata including a `string?` `Error`, a `bool` `PasswordRequired`, and optional encryption data (`string?` `EncryptionSalt`, `string?` `WrappedRoomKey`).
---
## MessageDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record MessageDto(
Guid Id,
string Content,
string SenderUsername,
string? SenderNicknameColor,
string ChannelName,
DateTimeOffset SentAt,
List<AttachmentDto>? Attachments = null,
List<EmbedDto>? Embeds = null,
string? SenderDisplayName = null,
ReplyRefDto? ReplyTo = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Content` | `string` | — |
| `SenderUsername` | `string` | — |
| `SenderNicknameColor` | `string?` | — |
| `ChannelName` | `string` | — |
| `SentAt` | `DateTimeOffset` | — |
| `Attachments` | `List<AttachmentDto>?` | `null` |
| `Embeds` | `List<EmbedDto>?` | `null` |
| `SenderDisplayName` | `string?` | `null` |
| `ReplyTo` | `ReplyRefDto?` | `null` |
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 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`; treat them as empty sequences when rendering or iterating.
---
## RekeyChannelRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record RekeyChannelRequest(
string OldPassword,
string NewPassword,
string NewEncryptionSalt,
string NewWrappedRoomKey)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `OldPassword` | `string` | — |
| `NewPassword` | `string` | — |
| `NewEncryptionSalt` | `string` | — |
| `NewWrappedRoomKey` | `string` | — |
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 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 `OldPassword` or `NewPassword`; treat these values as ephemeral and ensure transport-layer secrecy.
---
## ReplyRefDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ReplyRefDto(
Guid MessageId,
string SenderUsername,
string Content)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `MessageId` | `Guid` | — |
| `SenderUsername` | `string` | — |
| `Content` | `string` | — |
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
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
- 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.
---
## SendMessageRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record SendMessageRequest(string ChannelName, string Content)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ChannelName` | `string` | — |
| `Content` | `string` | — |
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.
---
## SendUrlRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record SendUrlRequest(string Url)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Url` | `string` | — |
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
`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 req = new SendUrlRequest("https://example.com");
```
## Notes
- No URL validation is performed by this type; validate the URL in the caller or service layer before processing.
---
## UpdateTopicRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record UpdateTopicRequest(string? Topic)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Topic` | `string?` | — |
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.
---
## UserDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record UserDto(
Guid Id,
string Username,
string? DisplayName,
string? NicknameColor,
UserStatus Status,
DateTimeOffset LastSeenAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `LastSeenAt` | `DateTimeOffset` | — |
`UserDto` is an immutable data transfer object that carries a concise snapshot of a user for chat workflows. It exposes the users `Id` (`Guid`), `Username`, optional `DisplayName` and `NicknameColor`, the current `Status` ([`UserStatus`](../Models/UserStatus.cs.md)), and the `LastSeenAt` timestamp (`DateTimeOffset`). Use this DTO when returning or transferring lightweight user data across API boundaries or UI layers instead of exposing full domain entities.
## Remarks
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
- 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.
---
@@ -0,0 +1,254 @@
# CommonDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
## Contents
- [ApiResponse](#apiresponse)
- [ApiResponse](#apiresponse-1)
- [ChannelOperationResult](#channeloperationresult)
- [ErrorResponse](#errorresponse)
- [PaginatedResponse](#paginatedresponse)
- [UserOperationResult](#useroperationresult)
- [ChannelError](#channelerror)
- [UserError](#usererror)
---
## ApiResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ApiResponse(bool Success, string? Message = null, List<string>? Errors = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
| `Errors` | `List<string>?` | `null` |
Represents a standard API outcome as a `record` with a `bool` `Success`, an optional `string?` [`Message`](../Models/Message.cs.md), and an optional `List<string>?` `Errors`. Use `ApiResponse` to package the result of API operations or service methods into a single, strongly-typed object for consistent client consumption instead of scattering boolean flags and messages across code.
## Remarks
By centralizing outcome data in `ApiResponse`, callers can handle success/failure logic in a uniform way and avoid ad-hoc boolean checks scattered through the code. The `Errors` collection is intended for granular, field-level validation messages that the client can display; the [`Message`](../Models/Message.cs.md) offers a concise summary, while `Success` drives flow control.
## Example
```csharp
var success = new ApiResponse(true);
var failure = new ApiResponse(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" });
```
## Notes
- The `Errors` property is a mutable `List<string>`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList<string>` or an immutable collection.
- When `Success` is true, you may omit [`Message`](../Models/Message.cs.md) and `Errors` or set them as appropriate; when `Success` is false, provide a meaningful [`Message`](../Models/Message.cs.md) and optionally populate `Errors` to detail issues.
---
## ApiResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ApiResponse<T>(bool Success, string? Message = null, List<string>? Errors = null, T? Data = default)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
| `Errors` | `List<string>?` | `null` |
| `Data` | `T?` | `default` |
A generic wrapper for operation results that standardizes API responses. It indicates success with `Success` and carries an optional [`Message`](../Models/Message.cs.md), a `List<string>` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`.
## Remarks
This abstraction decouples the shape of a successful response from the actual data, enabling consistent error handling and client-side parsing across services. By returning `ApiResponse<T>` from operations, you centralize how success, messages, and validation details are conveyed, which simplifies cross-cutting concerns like localization and error translation.
## Example
```csharp
// Successful response with data
var success = new ApiResponse<string>(true, "Operation completed", null, "payload-data");
// Failed response with errors
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" }, null);
```
---
## ChannelOperationResult
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ChannelOperationResult(ChannelDto? Channel, ChannelError? Error, string? ErrorMessage)
{
public bool IsSuccess => Error is null;
public static ChannelOperationResult Success(ChannelDto channel) => new(channel, null, null);
public static ChannelOperationResult Fail(ChannelError error, string message) => new(null, error, message);
}
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`Channel`](../Models/Channel.cs.md) | `ChannelDto?` | — |
| `Error` | `ChannelError?` | — |
| `ErrorMessage` | `string?` | — |
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
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 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`.
---
## ErrorResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ErrorResponse(string Error, string? Detail = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Error` | `string` | — |
| `Detail` | `string?` | `null` |
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
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.
---
## PaginatedResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record PaginatedResponse<T>(List<T> Items, int Total, int Offset, int Limit)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Items` | `List<T>` | — |
| `Total` | `int` | — |
| `Offset` | `int` | — |
| `Limit` | `int` | — |
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
Using a `record` for `PaginatedResponse<T>` gives value-based equality and an immutable data shape, which makes it natural for transporting paging results across boundaries. It centralizes both the data (`Items`) and its paging metadata (`Total`, `Offset`, `Limit`) in a single coherent DTO, reducing the risk of mismatch between data and paging state when consumed by clients or other services.
## Notes
- The `Items` collection is a `List<T>`, which is mutable. If you require immutability guarantees, wrap it in a read-only collection or clone the list before exposure.
---
## UserOperationResult
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record UserOperationResult(UserProfileDto? User, UserError? Error, string? ErrorMessage)
{
public bool IsSuccess => Error is null;
public static UserOperationResult Success(UserProfileDto user) => new(user, null, null);
public static UserOperationResult Fail(UserError error, string message) => new(null, error, message);
}
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`User`](../Models/User.cs.md) | `UserProfileDto?` | — |
| `Error` | `UserError?` | — |
| `ErrorMessage` | `string?` | — |
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
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
- 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`.
---
## ChannelError
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** enum
```csharp
public enum ChannelError
{
ValidationFailed,
AlreadyExists,
NotFound,
Forbidden,
Protected
}
```
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
`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.
---
## UserError
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** enum
```csharp
public enum UserError
{
ValidationFailed,
AlreadyExists,
NotFound,
InvalidCredentials,
Banned
}
```
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
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
- When mapping these errors to user-facing messages, avoid exposing sensitive internal details and rely on generic messaging driven by the enum value.
---
@@ -0,0 +1,69 @@
# InviteDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
## Contents
- [CreateInviteRequest](#createinviterequest)
- [InviteDto](#invitedto)
---
## CreateInviteRequest
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
> **Kind:** record
```csharp
public record CreateInviteRequest(int? MaxUses = null, int? ExpiresInHours = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `MaxUses` | `int?` | `null` |
| `ExpiresInHours` | `int?` | `null` |
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
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 values indicate 'not specified' and will be treated as absent by the invite-creation endpoint; set only the fields you intend to constrain.
---
## InviteDto
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
> **Kind:** record
```csharp
public record InviteDto(
string Code,
string CreatedByUsername,
DateTimeOffset CreatedAt,
DateTimeOffset? ExpiresAt,
int MaxUses,
int UseCount)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Code` | `string` | — |
| `CreatedByUsername` | `string` | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `ExpiresAt` | `DateTimeOffset?` | — |
| `MaxUses` | `int` | — |
| `UseCount` | `int` | — |
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
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.
---
@@ -0,0 +1,109 @@
# ModerationDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
## Contents
- [AssignRoleRequest](#assignrolerequest)
- [BanRequest](#banrequest)
- [KickRequest](#kickrequest)
- [MuteRequest](#muterequest)
---
## AssignRoleRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record AssignRoleRequest(string Username, ServerRole Role)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
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
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` 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.
---
## BanRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record BanRequest(string? Reason = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Reason` | `string?` | `null` |
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.
---
## KickRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record KickRequest(string? Reason = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Reason` | `string?` | `null` |
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 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 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.
---
## MuteRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record MuteRequest(string? Reason = null, int? DurationMinutes = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Reason` | `string?` | `null` |
| `DurationMinutes` | `int?` | `null` |
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.
---
@@ -0,0 +1,179 @@
# ProfileDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
## Contents
- [AvatarUploadResponse](#avataruploadresponse)
- [UpdateProfileRequest](#updateprofilerequest)
- [UpdateStatusRequest](#updatestatusrequest)
- [UserPresenceDto](#userpresencedto)
- [UserProfileDto](#userprofiledto)
---
## AvatarUploadResponse
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record AvatarUploadResponse(string AvatarAscii)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `AvatarAscii` | `string` | — |
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.
---
## UpdateProfileRequest
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UpdateProfileRequest(
string? DisplayName = null,
string? Bio = null,
string? NicknameColor = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `DisplayName` | `string?` | `null` |
| `Bio` | `string?` | `null` |
| `NicknameColor` | `string?` | `null` |
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
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
var request = new UpdateProfileRequest(DisplayName: "Nova", NicknameColor: "#FFAA00");
```
## Notes
- Ensure the update handler interprets nulls as "no change" to avoid overwriting existing values.
---
## UpdateStatusRequest
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UpdateStatusRequest(
UserStatus Status,
string? StatusMessage = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | `null` |
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
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
- The `StatusMessage` property is nullable. Callers must handle the possibility of a missing message when consuming this payload.
---
## UserPresenceDto
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UserPresenceDto(
string Username,
string? DisplayName,
string? NicknameColor,
UserStatus Status,
string? StatusMessage,
ServerRole Role,
bool IsIrc = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
| `IsIrc` | `bool` | `false` |
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
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.
---
## UserProfileDto
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UserProfileDto(
Guid Id,
string Username,
string? DisplayName,
string? Bio,
string? NicknameColor,
string? AvatarAscii,
UserStatus Status,
string? StatusMessage,
ServerRole Role,
DateTimeOffset CreatedAt,
DateTimeOffset LastSeenAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `Bio` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `AvatarAscii` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `LastSeenAt` | `DateTimeOffset` | — |
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
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.
---
@@ -0,0 +1,78 @@
# ServerDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
## Contents
- [EncryptionKeyResponse](#encryptionkeyresponse)
- [ServerStatusDto](#serverstatusdto)
---
## EncryptionKeyResponse
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
> **Kind:** record
```csharp
public record EncryptionKeyResponse(string Key)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Key` | `string` | — |
`EncryptionKeyResponse` is a concise data-transfer `record` that carries a single string property named `Key`, representing an encryption key. Use this type when an API response or internal boundary needs to convey the key as a structured envelope rather than a raw string, benefiting from the immutability and value-based equality of a `record`.
## Remarks
By modeling the payload as its own type, this symbol helps keep key handling explicit and self-describing across boundaries. It pairs with other server DTOs to form a consistent contract for encryption-related data, and it can evolve to carry extra metadata (expiry, algorithm) without breaking existing clients.
## Notes
- Treat the `Key` as sensitive data; avoid logging it or exposing it in traces. Ensure it is transmitted only over secure channels and managed according to your security policy.
---
## ServerStatusDto
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
> **Kind:** record
```csharp
public record ServerStatusDto(
string Name,
string? Description,
int OnlineUsers,
int TotalChannels,
string RegistrationMode = "open",
string Version = "0.0.0")
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Description` | `string?` | — |
| `OnlineUsers` | `int` | — |
| `TotalChannels` | `int` | — |
| `RegistrationMode` | `string` | `"open"` |
| `Version` | `string` | `"0.0.0"` |
Represents a compact, immutable data transfer object that conveys a server's identity and current activity. It exposes the server's `Name`, optional `Description`, the `OnlineUsers` count, the `TotalChannels`, and optional `RegistrationMode` and `Version` (defaulting to `"open"` and `"0.0.0"` when omitted). Use this DTO in API responses or status endpoints to deliver a stable snapshot of server state.
## Remarks
Because it is a `record`, `ServerStatusDto` benefits from value-based equality and deconstruction semantics, making it convenient to compare status payloads in tests or across clients. The trailing `RegistrationMode` and `Version` parameters are optional in construction, allowing callers to supply just the core metrics while still producing a complete payload. This DTO isolates status representation from internal domain entities and keeps the shape stable for clients and tooling.
## Example
```csharp
// Minimal construction: Description omitted (use null)
var status = new ServerStatusDto("EchoHub", null, 12, 3);
// Full construction with explicit values
var statusFull = new ServerStatusDto("EchoHub", "Main gateway", 12, 3, "open", "1.2.0");
```
---
@@ -0,0 +1,21 @@
# Attachment
> **File:** `src/EchoHub.Core/Models/Attachment.cs`
> **Kind:** class
```csharp
public class Attachment
```
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
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
- 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.
@@ -0,0 +1,19 @@
# AttachmentKind
> **File:** `src/EchoHub.Core/Models/AttachmentKind.cs`
> **Kind:** enum
```csharp
public enum AttachmentKind
{
Image,
Audio,
File
}
```
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 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.
@@ -0,0 +1,11 @@
# Channel
> **File:** `src/EchoHub.Core/Models/Channel.cs`
> **Kind:** class
```csharp
public class Channel
```
Channel models a chat channel within EchoHub's chat surface. It exposes an identifier `Id` (`Guid`), a required `Name` (`string`), an optional `Topic` (`string?`), and a flag `IsPublic` (`bool`) that defaults to `true`. The model also supports server-managed channels via `IsSystem` (`bool`), which are auto-created and read-only for all roles; users cannot create them. When a channel is password-protected, `PasswordHash` (`string?`) stores the hashed password. For end-to-end encryption, the envelope is represented by `EncryptionSalt` (`string?`) and `WrappedRoomKey` (`string?`), both client-generated so that the server never has access to the room content. Creation metadata is captured by `CreatedAt` (`DateTimeOffset`) and `CreatedByUserId` (`Guid`). The `Messages` collection (`List&lt;Message&gt;`) contains the related [`Message`](Message.cs.md) entities that belong to this channel.
@@ -0,0 +1,18 @@
# ChannelMembership
> **File:** `src/EchoHub.Core/Models/ChannelMembership.cs`
> **Kind:** class
```csharp
public class ChannelMembership
```
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
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
- `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.
@@ -0,0 +1,11 @@
# InviteCode
> **File:** `src/EchoHub.Core/Models/InviteCode.cs`
> **Kind:** class
```csharp
public class InviteCode
```
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.
@@ -0,0 +1,43 @@
# Message
> **File:** `src/EchoHub.Core/Models/Message.cs`
> **Kind:** class
```csharp
public class Message
```
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
Message is the domain aggregate for a chat entry, linking to its [`Channel`](Channel.cs.md) via `ChannelId`/[`Channel`](Channel.cs.md) and to its sender via `SenderUserId`/`SenderUsername`. Attachments are modeled as a separate collection (`Attachments`), enabling a clean separation between textual payloads and media. Legacy fields (`Type`, `AttachmentUrl`, `AttachmentFileName`, `AttachmentFileSize`) exist solely to support a one-time startup data migration into the new attachments model; new writes should use the `Attachments` collection, and these legacy fields are not exposed in DTOs and are nulled after migration. The `ReplyToMessageId` enables basic threading by pointing to the message this one replies to, if any; downstream logic should gracefully handle references to messages that may have been deleted.
## Example
```csharp
var message = new Message
{
Id = Guid.NewGuid(),
Content = "Welcome to the channel!",
SenderUserId = Guid.NewGuid(),
SenderUsername = "system",
ChannelId = Guid.NewGuid(),
Attachments = new List<Attachment>
{
new Attachment
{
Id = Guid.NewGuid(),
MessageId = Guid.Empty,
Url = "https://example.com/file.png",
FileName = "file.png",
FileSize = 4096
}
},
SentAt = DateTimeOffset.UtcNow
};
```
## Notes
- Legacy fields are for migration only; do not rely on them for new code.
- `SentAt` defaults to `DateTimeOffset.UtcNow` on instantiation; override if you have a specific send time.
- Use `EmbedJson` for optional rich content, and handle its absence gracefully in the UI.
@@ -0,0 +1,24 @@
# MessageType
> **File:** `src/EchoHub.Core/Models/MessageType.cs`
> **Kind:** enum
```csharp
public enum MessageType
{
Text,
Image,
File,
Audio
}
```
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
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 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.
@@ -0,0 +1,34 @@
# RefreshToken
> **File:** `src/EchoHub.Core/Models/RefreshToken.cs`
> **Kind:** class
```csharp
public class RefreshToken
```
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
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.
@@ -0,0 +1,20 @@
# ServerRole
> **File:** `src/EchoHub.Core/Models/ServerRole.cs`
> **Kind:** enum
```csharp
public enum ServerRole
{
Member = 0,
Mod = 1,
Admin = 2,
Owner = 3
}
```
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
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.

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