Reads raw image data from the OS clipboard (e.g. an image copied from a browser, or a
+Win+Shift+S screenshot), which terminals cannot paste as text. Always returns PNG bytes:
+clipboard PNG data is passed through, clipboard bitmaps (CF_DIB) are re-encoded.
Converts clipboard DIB bytes (a BITMAPINFOHEADER/V4/V5 + optional palette/masks + pixel
+data, i.e. a .bmp file without its 14-byte file header) to PNG. Returns null when the
+data is malformed or not decodable as a bitmap.
Encrypts cached room content keys at rest so the client config never holds them as
+plain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other
+platforms the keys are AES-GCM encrypted with a per-user master key file stored next
+to the config with 0600 permissions (prefix "k1:") — without an OS keychain that is
+file-permission-level protection, not zero-knowledge: anyone who can read both the
+config and the key file can recover the room keys. Values with no recognized prefix
+are legacy plain-base64 keys from older clients; they load once and are re-encrypted.
+The room passphrase itself is never stored in any form.
Decrypts a stored value back into a room key. wasLegacy is true when
+the value was an unencrypted legacy entry that should be re-persisted via
+Protect(byte[]). Returns false for unreadable values (wrong user/machine, missing
+or regenerated key file, malformed data) — the caller drops the entry and the user can
+recover it by re-entering the passphrase.
+
+
+
+
+
public bool TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy)
@@ -276,6 +346,42 @@ so users don't retype the passphrase every launch. Keys never leave this machine
+
+
+
+ MarkChannelEncrypted(string, bool)
+
+
+
+
Records whether a channel is end-to-end encrypted (from channel listings, crypto
+metadata, or join outcomes). Senders consult this to block plaintext into rooms
+whose key isn't cached.
+
+
+
+
+
public void MarkChannelEncrypted(string channelName, bool isEncrypted)
Unwraps a fresh key envelope and caches the key, overwriting any stale cached key
+(e.g. the channel was deleted and recreated under the same name, so the old key
+would encrypt messages nobody else can read). Returns false when the KEK doesn't
+open the envelope — the cache is left untouched.
+
+
+
+
+
public bool TryStoreFromEnvelope(string channelName, string wrappedRoomKey, byte[] kek)
Thrown when sending into an end-to-end encrypted channel whose room key isn't cached:
+without the key the message would leave the client as plaintext, which must never happen.
+
+
+
+
+
public sealed class RoomLockedException : Exception
+
+
+
+
+
+
+
diff --git a/api/client/EchoHub.Client.Services.html b/api/client/EchoHub.Client.Services.html
index ff392eb..b8bc2a5 100644
--- a/api/client/EchoHub.Client.Services.html
+++ b/api/client/EchoHub.Client.Services.html
@@ -129,6 +129,13 @@ so messages are encrypted end-to-end between client and server.
Reads file paths that live on the OS clipboard as a file list (e.g. after copying a file in
Explorer/Finder/Nautilus), which terminals do not paste as text. Lets Ctrl+V attach a copied
file directly instead of requiring the user to paste a raw path.
Reads raw image data from the OS clipboard (e.g. an image copied from a browser, or a
+Win+Shift+S screenshot), which terminals cannot paste as text. Always returns PNG bytes:
+clipboard PNG data is passed through, clipboard bitmaps (CF_DIB) are re-encoded.
@@ -168,13 +175,34 @@ For normal channels only PathSetup
Ensures the application's directory is on the system PATH so users
can run 'echohub' from any terminal session.
Encrypts cached room content keys at rest so the client config never holds them as
+plain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other
+platforms the keys are AES-GCM encrypted with a per-user master key file stored next
+to the config with 0600 permissions (prefix "k1:") — without an OS keychain that is
+file-permission-level protection, not zero-knowledge: anyone who can read both the
+config and the key file can recover the room keys. Values with no recognized prefix
+are legacy plain-base64 keys from older clients; they load once and are re-encrypted.
+The room passphrase itself is never stored in any form.
Holds room content keys for end-to-end encrypted channels: in-memory for the
active session, persisted per-server in the client config (like saved sessions)
-so users don't retype the passphrase every launch. Keys never leave this machine.
+so users don't retype the passphrase every launch. Keys never leave this machine
+and are encrypted at rest by RoomKeyProtector. Also tracks which
+channels are known to be end-to-end encrypted, so senders can refuse to emit
+plaintext into a room whose key isn't cached yet.
+
Thrown when sending into an end-to-end encrypted channel whose room key isn't cached:
+without the key the message would leave the client as plaintext, which must never happen.
diff --git a/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html b/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html
index 8267cfd..11a9463 100644
--- a/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html
+++ b/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html
@@ -459,8 +459,8 @@ Active channel gets a > indicator, unread channels are bright with a count ba
-
diff --git a/api/client/EchoHub.Client.UI.MainWindow.html b/api/client/EchoHub.Client.UI.MainWindow.html
index 1298eb9..f5992f1 100644
--- a/api/client/EchoHub.Client.UI.MainWindow.html
+++ b/api/client/EchoHub.Client.UI.MainWindow.html
@@ -1698,6 +1698,70 @@ current ASCII-art size for images. Passing an empty list restores the default hi
+
+ OnFilesStaged
+
+
+
+
Fired when local files arrive via paste or drag-and-drop to be staged as attachments.
+Parameters: channel name, absolute paths of existing files.
+
+
+
+
+
public event Action<string, IReadOnlyList<string>>? OnFilesStaged
Fired when raw image data is pasted from the clipboard (e.g. copied from a browser or a
+screenshot tool). Parameters: channel name, PNG-encoded image bytes.
+
+
+
+
+
public event Action<string, byte[]>? OnImagePasted
- Deconstruct(out Guid, out string, out string, out string?, out string, out DateTimeOffset, out List<AttachmentDto>?, out List<EmbedDto>?)
+
+ Deconstruct(out Guid, out string, out string, out string?, out string, out DateTimeOffset, out List<AttachmentDto>?, out List<EmbedDto>?, out string?)
+
diff --git a/api/server/EchoHub.Server.Services.PresenceTracker.html b/api/server/EchoHub.Server.Services.PresenceTracker.html
index 6f15bcc..c1760a1 100644
--- a/api/server/EchoHub.Server.Services.PresenceTracker.html
+++ b/api/server/EchoHub.Server.Services.PresenceTracker.html
@@ -396,6 +396,44 @@ so the caller can broadcast departures and force-disconnect connections.
+
+
+
+ IsIrcOnly(string)
+
+
+
+
True when the user is online exclusively through the IRC gateway. A user who also has a
+native client connected has full features, so they don't count as IRC-only.
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 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.
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 page for details on how the IRC gateway integrates with the chat service.
+
See the IRC Gateway guide for command mapping, attachment rendering, and limitations, or Architecture for how the gateway integrates with the chat service.
Configuration
Server configuration is in appsettings.json (auto-generated on first run). You can also use environment variables or command-line arguments to override settings.
See the Configuration guide for the full reference and how it all works.
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).
+
Enabling the gateway
+
The gateway is off by default. Enable it in appsettings.json (or Irc__Enabled=true as an
+environment variable):
The plaintext listener always starts on Port. The TLS listener on TlsPort starts only
+when TlsEnabled is trueandTlsCertPath points to a PKCS#12 (.pfx) certificate.
+See the configuration reference for every option.
+
Connecting & authentication
+
Your IRC nick is your EchoHub username and your server password is your account
+password. Two flows are supported:
+
# 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: 3–50 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 NOTICEs.
+
+
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 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.
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 (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.
+
┌ 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 (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).
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.
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 higherand 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.)
+
+
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 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.
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.
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)
+
+
+
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)
+
+
+
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>
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 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.
A reliability and security pass over end-to-end encrypted rooms: locked channels now offer the passphrase prompt instead of dead-ending on the "rejoin to unlock" placeholder, the client can no longer be tricked into sending plaintext into an encrypted room, and cached room keys are encrypted at rest instead of sitting in the config as base64. Ctrl+V grows up too — images copied from a browser or screenshot tool paste straight into the chat as attachments, and copying several files pastes them all into one message. Plus a set of IRC gateway fixes — decrypted image previews, display-name plumbing, and user/channel indicators.
+
New Features
+
+
Paste images straight from the clipboard — copy an image from a browser, a screenshot tool (Win+Shift+S), or an image editor and Ctrl+V it into the input: it's attached as a PNG (image.png), no saving to disk first, Discord-style. Transparency is preserved when the source provides PNG data; plain clipboard bitmaps are converted automatically. On Linux this uses wl-paste/xclip; on macOS it requires pngpaste. In end-to-end encrypted rooms pasted images go through the same client-side encryption as any other attachment.
+
Multi-file paste — copying several files in your file manager and pasting attaches them all to a single message (up to the 10-attachment cap), staged as one batch alongside anything you type as the caption. Previously each pasted file was routed through its own /send, which could misbehave on large batches.
+
Room keys encrypted at rest — the per-channel room keys cached so you don't retype a passphrase every launch are no longer stored as plain base64 in config.json. On Windows they're protected with DPAPI (current-user scope); on Linux/macOS with AES-GCM under a per-user key file created with 0600 permissions next to the config. Existing plain entries migrate to the encrypted format automatically on first load. The passphrase itself is never stored in any form.
+
[irc] tag in the users panel — users online only through the IRC gateway are tagged [irc], useful context since IRC clients lack encryption, attachments, and profiles. Someone also running the TUI shows untagged.
+
~ marker for private channels — the channel list now marks private (unlisted) channels with a trailing ~, alongside the existing * for password-protected ones (#room*~ when both apply).
+
+
Bug Fixes
+
+
Locked encrypted channels now prompt for the passphrase. Auto-joining your channels at connect silently entered end-to-end encrypted rooms you're a member of without running the unlock flow — on a new device (or after a cancelled prompt) the room showed only [encrypted — rejoin this channel with its passphrase to unlock] placeholders, and only a manual /join recovered it. Selecting the channel now offers the passphrase prompt; entering it unlocks history and live messages in place. Cancelling is remembered for the session so reselecting the channel doesn't nag — /join or trying to send always re-offers the prompt.
+
A stale cached room key no longer beats a fresh one. If an encrypted channel was deleted and recreated under the same name, a client that still had the old key cached kept encrypting messages nobody else could read. Typing the passphrase on join now always adopts the key from the server's current envelope, replacing the stale cache.
+
IRC clients no longer get flooded with ciphertext for image messages. The gateway forwarded image ASCII previews without stripping transport encryption, spamming IRC clients with one enormous $ENC$v1$… line per image. Previews are now decrypted before formatting, and any that still can't be read (e.g. end-to-end room ciphertext the server cannot decrypt) are skipped in favor of the plain [Image: name] url line.
+
Display names now show on chat messages. Messages only carried the sender's username, so a configured display name appeared in the user list but not on the messages themselves. Live messages, history, and attachment messages now all carry it; mention and profile lookups stay keyed to the username.
+
+
Security
+
+
No plaintext can leak into an encrypted room. Previously, a client without the room key silently sent unencrypted text into an end-to-end encrypted channel (and other members saw it as a normal message, none the wiser it went over the wire readable by the server). All send paths — typed messages, staged file attachments, and URL sends — are now blocked while a room is locked: the client offers the unlock prompt, keeps staged files in the tray, and refuses to transmit until the key is present, with a hard guard at the connection layer as backstop.
diff --git a/index.json b/index.json
index a592ef5..01a868a 100644
--- a/index.json
+++ b/index.json
@@ -37,7 +37,7 @@
"api/client/EchoHub.Client.Config.ConfigManager.html": {
"href": "api/client/EchoHub.Client.Config.ConfigManager.html",
"title": "Class ConfigManager | EchoHub Documentation",
- "summary": "Class ConfigManager Namespace EchoHub.Client.Config Assembly EchoHub.Client.dll public static class ConfigManager Inheritance object ConfigManager Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Methods Load() public static ClientConfig Load() Returns ClientConfig RemoveServer(string) public static void RemoveServer(string url) Parameters url string Save(ClientConfig) public static void Save(ClientConfig config) Parameters config ClientConfig SaveServer(SavedServer) public static void SaveServer(SavedServer server) Parameters server SavedServer"
+ "summary": "Class ConfigManager Namespace EchoHub.Client.Config Assembly EchoHub.Client.dll public static class ConfigManager Inheritance object ConfigManager Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Properties ConfigDirectory Directory holding the client config and local key material. public static string ConfigDirectory { get; } Property Value string Methods Load() public static ClientConfig Load() Returns ClientConfig RemoveServer(string) public static void RemoveServer(string url) Parameters url string Save(ClientConfig) public static void Save(ClientConfig config) Parameters config ClientConfig SaveServer(SavedServer) public static void SaveServer(SavedServer server) Parameters server SavedServer"
},
"api/client/EchoHub.Client.Config.NotificationConfig.html": {
"href": "api/client/EchoHub.Client.Config.NotificationConfig.html",
@@ -47,7 +47,7 @@
"api/client/EchoHub.Client.Config.SavedServer.html": {
"href": "api/client/EchoHub.Client.Config.SavedServer.html",
"title": "Class SavedServer | EchoHub Documentation",
- "summary": "Class SavedServer Namespace EchoHub.Client.Config Assembly EchoHub.Client.dll public class SavedServer Inheritance object SavedServer Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors SavedServer() public SavedServer() Properties ChannelKeys Cached room content keys for end-to-end encrypted channels on this server, keyed by channel name (base64). Like RefreshToken, these live only on the user's machine — the server never sees them. public Dictionary ChannelKeys { get; set; } Property Value Dictionary LastConnected public DateTimeOffset LastConnected { get; set; } Property Value DateTimeOffset LastReadMessages Last message the user has read per channel (message id as string), persisted so unread counts, @mention highlights, and the \"new messages\" marker survive restarts. public Dictionary LastReadMessages { get; set; } Property Value Dictionary LeftChannels Channels the user explicitly left with /leave. Excluded from the automatic join-all-channels pass at connect until the user joins them again. public List LeftChannels { get; set; } Property Value List Name public required string Name { get; set; } Property Value string RefreshToken public string? RefreshToken { get; set; } Property Value string RememberMe public bool RememberMe { get; set; } Property Value bool Url public required string Url { get; set; } Property Value string Username public string? Username { get; set; } Property Value string"
+ "summary": "Class SavedServer Namespace EchoHub.Client.Config Assembly EchoHub.Client.dll public class SavedServer Inheritance object SavedServer Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors SavedServer() public SavedServer() Properties ChannelKeys Cached room content keys for end-to-end encrypted channels on this server, keyed by channel name and encrypted at rest (see RoomKeyProtector; legacy entries were plain base64). Like RefreshToken, these live only on the user's machine — the server never sees them. public Dictionary ChannelKeys { get; set; } Property Value Dictionary LastConnected public DateTimeOffset LastConnected { get; set; } Property Value DateTimeOffset LastReadMessages Last message the user has read per channel (message id as string), persisted so unread counts, @mention highlights, and the \"new messages\" marker survive restarts. public Dictionary LastReadMessages { get; set; } Property Value Dictionary LeftChannels Channels the user explicitly left with /leave. Excluded from the automatic join-all-channels pass at connect until the user joins them again. public List LeftChannels { get; set; } Property Value List Name public required string Name { get; set; } Property Value string RefreshToken public string? RefreshToken { get; set; } Property Value string RememberMe public bool RememberMe { get; set; } Property Value bool Url public required string Url { get; set; } Property Value string Username public string? Username { get; set; } Property Value string"
},
"api/client/EchoHub.Client.Config.html": {
"href": "api/client/EchoHub.Client.Config.html",
@@ -89,6 +89,11 @@
"title": "Class ClipboardFiles | EchoHub Documentation",
"summary": "Class ClipboardFiles Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Reads file paths that live on the OS clipboard as a file list (e.g. after copying a file in Explorer/Finder/Nautilus), which terminals do not paste as text. Lets Ctrl+V attach a copied file directly instead of requiring the user to paste a raw path. public static class ClipboardFiles Inheritance object ClipboardFiles Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Methods TryGetFiles(out List) public static bool TryGetFiles(out List files) Parameters files List Returns bool"
},
+ "api/client/EchoHub.Client.Services.ClipboardImage.html": {
+ "href": "api/client/EchoHub.Client.Services.ClipboardImage.html",
+ "title": "Class ClipboardImage | EchoHub Documentation",
+ "summary": "Class ClipboardImage Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Reads raw image data from the OS clipboard (e.g. an image copied from a browser, or a Win+Shift+S screenshot), which terminals cannot paste as text. Always returns PNG bytes: clipboard PNG data is passed through, clipboard bitmaps (CF_DIB) are re-encoded. public static class ClipboardImage Inheritance object ClipboardImage Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Methods DibToPng(byte[]) Converts clipboard DIB bytes (a BITMAPINFOHEADER/V4/V5 + optional palette/masks + pixel data, i.e. a .bmp file without its 14-byte file header) to PNG. Returns null when the data is malformed or not decodable as a bitmap. public static byte[]? DibToPng(byte[] dib) Parameters dib byte[] Returns byte[] TryGetPng(out byte[]) public static bool TryGetPng(out byte[] png) Parameters png byte[] Returns bool"
+ },
"api/client/EchoHub.Client.Services.EchoHubConnection.html": {
"href": "api/client/EchoHub.Client.Services.EchoHubConnection.html",
"title": "Class EchoHubConnection | EchoHub Documentation",
@@ -129,10 +134,20 @@
"title": "Enum PickerOutcome | EchoHub Documentation",
"summary": "Enum PickerOutcome Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll public enum PickerOutcome Fields Cancelled = 1 The native dialog ran but the user cancelled it. Chosen = 0 The user picked a folder (Path is set). Unavailable = 2 No native picker is available on this machine (headless, missing tool, etc.)."
},
+ "api/client/EchoHub.Client.Services.RoomKeyProtector.html": {
+ "href": "api/client/EchoHub.Client.Services.RoomKeyProtector.html",
+ "title": "Class RoomKeyProtector | EchoHub Documentation",
+ "summary": "Class RoomKeyProtector Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix \"dp1:\"). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with 0600 permissions (prefix \"k1:\") — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form. public sealed class RoomKeyProtector Inheritance object RoomKeyProtector Inherited Members object.GetType() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors RoomKeyProtector(string, bool?) public RoomKeyProtector(string keyDirectory, bool? useDpapi = null) Parameters keyDirectory string Directory holding the master key file (the client config dir). useDpapi bool? Overrides the platform default (DPAPI on Windows) — for tests. Fields DpapiPrefix public const string DpapiPrefix = \"dp1:\" Field Value string KeyFilePrefix public const string KeyFilePrefix = \"k1:\" Field Value string Methods Protect(byte[]) Encrypts a room key for storage in the config file. public string Protect(byte[] roomKey) Parameters roomKey byte[] Returns string TryUnprotect(string, out byte[], out bool) Decrypts a stored value back into a room key. wasLegacy is true when the value was an unencrypted legacy entry that should be re-persisted via Protect(byte[]). Returns false for unreadable values (wrong user/machine, missing or regenerated key file, malformed data) — the caller drops the entry and the user can recover it by re-entering the passphrase. public bool TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy) Parameters stored string roomKey byte[] wasLegacy bool Returns bool"
+ },
"api/client/EchoHub.Client.Services.RoomKeyStore.html": {
"href": "api/client/EchoHub.Client.Services.RoomKeyStore.html",
"title": "Class RoomKeyStore | EchoHub Documentation",
- "summary": "Class RoomKeyStore Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Holds room content keys for end-to-end encrypted channels: in-memory for the active session, persisted per-server in the client config (like saved sessions) so users don't retype the passphrase every launch. Keys never leave this machine. public sealed class RoomKeyStore Inheritance object RoomKeyStore Inherited Members object.GetType() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors RoomKeyStore() public RoomKeyStore() Methods Clear() public void Clear() HasKey(string) public bool HasKey(string channelName) Parameters channelName string Returns bool LoadForServer(string) Binds the store to a server and loads that server's cached keys from config. public void LoadForServer(string serverUrl) Parameters serverUrl string RemoveKey(string) public void RemoveKey(string channelName) Parameters channelName string StoreKey(string, byte[]) Stores a key for the session and persists it to the server's config entry. public void StoreKey(string channelName, byte[] key) Parameters channelName string key byte[] TryGetKey(string, out byte[]) public bool TryGetKey(string channelName, out byte[] key) Parameters channelName string key byte[] Returns bool"
+ "summary": "Class RoomKeyStore Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Holds room content keys for end-to-end encrypted channels: in-memory for the active session, persisted per-server in the client config (like saved sessions) so users don't retype the passphrase every launch. Keys never leave this machine and are encrypted at rest by RoomKeyProtector. Also tracks which channels are known to be end-to-end encrypted, so senders can refuse to emit plaintext into a room whose key isn't cached yet. public sealed class RoomKeyStore Inheritance object RoomKeyStore Inherited Members object.GetType() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors RoomKeyStore() public RoomKeyStore() RoomKeyStore(RoomKeyProtector) public RoomKeyStore(RoomKeyProtector protector) Parameters protector RoomKeyProtector Methods Clear() public void Clear() HasKey(string) public bool HasKey(string channelName) Parameters channelName string Returns bool IsChannelEncrypted(string) public bool IsChannelEncrypted(string channelName) Parameters channelName string Returns bool LoadForServer(string) Binds the store to a server and loads that server's cached keys from config. public void LoadForServer(string serverUrl) Parameters serverUrl string MarkChannelEncrypted(string, bool) Records whether a channel is end-to-end encrypted (from channel listings, crypto metadata, or join outcomes). Senders consult this to block plaintext into rooms whose key isn't cached. public void MarkChannelEncrypted(string channelName, bool isEncrypted) Parameters channelName string isEncrypted bool RemoveKey(string) public void RemoveKey(string channelName) Parameters channelName string StoreKey(string, byte[]) Stores a key for the session and persists it to the server's config entry. public void StoreKey(string channelName, byte[] key) Parameters channelName string key byte[] TryGetKey(string, out byte[]) public bool TryGetKey(string channelName, out byte[] key) Parameters channelName string key byte[] Returns bool TryStoreFromEnvelope(string, string, byte[]) Unwraps a fresh key envelope and caches the key, overwriting any stale cached key (e.g. the channel was deleted and recreated under the same name, so the old key would encrypt messages nobody else can read). Returns false when the KEK doesn't open the envelope — the cache is left untouched. public bool TryStoreFromEnvelope(string channelName, string wrappedRoomKey, byte[] kek) Parameters channelName string wrappedRoomKey string kek byte[] Returns bool"
+ },
+ "api/client/EchoHub.Client.Services.RoomLockedException.html": {
+ "href": "api/client/EchoHub.Client.Services.RoomLockedException.html",
+ "title": "Class RoomLockedException | EchoHub Documentation",
+ "summary": "Class RoomLockedException Namespace EchoHub.Client.Services Assembly EchoHub.Client.dll Thrown when sending into an end-to-end encrypted channel whose room key isn't cached: without the key the message would leave the client as plaintext, which must never happen. public sealed class RoomLockedException : Exception Inheritance object Exception RoomLockedException Inherited Members Exception.GetBaseException() Exception.ToString() Exception.GetType() Exception.TargetSite Exception.Message Exception.Data Exception.InnerException Exception.HelpLink Exception.Source Exception.HResult Exception.StackTrace object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors RoomLockedException(string) public RoomLockedException(string channelName) Parameters channelName string Properties ChannelName public string ChannelName { get; } Property Value string"
},
"api/client/EchoHub.Client.Services.UpdateBackupService.html": {
"href": "api/client/EchoHub.Client.Services.UpdateBackupService.html",
@@ -147,7 +162,7 @@
"api/client/EchoHub.Client.Services.html": {
"href": "api/client/EchoHub.Client.Services.html",
"title": "Namespace EchoHub.Client.Services | EchoHub Documentation",
- "summary": "Namespace EchoHub.Client.Services Classes ApiClient AsyncRunner Eliminates repeated Task.Run/try/catch/app.Invoke(ShowError) boilerplate. Runs async work on a background thread and routes exceptions to the UI. AudioPlaybackService BackupInfo ChannelPasswordRequiredException Thrown when joining a channel fails because a password is required or incorrect. The UI catches this to prompt the user and retry. ClientEncryptionService Client-side encryption service. Uses the same AES-256-GCM format as the server so messages are encrypted end-to-end between client and server. ClipboardFiles Reads file paths that live on the OS clipboard as a file list (e.g. after copying a file in Explorer/Finder/Nautilus), which terminals do not paste as text. Lets Ctrl+V attach a copied file directly instead of requiring the user to paste a raw path. EchoHubConnection FolderPickResult JoinOutcome Result of joining a channel: decrypted history plus, for end-to-end encrypted channels, the key envelope needed to unlock the room content key. NativeFolderPicker Opens the OS-native folder chooser (Windows Explorer, macOS Finder, Linux GTK/KDE) by shelling out, so the TUI doesn't need a GUI toolkit reference. Returns Unavailable when no native dialog can run, so callers can fall back to a configured path. NotificationSoundService OutgoingAttachment One file to upload as part of a message. For end-to-end encrypted channels the stream is already ciphertext, DeclaredKind is set (image/audio/file), and EncryptedPreview holds the room-encrypted ASCII art for images. For normal channels only Stream and FileName are set. PathSetup Ensures the application's directory is on the system PATH so users can run 'echohub' from any terminal session. RoomKeyStore Holds room content keys for end-to-end encrypted channels: in-memory for the active session, persisted per-server in the client config (like saved sessions) so users don't retype the passphrase every launch. Keys never leave this machine. UpdateBackupService Manages pre-update backups and rollback restoration for the auto-updater. Backup location: ~/.echohub/update-backup/ UpdateChecker Enums PickerOutcome"
+ "summary": "Namespace EchoHub.Client.Services Classes ApiClient AsyncRunner Eliminates repeated Task.Run/try/catch/app.Invoke(ShowError) boilerplate. Runs async work on a background thread and routes exceptions to the UI. AudioPlaybackService BackupInfo ChannelPasswordRequiredException Thrown when joining a channel fails because a password is required or incorrect. The UI catches this to prompt the user and retry. ClientEncryptionService Client-side encryption service. Uses the same AES-256-GCM format as the server so messages are encrypted end-to-end between client and server. ClipboardFiles Reads file paths that live on the OS clipboard as a file list (e.g. after copying a file in Explorer/Finder/Nautilus), which terminals do not paste as text. Lets Ctrl+V attach a copied file directly instead of requiring the user to paste a raw path. ClipboardImage Reads raw image data from the OS clipboard (e.g. an image copied from a browser, or a Win+Shift+S screenshot), which terminals cannot paste as text. Always returns PNG bytes: clipboard PNG data is passed through, clipboard bitmaps (CF_DIB) are re-encoded. EchoHubConnection FolderPickResult JoinOutcome Result of joining a channel: decrypted history plus, for end-to-end encrypted channels, the key envelope needed to unlock the room content key. NativeFolderPicker Opens the OS-native folder chooser (Windows Explorer, macOS Finder, Linux GTK/KDE) by shelling out, so the TUI doesn't need a GUI toolkit reference. Returns Unavailable when no native dialog can run, so callers can fall back to a configured path. NotificationSoundService OutgoingAttachment One file to upload as part of a message. For end-to-end encrypted channels the stream is already ciphertext, DeclaredKind is set (image/audio/file), and EncryptedPreview holds the room-encrypted ASCII art for images. For normal channels only Stream and FileName are set. PathSetup Ensures the application's directory is on the system PATH so users can run 'echohub' from any terminal session. RoomKeyProtector Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix \"dp1:\"). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with 0600 permissions (prefix \"k1:\") — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form. RoomKeyStore Holds room content keys for end-to-end encrypted channels: in-memory for the active session, persisted per-server in the client config (like saved sessions) so users don't retype the passphrase every launch. Keys never leave this machine and are encrypted at rest by RoomKeyProtector. Also tracks which channels are known to be end-to-end encrypted, so senders can refuse to emit plaintext into a room whose key isn't cached yet. RoomLockedException Thrown when sending into an end-to-end encrypted channel whose room key isn't cached: without the key the message would leave the client as plaintext, which must never happen. UpdateBackupService Manages pre-update backups and rollback restoration for the auto-updater. Backup location: ~/.echohub/update-backup/ UpdateChecker Enums PickerOutcome"
},
"api/client/EchoHub.Client.Themes.Theme.html": {
"href": "api/client/EchoHub.Client.Themes.Theme.html",
@@ -312,7 +327,7 @@
"api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html": {
"href": "api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html",
"title": "Class ChannelListSource | EchoHub Documentation",
- "summary": "Class ChannelListSource Namespace EchoHub.Client.UI.ListSources Assembly EchoHub.Client.dll Custom list data source for colored channel list rendering. Active channel gets a > indicator, unread channels are bright with a count badge. public class ChannelListSource : IListDataSource, IDisposable Inheritance object ChannelListSource Implements IListDataSource IDisposable Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors ChannelListSource() public ChannelListSource() Properties Count public int Count { get; } Property Value int MaxItemLength public int MaxItemLength { get; } Property Value int SuspendCollectionChangedEvent public bool SuspendCollectionChangedEvent { get; set; } Property Value bool Methods Dispose() public void Dispose() IsMarked(int) public bool IsMarked(int item) Parameters item int Returns bool Render(ListView, bool, int, int, int, int, int) public void Render(ListView listView, bool selected, int item, int col, int row, int width, int viewportX = 0) Parameters listView ListView selected bool item int col int row int width int viewportX int SetMark(int, bool) public void SetMark(int item, bool value) Parameters item int value bool ToList() public IList ToList() Returns IList Update(List, Dictionary, string, IReadOnlySet?, IReadOnlySet?) public void Update(List channels, Dictionary unread, string activeChannel, IReadOnlySet? protectedChannels = null, IReadOnlySet? mentionChannels = null) Parameters channels List unread Dictionary activeChannel string protectedChannels IReadOnlySet mentionChannels IReadOnlySet Events CollectionChanged public event NotifyCollectionChangedEventHandler? CollectionChanged Event Type NotifyCollectionChangedEventHandler"
+ "summary": "Class ChannelListSource Namespace EchoHub.Client.UI.ListSources Assembly EchoHub.Client.dll Custom list data source for colored channel list rendering. Active channel gets a > indicator, unread channels are bright with a count badge. public class ChannelListSource : IListDataSource, IDisposable Inheritance object ChannelListSource Implements IListDataSource IDisposable Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors ChannelListSource() public ChannelListSource() Properties Count public int Count { get; } Property Value int MaxItemLength public int MaxItemLength { get; } Property Value int SuspendCollectionChangedEvent public bool SuspendCollectionChangedEvent { get; set; } Property Value bool Methods Dispose() public void Dispose() IsMarked(int) public bool IsMarked(int item) Parameters item int Returns bool Render(ListView, bool, int, int, int, int, int) public void Render(ListView listView, bool selected, int item, int col, int row, int width, int viewportX = 0) Parameters listView ListView selected bool item int col int row int width int viewportX int SetMark(int, bool) public void SetMark(int item, bool value) Parameters item int value bool ToList() public IList ToList() Returns IList Update(List, Dictionary, string, IReadOnlySet?, IReadOnlySet?, IReadOnlySet?) public void Update(List channels, Dictionary unread, string activeChannel, IReadOnlySet? protectedChannels = null, IReadOnlySet? mentionChannels = null, IReadOnlySet? privateChannels = null) Parameters channels List unread Dictionary activeChannel string protectedChannels IReadOnlySet mentionChannels IReadOnlySet privateChannels IReadOnlySet Events CollectionChanged public event NotifyCollectionChangedEventHandler? CollectionChanged Event Type NotifyCollectionChangedEventHandler"
},
"api/client/EchoHub.Client.UI.ListSources.SearchListSource.html": {
"href": "api/client/EchoHub.Client.UI.ListSources.SearchListSource.html",
@@ -332,7 +347,7 @@
"api/client/EchoHub.Client.UI.MainWindow.html": {
"href": "api/client/EchoHub.Client.UI.MainWindow.html",
"title": "Class MainWindow | EchoHub Documentation",
- "summary": "Class MainWindow Namespace EchoHub.Client.UI Assembly EchoHub.Client.dll Main Terminal.Gui window for the EchoHub chat client. public sealed class MainWindow : Runnable, IDisposable, IRunnable Inheritance object View Runnable MainWindow Implements IDisposable IRunnable Inherited Members Runnable.SetApp(IApplication) Runnable.SetIsRunning(bool) Runnable.RequestStop() Runnable.RaiseIsRunningChanging(bool, bool) Runnable.RaiseIsRunningChangedEvent(bool) Runnable.SetIsModal(bool) Runnable.RaiseIsModalChangedEvent(bool) Runnable.Result Runnable.IsRunning Runnable.IsModal Runnable.StopRequested Runnable.IsRunningChanging Runnable.IsRunningChanged Runnable.IsModalChanged View.NewMouseEvent(Mouse) View.RaiseMouseEvent(Mouse) View.GetAdornmentsThickness() View.GetSupportedCommands() View.InvokeCommands(Command[], ICommandBinding) View.InvokeCommand(Command, ICommandBinding) View.InvokeCommand(Command, ICommandContext) View.InvokeCommand(Command) View.SetContentSize(Size?) View.GetContentSize() View.GetWidthRequiredForSubViews() View.GetHeightRequiredForSubViews() View.ContentToScreen(in Point) View.ScreenToContent(in Point) View.ViewportToScreen(in Rectangle) View.ViewportToScreen(in Point) View.ViewportToScreen() View.ScreenToViewport(in Point) View.GetViewportOffsetFromFrame() View.ScrollVertical(int) View.ScrollHorizontal(int) View.Dispose() View.ToString() View.ToDebugString() View.BeginInit() View.EndInit() View.OnEnabledChanged() View.SetCursorNeedsUpdate() View.GetCurrentAttribute() View.GetAttributeForRole(VisualRole) View.SetAttribute(Attribute) View.SetAttributeForRole(VisualRole) View.GetClip() View.SetClip(Region) View.SetClipToScreen() View.ExcludeFromClip(Rectangle) View.ExcludeFromClip(Region) View.AddViewportToClip() View.Draw(DrawContext) View.DrawAdornments() View.ClearViewport(DrawContext) View.DrawText(DrawContext) View.DrawSubViews(DrawContext) View.RenderLineCanvas(DrawContext) View.Move(int, int) View.AddRune(Rune) View.AddRune(char) View.AddRune(int, int, Rune) View.AddStr(string) View.AddStr(int, int, string) View.DrawHotString(string, Attribute, Attribute) View.DrawHotString(string, bool) View.FillRect(Rectangle, Color?) View.FillRect(Rectangle, Rune) View.GetScheme() View.SetScheme(Scheme) View.GetSubViews(bool, bool, bool) View.Add(View) View.Add(params View[]) View.Remove(View) View.RemoveAll() View.RemoveAll() View.GetTopSuperView(View, View) View.IsInHierarchy(View, View, bool) View.MoveSubViewTowardsEnd(View) View.MoveSubViewToEnd(View) View.MoveSubViewTowardsStart(View) View.MoveSubViewToStart(View) View.AddKeyBindingsForHotKey(Key, Key, object) View.AssignHotKeysToSubViews() View.NewKeyDownEvent(Key) View.Contains(in Point) View.FrameToScreen() View.ScreenToFrame(in Point) View.Layout(Size) View.Layout() View.SetRelativeLayout(Size) View.SetNeedsLayout() View.GetContainerSize() View.GetViewsUnderLocation(in Point, ViewportSettingsFlags) View.AdvanceFocus(NavigationDirection, TabBehavior?) View.FocusDeepest(NavigationDirection, TabBehavior?) View.OnCanFocusChanged() View.ClearFocus() View.SetFocus() View.SetNeedsDraw() View.SetNeedsDraw(Rectangle) View.OnTextChanged() View.MouseBindings View.MouseHoldRepeat View.MousePositionTracking View.MouseState View.MouseHighlightStates View.Margin View.ShadowStyle View.Border View.BorderStyle View.Padding View.Arrangement View.DefaultAcceptView View.CommandsToBubbleUp View.ContentSizeTracksViewport View.ViewportSettings View.Viewport View.Data View.Id View.App View.IsInitialized View.Enabled View.Visible View.Title View.Cursor View.Diagnostics View.LineCanvas View.SuperViewRendersLineCanvas View.SchemeName View.HasScheme View.SubViews View.SuperView View.HotKey View.HotKeySpecifier View.AssignHotKeys View.UsedHotKeys View.KeyBindings View.HotKeyBindings View.Frame View.X View.Y View.Height View.Width View.NeedsLayout View.ValidatePosDim View.CanFocus View.Focused View.IsCurrentTop View.MostFocused View.HasFocus View.TabStop View.NeedsDraw View.HorizontalScrollBar View.VerticalScrollBar View.PreserveTrailingSpaces View.Text View.TextAlignment View.TextDirection View.TextFormatter View.VerticalTextAlignment View.MouseEnter View.MouseLeave View.MouseHoldRepeatChanging View.MouseHoldRepeatChanged View.MouseEvent View.MouseStateChanged View.BorderStyleChanged View.CommandNotBound View.Accepting View.Accepted View.Activating View.Activated View.HandlingHotKey View.HotKeyCommand View.ContentSizeChanging View.ContentSizeChanged View.ViewportChanged View.Disposing View.Initialized View.EnabledChanged View.VisibleChanging View.VisibleChanged View.TitleChanging View.TitleChanged View.GettingAttributeForRole View.ClearingViewport View.ClearedViewport View.DrawingText View.DrewText View.DrawingContent View.DrawingSubViews View.DrawComplete View.SchemeNameChanging View.SchemeNameChanged View.GettingScheme View.SchemeChanging View.SchemeChanged View.SuperViewChanging View.SuperViewChanged View.SubViewAdded View.SubViewRemoved View.Removed View.HotKeyChanged View.KeyDown View.KeyDownNotHandled View.FrameChanged View.HeightChanging View.HeightChanged View.WidthChanging View.WidthChanged View.SubViewLayout View.SubViewsLaidOut View.AdvancingFocus View.CanFocusChanged View.FocusedChanged View.HasFocusChanging View.HasFocusChanged View.TextChanged object.GetType() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors MainWindow(IApplication, ChatMessageManager) public MainWindow(IApplication app, ChatMessageManager messageManager) Parameters app IApplication messageManager ChatMessageManager Properties CurrentChannel Get the current channel name. public string CurrentChannel { get; } Property Value string Methods ApplyColorSchemes() Applies the currently registered color schemes to all views. Call after theme changes to refresh colors. public void ApplyColorSchemes() ClearAll() Clear all messages and channels (used on disconnect). public void ClearAll() EnsureChannelInList(string, bool?, bool?) Ensure a channel exists in the left panel list (used for private channels joined via /join). public void EnsureChannelInList(string channelName, bool? isPublic = null, bool? isProtected = null) Parameters channelName string isPublic bool? isProtected bool? FocusInput() Focus the input field for typing. public void FocusInput() GetChannelNames() Get all channel names that have message buffers (for broadcasting status changes). public IReadOnlyList GetChannelNames() Returns IReadOnlyList RefreshMenuBar() Rebuilds and replaces the menu bar (e.g., after theme list changes). public void RefreshMenuBar() RemoveChannel(string) Remove a channel from the left panel list. public void RemoveChannel(string channelName) Parameters channelName string SetChannelTopic(string, string?) Update the topic for a specific channel. public void SetChannelTopic(string channelName, string? topic) Parameters channelName string topic string SetChannels(List) Set the list of available channels, storing topics, and refresh the channel list view. public void SetChannels(List channels) Parameters channels List SetCurrentUser(string) Set the current user name (delegates to message manager for @mention detection). public void SetCurrentUser(string username) Parameters username string SetStagedAttachments(IReadOnlyList, string) Updates the attachment staging indicator shown on the input frame's title, including the current ASCII-art size for images. Passing an empty list restores the default hint. public void SetStagedAttachments(IReadOnlyList fileNames, string asciiSizeLabel) Parameters fileNames IReadOnlyList asciiSizeLabel string ShowError(string) Show an error message to the user. public void ShowError(string message) Parameters message string SwitchToChannel(string) Switch the chat view to the given channel, resetting its unread count and updating the topic bar. public void SwitchToChannel(string channelName) Parameters channelName string ToggleUsersPanel() Toggle the online users panel visibility (F2). public void ToggleUsersPanel() UpdateOnlineUsers(List) Update the online users list display. public void UpdateOnlineUsers(List users) Parameters users List UpdateStatusBar(string) Update the connection status displayed in the status bar. public void UpdateStatusBar(string status) Parameters status string Events OnAudioPlayRequested Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName. public event Action? OnAudioPlayRequested Event Type Action OnChannelJoinRequested Fired when the user activates a #channel reference in a message. Parameter is the channel name. public event Action? OnChannelJoinRequested Event Type Action OnChannelSelected Fired when the user selects a channel. Parameter is the channel name. public event Action? OnChannelSelected Event Type Action OnCheckForUpdatesRequested Fired when the user requests to check for updates. public event Action? OnCheckForUpdatesRequested Event Type Action OnConnectRequested Fired when the user requests to connect via the menu. public event Action? OnConnectRequested Event Type Action OnCreateChannelRequested Fired when the user requests to create a new channel. public event Action? OnCreateChannelRequested Event Type Action OnDeleteChannelRequested Fired when the user requests to delete the current channel. public event Action? OnDeleteChannelRequested Event Type Action OnDeleteMessageRequested Fired when the user presses Delete on the selected message. Parameter is the message id. public event Action? OnDeleteMessageRequested Event Type Action OnDisconnectRequested Fired when the user requests to disconnect via the menu. public event Action? OnDisconnectRequested Event Type Action OnFileDownloadRequested Fired when the user activates (Enter/click) a file message. Parameters: attachmentUrl, fileName. public event Action? OnFileDownloadRequested Event Type Action OnImageSaveRequested Fired when the user activates an image's \"[save original]\" line. Parameters: attachmentUrl, fileName. public event Action? OnImageSaveRequested Event Type Action OnLoadMoreRequested Fired when the user scrolls to the top of the message list and older messages should be loaded. public event Action? OnLoadMoreRequested Event Type Action OnLogoutRequested Fired when the user requests to logout (disconnect + revoke session). public event Action? OnLogoutRequested Event Type Action OnMessageSubmitted Fired when the user presses Enter in the input field. Parameters: channel name, message content. public event Action? OnMessageSubmitted Event Type Action OnProfileRequested Fired when the user requests to open their profile panel. public event Action? OnProfileRequested Event Type Action OnRollbackRequested Fired when the user requests to rollback to the previous version. public event Action? OnRollbackRequested Event Type Action OnSavedServersRequested Fired when the user requests to view saved servers. public event Action? OnSavedServersRequested Event Type Action OnSearchRequested Fired when the user requests to open the search dialog (via menu or Ctrl+K). public event Action? OnSearchRequested Event Type Action OnStatusRequested Fired when the user requests to set their status. public event Action? OnStatusRequested Event Type Action OnThemeSelected Fired when the user selects a theme from the menu. Parameter is the theme name. public event Action? OnThemeSelected Event Type Action OnUserProfileRequested Fired when the user activates a username (in userlist or message). Parameter is the username. public event Action? OnUserProfileRequested Event Type Action"
+ "summary": "Class MainWindow Namespace EchoHub.Client.UI Assembly EchoHub.Client.dll Main Terminal.Gui window for the EchoHub chat client. public sealed class MainWindow : Runnable, IDisposable, IRunnable Inheritance object View Runnable MainWindow Implements IDisposable IRunnable Inherited Members Runnable.SetApp(IApplication) Runnable.SetIsRunning(bool) Runnable.RequestStop() Runnable.RaiseIsRunningChanging(bool, bool) Runnable.RaiseIsRunningChangedEvent(bool) Runnable.SetIsModal(bool) Runnable.RaiseIsModalChangedEvent(bool) Runnable.Result Runnable.IsRunning Runnable.IsModal Runnable.StopRequested Runnable.IsRunningChanging Runnable.IsRunningChanged Runnable.IsModalChanged View.NewMouseEvent(Mouse) View.RaiseMouseEvent(Mouse) View.GetAdornmentsThickness() View.GetSupportedCommands() View.InvokeCommands(Command[], ICommandBinding) View.InvokeCommand(Command, ICommandBinding) View.InvokeCommand(Command, ICommandContext) View.InvokeCommand(Command) View.SetContentSize(Size?) View.GetContentSize() View.GetWidthRequiredForSubViews() View.GetHeightRequiredForSubViews() View.ContentToScreen(in Point) View.ScreenToContent(in Point) View.ViewportToScreen(in Rectangle) View.ViewportToScreen(in Point) View.ViewportToScreen() View.ScreenToViewport(in Point) View.GetViewportOffsetFromFrame() View.ScrollVertical(int) View.ScrollHorizontal(int) View.Dispose() View.ToString() View.ToDebugString() View.BeginInit() View.EndInit() View.OnEnabledChanged() View.SetCursorNeedsUpdate() View.GetCurrentAttribute() View.GetAttributeForRole(VisualRole) View.SetAttribute(Attribute) View.SetAttributeForRole(VisualRole) View.GetClip() View.SetClip(Region) View.SetClipToScreen() View.ExcludeFromClip(Rectangle) View.ExcludeFromClip(Region) View.AddViewportToClip() View.Draw(DrawContext) View.DrawAdornments() View.ClearViewport(DrawContext) View.DrawText(DrawContext) View.DrawSubViews(DrawContext) View.RenderLineCanvas(DrawContext) View.Move(int, int) View.AddRune(Rune) View.AddRune(char) View.AddRune(int, int, Rune) View.AddStr(string) View.AddStr(int, int, string) View.DrawHotString(string, Attribute, Attribute) View.DrawHotString(string, bool) View.FillRect(Rectangle, Color?) View.FillRect(Rectangle, Rune) View.GetScheme() View.SetScheme(Scheme) View.GetSubViews(bool, bool, bool) View.Add(View) View.Add(params View[]) View.Remove(View) View.RemoveAll() View.RemoveAll() View.GetTopSuperView(View, View) View.IsInHierarchy(View, View, bool) View.MoveSubViewTowardsEnd(View) View.MoveSubViewToEnd(View) View.MoveSubViewTowardsStart(View) View.MoveSubViewToStart(View) View.AddKeyBindingsForHotKey(Key, Key, object) View.AssignHotKeysToSubViews() View.NewKeyDownEvent(Key) View.Contains(in Point) View.FrameToScreen() View.ScreenToFrame(in Point) View.Layout(Size) View.Layout() View.SetRelativeLayout(Size) View.SetNeedsLayout() View.GetContainerSize() View.GetViewsUnderLocation(in Point, ViewportSettingsFlags) View.AdvanceFocus(NavigationDirection, TabBehavior?) View.FocusDeepest(NavigationDirection, TabBehavior?) View.OnCanFocusChanged() View.ClearFocus() View.SetFocus() View.SetNeedsDraw() View.SetNeedsDraw(Rectangle) View.OnTextChanged() View.MouseBindings View.MouseHoldRepeat View.MousePositionTracking View.MouseState View.MouseHighlightStates View.Margin View.ShadowStyle View.Border View.BorderStyle View.Padding View.Arrangement View.DefaultAcceptView View.CommandsToBubbleUp View.ContentSizeTracksViewport View.ViewportSettings View.Viewport View.Data View.Id View.App View.IsInitialized View.Enabled View.Visible View.Title View.Cursor View.Diagnostics View.LineCanvas View.SuperViewRendersLineCanvas View.SchemeName View.HasScheme View.SubViews View.SuperView View.HotKey View.HotKeySpecifier View.AssignHotKeys View.UsedHotKeys View.KeyBindings View.HotKeyBindings View.Frame View.X View.Y View.Height View.Width View.NeedsLayout View.ValidatePosDim View.CanFocus View.Focused View.IsCurrentTop View.MostFocused View.HasFocus View.TabStop View.NeedsDraw View.HorizontalScrollBar View.VerticalScrollBar View.PreserveTrailingSpaces View.Text View.TextAlignment View.TextDirection View.TextFormatter View.VerticalTextAlignment View.MouseEnter View.MouseLeave View.MouseHoldRepeatChanging View.MouseHoldRepeatChanged View.MouseEvent View.MouseStateChanged View.BorderStyleChanged View.CommandNotBound View.Accepting View.Accepted View.Activating View.Activated View.HandlingHotKey View.HotKeyCommand View.ContentSizeChanging View.ContentSizeChanged View.ViewportChanged View.Disposing View.Initialized View.EnabledChanged View.VisibleChanging View.VisibleChanged View.TitleChanging View.TitleChanged View.GettingAttributeForRole View.ClearingViewport View.ClearedViewport View.DrawingText View.DrewText View.DrawingContent View.DrawingSubViews View.DrawComplete View.SchemeNameChanging View.SchemeNameChanged View.GettingScheme View.SchemeChanging View.SchemeChanged View.SuperViewChanging View.SuperViewChanged View.SubViewAdded View.SubViewRemoved View.Removed View.HotKeyChanged View.KeyDown View.KeyDownNotHandled View.FrameChanged View.HeightChanging View.HeightChanged View.WidthChanging View.WidthChanged View.SubViewLayout View.SubViewsLaidOut View.AdvancingFocus View.CanFocusChanged View.FocusedChanged View.HasFocusChanging View.HasFocusChanged View.TextChanged object.GetType() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors MainWindow(IApplication, ChatMessageManager) public MainWindow(IApplication app, ChatMessageManager messageManager) Parameters app IApplication messageManager ChatMessageManager Properties CurrentChannel Get the current channel name. public string CurrentChannel { get; } Property Value string Methods ApplyColorSchemes() Applies the currently registered color schemes to all views. Call after theme changes to refresh colors. public void ApplyColorSchemes() ClearAll() Clear all messages and channels (used on disconnect). public void ClearAll() EnsureChannelInList(string, bool?, bool?) Ensure a channel exists in the left panel list (used for private channels joined via /join). public void EnsureChannelInList(string channelName, bool? isPublic = null, bool? isProtected = null) Parameters channelName string isPublic bool? isProtected bool? FocusInput() Focus the input field for typing. public void FocusInput() GetChannelNames() Get all channel names that have message buffers (for broadcasting status changes). public IReadOnlyList GetChannelNames() Returns IReadOnlyList RefreshMenuBar() Rebuilds and replaces the menu bar (e.g., after theme list changes). public void RefreshMenuBar() RemoveChannel(string) Remove a channel from the left panel list. public void RemoveChannel(string channelName) Parameters channelName string SetChannelTopic(string, string?) Update the topic for a specific channel. public void SetChannelTopic(string channelName, string? topic) Parameters channelName string topic string SetChannels(List) Set the list of available channels, storing topics, and refresh the channel list view. public void SetChannels(List channels) Parameters channels List SetCurrentUser(string) Set the current user name (delegates to message manager for @mention detection). public void SetCurrentUser(string username) Parameters username string SetStagedAttachments(IReadOnlyList, string) Updates the attachment staging indicator shown on the input frame's title, including the current ASCII-art size for images. Passing an empty list restores the default hint. public void SetStagedAttachments(IReadOnlyList fileNames, string asciiSizeLabel) Parameters fileNames IReadOnlyList asciiSizeLabel string ShowError(string) Show an error message to the user. public void ShowError(string message) Parameters message string SwitchToChannel(string) Switch the chat view to the given channel, resetting its unread count and updating the topic bar. public void SwitchToChannel(string channelName) Parameters channelName string ToggleUsersPanel() Toggle the online users panel visibility (F2). public void ToggleUsersPanel() UpdateOnlineUsers(List) Update the online users list display. public void UpdateOnlineUsers(List users) Parameters users List UpdateStatusBar(string) Update the connection status displayed in the status bar. public void UpdateStatusBar(string status) Parameters status string Events OnAudioPlayRequested Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName. public event Action? OnAudioPlayRequested Event Type Action OnChannelJoinRequested Fired when the user activates a #channel reference in a message. Parameter is the channel name. public event Action? OnChannelJoinRequested Event Type Action OnChannelSelected Fired when the user selects a channel. Parameter is the channel name. public event Action? OnChannelSelected Event Type Action OnCheckForUpdatesRequested Fired when the user requests to check for updates. public event Action? OnCheckForUpdatesRequested Event Type Action OnConnectRequested Fired when the user requests to connect via the menu. public event Action? OnConnectRequested Event Type Action OnCreateChannelRequested Fired when the user requests to create a new channel. public event Action? OnCreateChannelRequested Event Type Action OnDeleteChannelRequested Fired when the user requests to delete the current channel. public event Action? OnDeleteChannelRequested Event Type Action OnDeleteMessageRequested Fired when the user presses Delete on the selected message. Parameter is the message id. public event Action? OnDeleteMessageRequested Event Type Action OnDisconnectRequested Fired when the user requests to disconnect via the menu. public event Action? OnDisconnectRequested Event Type Action OnFileDownloadRequested Fired when the user activates (Enter/click) a file message. Parameters: attachmentUrl, fileName. public event Action? OnFileDownloadRequested Event Type Action OnFilesStaged Fired when local files arrive via paste or drag-and-drop to be staged as attachments. Parameters: channel name, absolute paths of existing files. public event Action>? OnFilesStaged Event Type Action> OnImagePasted Fired when raw image data is pasted from the clipboard (e.g. copied from a browser or a screenshot tool). Parameters: channel name, PNG-encoded image bytes. public event Action? OnImagePasted Event Type Action OnImageSaveRequested Fired when the user activates an image's \"[save original]\" line. Parameters: attachmentUrl, fileName. public event Action? OnImageSaveRequested Event Type Action OnLoadMoreRequested Fired when the user scrolls to the top of the message list and older messages should be loaded. public event Action? OnLoadMoreRequested Event Type Action OnLogoutRequested Fired when the user requests to logout (disconnect + revoke session). public event Action? OnLogoutRequested Event Type Action OnMessageSubmitted Fired when the user presses Enter in the input field. Parameters: channel name, message content. public event Action? OnMessageSubmitted Event Type Action OnProfileRequested Fired when the user requests to open their profile panel. public event Action? OnProfileRequested Event Type Action OnRollbackRequested Fired when the user requests to rollback to the previous version. public event Action? OnRollbackRequested Event Type Action OnSavedServersRequested Fired when the user requests to view saved servers. public event Action? OnSavedServersRequested Event Type Action OnSearchRequested Fired when the user requests to open the search dialog (via menu or Ctrl+K). public event Action? OnSearchRequested Event Type Action OnStatusRequested Fired when the user requests to set their status. public event Action? OnStatusRequested Event Type Action OnThemeSelected Fired when the user selects a theme from the menu. Parameter is the theme name. public event Action? OnThemeSelected Event Type Action OnUserProfileRequested Fired when the user activates a username (in userlist or message). Parameter is the username. public event Action? OnUserProfileRequested Event Type Action"
},
"api/client/EchoHub.Client.UI.html": {
"href": "api/client/EchoHub.Client.UI.html",
@@ -352,7 +367,7 @@
"api/core/EchoHub.Core.Constants.HubConstants.html": {
"href": "api/core/EchoHub.Core.Constants.HubConstants.html",
"title": "Class HubConstants | EchoHub Documentation",
- "summary": "Class HubConstants Namespace EchoHub.Core.Constants Assembly EchoHub.Core.dll public static class HubConstants Inheritance object HubConstants Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Fields AsciiArtHeight public const int AsciiArtHeight = 40 Field Value int AsciiArtHeightHalfBlock public const int AsciiArtHeightHalfBlock = 80 Field Value int AsciiArtWidth public const int AsciiArtWidth = 80 Field Value int ChatHubPath public const string ChatHubPath = \"/hubs/chat\" Field Value string DefaultChannel public const string DefaultChannel = \"general\" Field Value string DefaultHistoryCount public const int DefaultHistoryCount = 100 Field Value int EmbedFetchTimeoutSeconds public const int EmbedFetchTimeoutSeconds = 5 Field Value int EmbedMaxDescriptionLength public const int EmbedMaxDescriptionLength = 500 Field Value int EmbedMaxHtmlBytes public const int EmbedMaxHtmlBytes = 65536 Field Value int EmbedMaxUrlsPerMessage public const int EmbedMaxUrlsPerMessage = 3 Field Value int MaxAttachmentsPerMessage public const int MaxAttachmentsPerMessage = 10 Field Value int MaxAudioFileSizeBytes public const int MaxAudioFileSizeBytes = 10485760 Field Value int MaxAvatarSizeBytes public const int MaxAvatarSizeBytes = 2097152 Field Value int MaxConsecutiveNewlines public const int MaxConsecutiveNewlines = 1 Field Value int MaxFileSizeBytes public const int MaxFileSizeBytes = 104857600 Field Value int MaxImageSizeBytes public const int MaxImageSizeBytes = 10485760 Field Value int MaxMessageLength public const int MaxMessageLength = 2000 Field Value int MaxMessageNewlines public const int MaxMessageNewlines = 30 Field Value int"
+ "summary": "Class HubConstants Namespace EchoHub.Core.Constants Assembly EchoHub.Core.dll public static class HubConstants Inheritance object HubConstants Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Fields AsciiArtHeight public const int AsciiArtHeight = 40 Field Value int AsciiArtHeightHalfBlock public const int AsciiArtHeightHalfBlock = 80 Field Value int AsciiArtWidth public const int AsciiArtWidth = 80 Field Value int ChatHubPath public const string ChatHubPath = \"/hubs/chat\" Field Value string DefaultChannel public const string DefaultChannel = \"general\" Field Value string DefaultHistoryCount public const int DefaultHistoryCount = 100 Field Value int EmbedFetchTimeoutSeconds public const int EmbedFetchTimeoutSeconds = 5 Field Value int EmbedMaxDescriptionLength public const int EmbedMaxDescriptionLength = 500 Field Value int EmbedMaxHtmlBytes public const int EmbedMaxHtmlBytes = 65536 Field Value int EmbedMaxUrlsPerMessage public const int EmbedMaxUrlsPerMessage = 3 Field Value int IrcConnectionIdPrefix Connection-id prefix for IRC gateway connections. The presence tracker uses it to tell IRC-only users apart from native (SignalR) clients. public const string IrcConnectionIdPrefix = \"irc-\" Field Value string MaxAttachmentsPerMessage public const int MaxAttachmentsPerMessage = 10 Field Value int MaxAudioFileSizeBytes public const int MaxAudioFileSizeBytes = 10485760 Field Value int MaxAvatarSizeBytes public const int MaxAvatarSizeBytes = 2097152 Field Value int MaxConsecutiveNewlines public const int MaxConsecutiveNewlines = 1 Field Value int MaxFileSizeBytes public const int MaxFileSizeBytes = 104857600 Field Value int MaxImageSizeBytes public const int MaxImageSizeBytes = 10485760 Field Value int MaxMessageLength public const int MaxMessageLength = 2000 Field Value int MaxMessageNewlines public const int MaxMessageNewlines = 30 Field Value int"
},
"api/core/EchoHub.Core.Constants.ValidationConstants.html": {
"href": "api/core/EchoHub.Core.Constants.ValidationConstants.html",
@@ -502,7 +517,7 @@
"api/core/EchoHub.Core.DTOs.MessageDto.html": {
"href": "api/core/EchoHub.Core.DTOs.MessageDto.html",
"title": "Class MessageDto | EchoHub Documentation",
- "summary": "Class MessageDto Namespace EchoHub.Core.DTOs Assembly EchoHub.Core.dll public record MessageDto : IEquatable Inheritance object MessageDto Implements IEquatable Inherited Members object.GetType() object.MemberwiseClone() object.Equals(object, object) object.ReferenceEquals(object, object) Constructors MessageDto(MessageDto) protected MessageDto(MessageDto original) Parameters original MessageDto MessageDto(Guid, string, string, string?, string, DateTimeOffset, List?, List?) public MessageDto(Guid Id, string Content, string SenderUsername, string? SenderNicknameColor, string ChannelName, DateTimeOffset SentAt, List? Attachments = null, List? Embeds = null) Parameters Id Guid Content string SenderUsername string SenderNicknameColor string ChannelName string SentAt DateTimeOffset Attachments List Embeds List Properties Attachments public List? Attachments { get; init; } Property Value List ChannelName public string ChannelName { get; init; } Property Value string Content public string Content { get; init; } Property Value string Embeds public List? Embeds { get; init; } Property Value List EqualityContract protected virtual Type EqualityContract { get; } Property Value Type Id public Guid Id { get; init; } Property Value Guid SenderNicknameColor public string? SenderNicknameColor { get; init; } Property Value string SenderUsername public string SenderUsername { get; init; } Property Value string SentAt public DateTimeOffset SentAt { get; init; } Property Value DateTimeOffset Methods Deconstruct(out Guid, out string, out string, out string?, out string, out DateTimeOffset, out List?, out List?) public void Deconstruct(out Guid Id, out string Content, out string SenderUsername, out string? SenderNicknameColor, out string ChannelName, out DateTimeOffset SentAt, out List? Attachments, out List? Embeds) Parameters Id Guid Content string SenderUsername string SenderNicknameColor string ChannelName string SentAt DateTimeOffset Attachments List Embeds List Equals(MessageDto?) public virtual bool Equals(MessageDto? other) Parameters other MessageDto Returns bool Equals(object?) public override bool Equals(object? obj) Parameters obj object Returns bool GetHashCode() public override int GetHashCode() Returns int PrintMembers(StringBuilder) protected virtual bool PrintMembers(StringBuilder builder) Parameters builder StringBuilder Returns bool ToString() public override string ToString() Returns string Operators operator ==(MessageDto?, MessageDto?) public static bool operator ==(MessageDto? left, MessageDto? right) Parameters left MessageDto right MessageDto Returns bool operator !=(MessageDto?, MessageDto?) public static bool operator !=(MessageDto? left, MessageDto? right) Parameters left MessageDto right MessageDto Returns bool"
+ "summary": "Class MessageDto Namespace EchoHub.Core.DTOs Assembly EchoHub.Core.dll public record MessageDto : IEquatable Inheritance object MessageDto Implements IEquatable Inherited Members object.GetType() object.MemberwiseClone() object.Equals(object, object) object.ReferenceEquals(object, object) Constructors MessageDto(MessageDto) protected MessageDto(MessageDto original) Parameters original MessageDto MessageDto(Guid, string, string, string?, string, DateTimeOffset, List?, List?, string?) public MessageDto(Guid Id, string Content, string SenderUsername, string? SenderNicknameColor, string ChannelName, DateTimeOffset SentAt, List? Attachments = null, List? Embeds = null, string? SenderDisplayName = null) Parameters Id Guid Content string SenderUsername string SenderNicknameColor string ChannelName string SentAt DateTimeOffset Attachments List Embeds List SenderDisplayName string Properties Attachments public List? Attachments { get; init; } Property Value List ChannelName public string ChannelName { get; init; } Property Value string Content public string Content { get; init; } Property Value string Embeds public List? Embeds { get; init; } Property Value List EqualityContract protected virtual Type EqualityContract { get; } Property Value Type Id public Guid Id { get; init; } Property Value Guid SenderDisplayName public string? SenderDisplayName { get; init; } Property Value string SenderNicknameColor public string? SenderNicknameColor { get; init; } Property Value string SenderUsername public string SenderUsername { get; init; } Property Value string SentAt public DateTimeOffset SentAt { get; init; } Property Value DateTimeOffset Methods Deconstruct(out Guid, out string, out string, out string?, out string, out DateTimeOffset, out List?, out List?, out string?) public void Deconstruct(out Guid Id, out string Content, out string SenderUsername, out string? SenderNicknameColor, out string ChannelName, out DateTimeOffset SentAt, out List? Attachments, out List? Embeds, out string? SenderDisplayName) Parameters Id Guid Content string SenderUsername string SenderNicknameColor string ChannelName string SentAt DateTimeOffset Attachments List Embeds List SenderDisplayName string Equals(MessageDto?) public virtual bool Equals(MessageDto? other) Parameters other MessageDto Returns bool Equals(object?) public override bool Equals(object? obj) Parameters obj object Returns bool GetHashCode() public override int GetHashCode() Returns int PrintMembers(StringBuilder) protected virtual bool PrintMembers(StringBuilder builder) Parameters builder StringBuilder Returns bool ToString() public override string ToString() Returns string Operators operator ==(MessageDto?, MessageDto?) public static bool operator ==(MessageDto? left, MessageDto? right) Parameters left MessageDto right MessageDto Returns bool operator !=(MessageDto?, MessageDto?) public static bool operator !=(MessageDto? left, MessageDto? right) Parameters left MessageDto right MessageDto Returns bool"
},
"api/core/EchoHub.Core.DTOs.MuteRequest.html": {
"href": "api/core/EchoHub.Core.DTOs.MuteRequest.html",
@@ -577,7 +592,7 @@
"api/core/EchoHub.Core.DTOs.UserPresenceDto.html": {
"href": "api/core/EchoHub.Core.DTOs.UserPresenceDto.html",
"title": "Class UserPresenceDto | EchoHub Documentation",
- "summary": "Class UserPresenceDto Namespace EchoHub.Core.DTOs Assembly EchoHub.Core.dll public record UserPresenceDto : IEquatable Inheritance object UserPresenceDto Implements IEquatable Inherited Members object.GetType() object.MemberwiseClone() object.Equals(object, object) object.ReferenceEquals(object, object) Constructors UserPresenceDto(UserPresenceDto) protected UserPresenceDto(UserPresenceDto original) Parameters original UserPresenceDto UserPresenceDto(string, string?, string?, UserStatus, string?, ServerRole) public UserPresenceDto(string Username, string? DisplayName, string? NicknameColor, UserStatus Status, string? StatusMessage, ServerRole Role) Parameters Username string DisplayName string NicknameColor string Status UserStatus StatusMessage string Role ServerRole Properties DisplayName public string? DisplayName { get; init; } Property Value string EqualityContract protected virtual Type EqualityContract { get; } Property Value Type NicknameColor public string? NicknameColor { get; init; } Property Value string Role public ServerRole Role { get; init; } Property Value ServerRole Status public UserStatus Status { get; init; } Property Value UserStatus StatusMessage public string? StatusMessage { get; init; } Property Value string Username public string Username { get; init; } Property Value string Methods Deconstruct(out string, out string?, out string?, out UserStatus, out string?, out ServerRole) public void Deconstruct(out string Username, out string? DisplayName, out string? NicknameColor, out UserStatus Status, out string? StatusMessage, out ServerRole Role) Parameters Username string DisplayName string NicknameColor string Status UserStatus StatusMessage string Role ServerRole Equals(UserPresenceDto?) public virtual bool Equals(UserPresenceDto? other) Parameters other UserPresenceDto Returns bool Equals(object?) public override bool Equals(object? obj) Parameters obj object Returns bool GetHashCode() public override int GetHashCode() Returns int PrintMembers(StringBuilder) protected virtual bool PrintMembers(StringBuilder builder) Parameters builder StringBuilder Returns bool ToString() public override string ToString() Returns string Operators operator ==(UserPresenceDto?, UserPresenceDto?) public static bool operator ==(UserPresenceDto? left, UserPresenceDto? right) Parameters left UserPresenceDto right UserPresenceDto Returns bool operator !=(UserPresenceDto?, UserPresenceDto?) public static bool operator !=(UserPresenceDto? left, UserPresenceDto? right) Parameters left UserPresenceDto right UserPresenceDto Returns bool"
+ "summary": "Class UserPresenceDto Namespace EchoHub.Core.DTOs Assembly EchoHub.Core.dll public record UserPresenceDto : IEquatable Inheritance object UserPresenceDto Implements IEquatable Inherited Members object.GetType() object.MemberwiseClone() object.Equals(object, object) object.ReferenceEquals(object, object) Constructors UserPresenceDto(UserPresenceDto) protected UserPresenceDto(UserPresenceDto original) Parameters original UserPresenceDto UserPresenceDto(string, string?, string?, UserStatus, string?, ServerRole, bool) public UserPresenceDto(string Username, string? DisplayName, string? NicknameColor, UserStatus Status, string? StatusMessage, ServerRole Role, bool IsIrc = false) Parameters Username string DisplayName string NicknameColor string Status UserStatus StatusMessage string Role ServerRole IsIrc bool Properties DisplayName public string? DisplayName { get; init; } Property Value string EqualityContract protected virtual Type EqualityContract { get; } Property Value Type IsIrc public bool IsIrc { get; init; } Property Value bool NicknameColor public string? NicknameColor { get; init; } Property Value string Role public ServerRole Role { get; init; } Property Value ServerRole Status public UserStatus Status { get; init; } Property Value UserStatus StatusMessage public string? StatusMessage { get; init; } Property Value string Username public string Username { get; init; } Property Value string Methods Deconstruct(out string, out string?, out string?, out UserStatus, out string?, out ServerRole, out bool) public void Deconstruct(out string Username, out string? DisplayName, out string? NicknameColor, out UserStatus Status, out string? StatusMessage, out ServerRole Role, out bool IsIrc) Parameters Username string DisplayName string NicknameColor string Status UserStatus StatusMessage string Role ServerRole IsIrc bool Equals(UserPresenceDto?) public virtual bool Equals(UserPresenceDto? other) Parameters other UserPresenceDto Returns bool Equals(object?) public override bool Equals(object? obj) Parameters obj object Returns bool GetHashCode() public override int GetHashCode() Returns int PrintMembers(StringBuilder) protected virtual bool PrintMembers(StringBuilder builder) Parameters builder StringBuilder Returns bool ToString() public override string ToString() Returns string Operators operator ==(UserPresenceDto?, UserPresenceDto?) public static bool operator ==(UserPresenceDto? left, UserPresenceDto? right) Parameters left UserPresenceDto right UserPresenceDto Returns bool operator !=(UserPresenceDto?, UserPresenceDto?) public static bool operator !=(UserPresenceDto? left, UserPresenceDto? right) Parameters left UserPresenceDto right UserPresenceDto Returns bool"
},
"api/core/EchoHub.Core.DTOs.UserProfileDto.html": {
"href": "api/core/EchoHub.Core.DTOs.UserProfileDto.html",
@@ -907,7 +922,7 @@
"api/server/EchoHub.Server.Services.PresenceTracker.html": {
"href": "api/server/EchoHub.Server.Services.PresenceTracker.html",
"title": "Class PresenceTracker | EchoHub Documentation",
- "summary": "Class PresenceTracker Namespace EchoHub.Server.Services Assembly EchoHub.Server.dll public class PresenceTracker Inheritance object PresenceTracker Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors PresenceTracker() public PresenceTracker() Methods ForceRemoveUser(string) Forcibly remove a user from all tracking. Returns their connection IDs and channels so the caller can broadcast departures and force-disconnect connections. public (List ConnectionIds, List Channels) ForceRemoveUser(string username) Parameters username string Returns (List ConnectionIds, List Channels) GetChannelsForUser(string) public List GetChannelsForUser(string username) Parameters username string Returns List GetConnectionsInChannels(List) Get all unique connection IDs for users who share any of the given channels. public List GetConnectionsInChannels(List channels) Parameters channels List Returns List GetOnlineUserCount() public int GetOnlineUserCount() Returns int GetOnlineUsersInChannel(string) public List GetOnlineUsersInChannel(string channelName) Parameters channelName string Returns List GetUsernameForConnection(string) public string? GetUsernameForConnection(string connectionId) Parameters connectionId string Returns string IsOnline(string) public bool IsOnline(string username) Parameters username string Returns bool JoinChannel(string, string) Returns true if this is a new join, false if the user was already in the channel. public bool JoinChannel(string username, string channelName) Parameters username string channelName string Returns bool LeaveChannel(string, string) public void LeaveChannel(string username, string channelName) Parameters username string channelName string UserConnected(string, Guid, string) public void UserConnected(string connectionId, Guid userId, string username) Parameters connectionId string userId Guid username string UserDisconnected(string) public string? UserDisconnected(string connectionId) Parameters connectionId string Returns string Events UserCountChanged Raised when the distinct online user count changes (multi-connection users only fire once). public event Action? UserCountChanged Event Type Action"
+ "summary": "Class PresenceTracker Namespace EchoHub.Server.Services Assembly EchoHub.Server.dll public class PresenceTracker Inheritance object PresenceTracker Inherited Members object.GetType() object.MemberwiseClone() object.ToString() object.Equals(object) object.Equals(object, object) object.ReferenceEquals(object, object) object.GetHashCode() Constructors PresenceTracker() public PresenceTracker() Methods ForceRemoveUser(string) Forcibly remove a user from all tracking. Returns their connection IDs and channels so the caller can broadcast departures and force-disconnect connections. public (List ConnectionIds, List Channels) ForceRemoveUser(string username) Parameters username string Returns (List ConnectionIds, List Channels) GetChannelsForUser(string) public List GetChannelsForUser(string username) Parameters username string Returns List GetConnectionsInChannels(List) Get all unique connection IDs for users who share any of the given channels. public List GetConnectionsInChannels(List channels) Parameters channels List Returns List GetOnlineUserCount() public int GetOnlineUserCount() Returns int GetOnlineUsersInChannel(string) public List GetOnlineUsersInChannel(string channelName) Parameters channelName string Returns List GetUsernameForConnection(string) public string? GetUsernameForConnection(string connectionId) Parameters connectionId string Returns string IsIrcOnly(string) True when the user is online exclusively through the IRC gateway. A user who also has a native client connected has full features, so they don't count as IRC-only. public bool IsIrcOnly(string username) Parameters username string Returns bool IsOnline(string) public bool IsOnline(string username) Parameters username string Returns bool JoinChannel(string, string) Returns true if this is a new join, false if the user was already in the channel. public bool JoinChannel(string username, string channelName) Parameters username string channelName string Returns bool LeaveChannel(string, string) public void LeaveChannel(string username, string channelName) Parameters username string channelName string UserConnected(string, Guid, string) public void UserConnected(string connectionId, Guid userId, string username) Parameters connectionId string userId Guid username string UserDisconnected(string) public string? UserDisconnected(string connectionId) Parameters connectionId string Returns string Events UserCountChanged Raised when the distinct online user count changes (multi-connection users only fire once). public event Action? UserCountChanged Event Type Action"
},
"api/server/EchoHub.Server.Services.RegistrationStatus.html": {
"href": "api/server/EchoHub.Server.Services.RegistrationStatus.html",
@@ -962,7 +977,7 @@
"articles/configuration.html": {
"href": "articles/configuration.html",
"title": "Configuration | EchoHub Documentation",
- "summary": "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: 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): { \"Server\": { \"Name\": \"My EchoHub Server\", \"PublicServer\": true } } Environment variables (Docker, systemd, shell export): export Server__Name=\"My EchoHub Server\" export Server__PublicServer=true Command-line arguments (quick overrides, highest priority): ./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. 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 Server:PublicHost (empty) Public hostname for the directory listing (e.g. chat.example.com:5000) Server:Admins [] Array of admin usernames (e.g. [\"alice\", \"bob\"]) 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: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 Logging EchoHub uses Serilog 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: # via environment variable export Serilog__MinimumLevel__Default=Debug # or command line ./EchoHub.Server --Serilog:MinimumLevel:Default=Debug"
+ "summary": "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: 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): { \"Server\": { \"Name\": \"My EchoHub Server\", \"PublicServer\": true } } Environment variables (Docker, systemd, shell export): export Server__Name=\"My EchoHub Server\" export Server__PublicServer=true Command-line arguments (quick overrides, highest priority): ./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. 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 Server:PublicHost (empty) Public hostname for the directory listing (e.g. chat.example.com:5000) Server:Admins [] Array of admin usernames (e.g. [\"alice\", \"bob\"]) 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 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. 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: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 Logging EchoHub uses Serilog 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: # via environment variable export Serilog__MinimumLevel__Default=Debug # or command line ./EchoHub.Server --Serilog:MinimumLevel:Default=Debug"
},
"articles/docker.html": {
"href": "articles/docker.html",
@@ -982,17 +997,37 @@
"articles/getting-started.html": {
"href": "articles/getting-started.html",
"title": "Getting Started | EchoHub Documentation",
- "summary": "Getting Started Install the Client Windows (Chocolatey) choco install echohub Linux / macOS curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh To install a specific version or to a custom directory: curl -sSfL .../install.sh | sh -s -- --version 0.2.11 curl -sSfL .../install.sh | sh -s -- --install-dir /opt/echohub Manual Download Grab a self-contained binary from Releases -- no runtime needed. Host a Server Docker The quickest way to host a server: cp .env.example .env docker compose up -d See the Docker guide for configuration, pre-built images, and more. From Source dotnet run --project src/EchoHub.Server Requires .NET 10 SDK. On first run, the server automatically: Creates appsettings.json from the example config Generates a secure JWT secret Creates the SQLite database with a #general channel 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 Enable the IRC gateway in the server's appsettings.json: { \"Irc\": { \"Enabled\": true, \"Port\": 6667 } } Then connect with any standard IRC client: irssi -c localhost -p 6667 -w -n 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. For TLS, set TlsEnabled: true, TlsPort: 6697, and provide a PKCS#12 certificate path. See the Architecture page for details on how the IRC gateway integrates with the chat service. Configuration Server configuration is in appsettings.json (auto-generated on first run). You can also use environment variables or command-line arguments to override settings. See the Configuration guide for the full reference and how it all works. Build from Source dotnet build src/EchoHub.slnx"
+ "summary": "Getting Started Install the Client Windows (Chocolatey) choco install echohub Linux / macOS curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh To install a specific version or to a custom directory: 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 -- no runtime needed. Host a Server Docker The quickest way to host a server: cp .env.example .env docker compose up -d See the Docker guide for configuration, pre-built images, and more. From Source dotnet run --project src/EchoHub.Server Requires .NET 10 SDK. On first run, the server automatically: Creates appsettings.json from the example config Generates a secure JWT secret Creates the SQLite database with a #general channel 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 Enable the IRC gateway in the server's appsettings.json: { \"Irc\": { \"Enabled\": true, \"Port\": 6667 } } Then connect with any standard IRC client: irssi -c localhost -p 6667 -w -n 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 IRC Gateway guide for command mapping, attachment rendering, and limitations, or Architecture for how the gateway integrates with the chat service. Configuration Server configuration is in appsettings.json (auto-generated on first run). You can also use environment variables or command-line arguments to override settings. See the Configuration guide for the full reference and how it all works. Build from Source dotnet build src/EchoHub.slnx"
+ },
+ "articles/irc-gateway.html": {
+ "href": "articles/irc-gateway.html",
+ "title": "IRC Gateway | EchoHub Documentation",
+ "summary": "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). Enabling the gateway The gateway is off by default. Enable it in appsettings.json (or Irc__Enabled=true as an environment variable): { \"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 for every option. Connecting & authentication Your IRC nick is your EchoHub username and your server password is your account password. Two flows are supported: # classic PASS/NICK/USER — most clients call this the \"server password\" irssi -c chat.example.com -p 6667 -w -n 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: 3–50 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 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 / -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 NOTICEs. 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 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."
+ },
+ "articles/messages-and-attachments.html": {
+ "href": "articles/messages-and-attachments.html",
+ "title": "Messages & Attachments | EchoHub Documentation",
+ "summary": "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 (brew install pngpaste). Drag & drop — drop a file onto the terminal window; the client recognizes the dropped path and stages the file. /send — 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. ┌ Message (2 attached: report.pdf, image.png) ──────────────┐ │ here's the summary and a screenshot_ │ └────────────────────────────────────────────────────────────┘ URL sends are different: /send 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 (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 every attachment is encrypted client-side before upload: 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."
+ },
+ "articles/moderation.html": {
+ "href": "articles/moderation.html",
+ "title": "Moderation & Roles | EchoHub Documentation",
+ "summary": "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 . 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 [reason] Mod Disconnects the user. Not persistent — they can reconnect immediately. /ban [reason] Admin Persistent: flags the account banned and disconnects it. Banned accounts are rejected at login. /unban Admin Lifts a ban. /mute [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 Mod Lifts a mute early. /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.) 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 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."
},
"articles/notification-sounds.html": {
"href": "articles/notification-sounds.html",
"title": "Notification Sounds | EchoHub Documentation",
"summary": "Notification Sounds EchoHub can play a notification sound when someone @mentions you. This is disabled by default and must be enabled in your profile settings. Enabling Notifications Open your profile (/profile) and check the \"Notification sound on @mention\" checkbox, then save. You can also adjust the Volume (0-100, default 30). All settings are persisted in ~/.echohub/config.json. Customizing the Sound The client ships with a default Notification.mp3 in the Assets folder. To use your own notification sound, replace the file at: /Assets/Notification.mp3 The file must be a valid .mp3 or .wav audio file. The replacement takes effect on the next app launch. Alternatively, set a custom path in ~/.echohub/config.json: { \"notifications\": { \"enabled\": true, \"volume\": 30, \"soundFile\": \"/path/to/your/sound.mp3\" } } When soundFile is set, EchoHub uses that file instead of the bundled default. Disabling Notifications Uncheck the option in your profile, or edit the config directly: { \"notifications\": { \"enabled\": false } }"
},
+ "articles/tui-guide.html": {
+ "href": "articles/tui-guide.html",
+ "title": "TUI Guide | EchoHub Documentation",
+ "summary": "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 ┌ Menu bar ──────────────────────────────────────────────────┐ │ ┌ Channels ─┐ ┌ Messages ────────────────────┐ ┌ Users ──┐ │ │ │ #general 3│ │ 12:01 hi │ │ ★ alice │ │ │ │ #dev │ │ ── new messages ── │ │ ❀ bob │ │ │ │ #random*~ │ │ 12:04 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) 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) 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 or /status Presence / status message /nick , /color <#hex>, /avatar Display name, nick color, avatar /theme Switch theme /send, /clear, /size, /downloadpath Attachments — see Messages & Attachments /join [password], /leave, /topic Channel membership and topic /passwd Rotate an encrypted room's passphrase /profile [user], /users, /meta Profiles, online users, room info /kick, /ban, /mute, /role, /nuke, … Moderation /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 . 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 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."
+ },
"changelog/index.html": {
"href": "changelog/index.html",
"title": "Changelog | EchoHub Documentation",
- "summary": "Changelog Release history for EchoHub. Releases v0.2.13 - Chat Visual Overhaul, Auto-Join All Channels & Persistent Read Positions v0.2.12 - End-to-End Encrypted Channels, IRC Channel Keys, Image Save & Ctrl+W Crash Fix v0.2.11 - EchoHubSpace Auth, Live Directory Updates & Server Browser Metadata v0.2.10 - Command Palette, Infinite History Scroll & Auto-Updater Fixes v0.2.9 - Install Script & Chocolatey Fixes v0.2.8 - Docker Support, IRC Account Creation & BOM Fix v0.2.7 - User List Fix & Terminal.Gui NuGet Migration v0.2.6 - Major Refactoring & Code Organization v0.2.5 - Session Persistence, Auto-Updates, Audio & Transparent Theme v0.2.4 - E2E Message Encryption v0.2.3 - Moderation, Embeds & UI Overhaul v0.2.2 - Startup & Shutdown Fixes v0.2.1 - Shutdown & CI Fixes v0.2.0 - IRC Gateway v0.1.1 - Directory Connection Self-Healing v0.1.0 - Initial Release"
+ "summary": "Changelog Release history for EchoHub. Releases v0.2.14 - Clipboard Image & Multi-File Paste, E2E Room Unlock Fixes, Encrypted Key Cache & IRC Gateway Polish v0.2.13 - Chat Visual Overhaul, Auto-Join All Channels & Persistent Read Positions v0.2.12 - End-to-End Encrypted Channels, IRC Channel Keys, Image Save & Ctrl+W Crash Fix v0.2.11 - EchoHubSpace Auth, Live Directory Updates & Server Browser Metadata v0.2.10 - Command Palette, Infinite History Scroll & Auto-Updater Fixes v0.2.9 - Install Script & Chocolatey Fixes v0.2.8 - Docker Support, IRC Account Creation & BOM Fix v0.2.7 - User List Fix & Terminal.Gui NuGet Migration v0.2.6 - Major Refactoring & Code Organization v0.2.5 - Session Persistence, Auto-Updates, Audio & Transparent Theme v0.2.4 - E2E Message Encryption v0.2.3 - Moderation, Embeds & UI Overhaul v0.2.2 - Startup & Shutdown Fixes v0.2.1 - Shutdown & CI Fixes v0.2.0 - IRC Gateway v0.1.1 - Directory Connection Self-Healing v0.1.0 - Initial Release"
},
"changelog/v0.1.0.html": {
"href": "changelog/v0.1.0.html",
@@ -1034,6 +1069,11 @@
"title": "v0.2.13 | EchoHub Documentation",
"summary": "v0.2.13 A visual overhaul of the chat client — WeeChat-style column layout, per-user nick colors, date rules, unread markers, an activity status segment, and an ASCII welcome banner — plus reliable cross-channel notifications: the client now joins all your channels at connect so unread counts and @mentions light up everywhere, and read positions persist across restarts so activity that happened while you were offline still shows. Modern polish, old IRC soul. New Features Aligned message layout (WeeChat-style) — every message renders as HH:mm nickname │ text: a dim timestamp, the nick right-aligned in a fixed 12-column gutter, and a dim │ rail separating names from content. Word-wrapped continuation lines, multi-line messages, attachments, ASCII-art image previews, and link embeds all align under the text column and extend the rail, so conversations read as a clean two-column grid. Over-long nicks are truncated with … in the gutter only — profiles, mentions, and the users panel always use the full name. System and status lines (joined, is now Away, …) show -- in the nick column, replacing the old ** prefix. Deterministic nickname colors — users who haven't picked a /color now get an automatic, stable color derived from their nick (12-entry palette chosen to stay readable on dark and light backgrounds). The same user is the same color in message headers and the online-users panel, on every client, every session. An explicitly chosen nickname color still takes priority. Date separator rules — a full-width ── Wed, Jul 16 2026 ───── rule is inserted whenever the calendar day changes (live, in loaded history, and when scrolling back through older messages). Timestamps are now a compact HH:mm — the date lives in the rules instead of being repeated on every line. \"New messages\" marker — the first message that arrives in a channel you're not viewing gets an orange ── new messages ── rule above it (irssi-style). Switch to the channel and the marker shows exactly where you left off; it survives the history reload on channel switch and is consumed when you move away again. Status bar activity segment — channels with unread messages appear in the status bar as │ Act: #dev,#random (up to 4, +n overflow). Channels where you were @mentioned show in orange, others in cyan. Connecting spinner — transitional connection states (Connecting, Reconnecting, Authenticating…) now animate a braille spinner in the status bar instead of sitting on static text. Mention-aware channel list — a channel where you were @mentioned turns orange in the channel list (name and unread badge), escalating above the cyan plain-unread highlight. The highlight clears when you view the channel. Welcome banner — with no channel selected (fresh start, or after disconnecting) the chat pane shows a gold-gradient ASCII \"ECHOHUB\" logo with the version and key hints, instead of an empty box. Narrow panes get a compact variant. Rounded frame borders — the channels, chat, input, and users panels draw with rounded corners. Auto-join all channels at connect — the client now joins every channel it lists for you (public channels and prior memberships) as part of connecting, so message events flow for all of them: unread badges, @mention highlights, and the status-bar activity segment work without having to open each channel first. Protected channels you've never entered are skipped silently (joining them stays a prompted, manual action), and channels you /leave are remembered and excluded until you join them again. Read positions survive restarts — the client persists the last message you read per channel (locally, per server). On the next connect it compares that against fetched history, so messages that arrived while you were offline still produce unread counts, mention highlights, and a correctly placed \"new messages\" marker. Theme-tinted frame borders — themes can now color the window frame borders independently of text via a new optional border section (hex values supported). The Transparent and TransparentLight themes use it to draw dim gray borders instead of stark white ones, for a subtler, glassier look. Custom theme JSON files without a border section keep their base colors. Bug Fixes The @mention highlight no longer drops off the second and later lines of a long mention message — wrapped continuation lines now inherit the highlight. The unread-position marker is re-anchored after a channel switch reloads history, instead of being wiped by the reload before you could see it. Cached room keys for end-to-end encrypted channels no longer disappear from the config on reconnect. Saving the server entry after a successful connect replaced it wholesale, wiping the cached keys (and forcing a passphrase re-entry on the next launch); the entry is now updated in place. A failed channel rejoin after a reconnect (e.g. a channel deleted while you were away) no longer aborts rejoining the remaining channels. Client config file access is now serialized across threads (token refresh, room-key cache, and read-position checkpoints all write it), preventing rare config corruption."
},
+ "changelog/v0.2.14.html": {
+ "href": "changelog/v0.2.14.html",
+ "title": "v0.2.14 | EchoHub Documentation",
+ "summary": "v0.2.14 A reliability and security pass over end-to-end encrypted rooms: locked channels now offer the passphrase prompt instead of dead-ending on the \"rejoin to unlock\" placeholder, the client can no longer be tricked into sending plaintext into an encrypted room, and cached room keys are encrypted at rest instead of sitting in the config as base64. Ctrl+V grows up too — images copied from a browser or screenshot tool paste straight into the chat as attachments, and copying several files pastes them all into one message. Plus a set of IRC gateway fixes — decrypted image previews, display-name plumbing, and user/channel indicators. New Features Paste images straight from the clipboard — copy an image from a browser, a screenshot tool (Win+Shift+S), or an image editor and Ctrl+V it into the input: it's attached as a PNG (image.png), no saving to disk first, Discord-style. Transparency is preserved when the source provides PNG data; plain clipboard bitmaps are converted automatically. On Linux this uses wl-paste/xclip; on macOS it requires pngpaste. In end-to-end encrypted rooms pasted images go through the same client-side encryption as any other attachment. Multi-file paste — copying several files in your file manager and pasting attaches them all to a single message (up to the 10-attachment cap), staged as one batch alongside anything you type as the caption. Previously each pasted file was routed through its own /send, which could misbehave on large batches. Room keys encrypted at rest — the per-channel room keys cached so you don't retype a passphrase every launch are no longer stored as plain base64 in config.json. On Windows they're protected with DPAPI (current-user scope); on Linux/macOS with AES-GCM under a per-user key file created with 0600 permissions next to the config. Existing plain entries migrate to the encrypted format automatically on first load. The passphrase itself is never stored in any form. [irc] tag in the users panel — users online only through the IRC gateway are tagged [irc], useful context since IRC clients lack encryption, attachments, and profiles. Someone also running the TUI shows untagged. ~ marker for private channels — the channel list now marks private (unlisted) channels with a trailing ~, alongside the existing * for password-protected ones (#room*~ when both apply). Bug Fixes Locked encrypted channels now prompt for the passphrase. Auto-joining your channels at connect silently entered end-to-end encrypted rooms you're a member of without running the unlock flow — on a new device (or after a cancelled prompt) the room showed only [encrypted — rejoin this channel with its passphrase to unlock] placeholders, and only a manual /join recovered it. Selecting the channel now offers the passphrase prompt; entering it unlocks history and live messages in place. Cancelling is remembered for the session so reselecting the channel doesn't nag — /join or trying to send always re-offers the prompt. A stale cached room key no longer beats a fresh one. If an encrypted channel was deleted and recreated under the same name, a client that still had the old key cached kept encrypting messages nobody else could read. Typing the passphrase on join now always adopts the key from the server's current envelope, replacing the stale cache. IRC clients no longer get flooded with ciphertext for image messages. The gateway forwarded image ASCII previews without stripping transport encryption, spamming IRC clients with one enormous $ENC$v1$… line per image. Previews are now decrypted before formatting, and any that still can't be read (e.g. end-to-end room ciphertext the server cannot decrypt) are skipped in favor of the plain [Image: name] url line. Display names now show on chat messages. Messages only carried the sender's username, so a configured display name appeared in the user list but not on the messages themselves. Live messages, history, and attachment messages now all carry it; mention and profile lookups stay keyed to the username. Security No plaintext can leak into an encrypted room. Previously, a client without the room key silently sent unencrypted text into an end-to-end encrypted channel (and other members saw it as a normal message, none the wiser it went over the wire readable by the server). All send paths — typed messages, staged file attachments, and URL sends — are now blocked while a room is locked: the client offers the unlock prompt, keeps staged files in the tray, and refuses to transmit until the key is present, with a hard guard at the connection layer as backstop."
+ },
"changelog/v0.2.2.html": {
"href": "changelog/v0.2.2.html",
"title": "v0.2.2 - Startup & Shutdown Fixes | EchoHub Documentation",
@@ -1112,7 +1152,7 @@
"index.html": {
"href": "index.html",
"title": "EchoHub Documentation | EchoHub Documentation",
- "summary": "EchoHub Documentation Welcome to the EchoHub documentation. EchoHub is a decentralized, IRC-style chat platform. Self-hosted, terminal-first, with a built-in IRC gateway so native IRC clients can connect alongside the TUI client. Website: echohub.voidcube.cloud | Public Servers: Server Directory Quick Links Getting Started - Set up and run EchoHub Architecture - System design and IRC gateway API Reference - Generated C# API documentation Changelog - Release history"
+ "summary": "EchoHub Documentation Welcome to the EchoHub documentation. EchoHub is a decentralized, IRC-style chat platform. Self-hosted, terminal-first, with a built-in IRC gateway so native IRC clients can connect alongside the TUI client. Website: echohub.voidcube.cloud | Public Servers: Server Directory Quick Links Getting Started - Set up and run EchoHub TUI Guide - Keyboard shortcuts, slash commands, and everyday usage Messages & Attachments - Attaching, pasting, and receiving files Encrypted Rooms - End-to-end encrypted channels IRC Gateway - Connect with any IRC client Architecture - System design and IRC gateway API Reference - Generated C# API documentation Changelog - Release history"
},
"todo.html": {
"href": "todo.html",
diff --git a/manifest.json b/manifest.json
index 93f636a..8a6f00a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -248,6 +248,20 @@
"Title": "EchoHub.Client.Services.ClipboardFiles",
"Summary": "
Reads file paths that live on the OS clipboard as a file list (e.g. after copying a file in\nExplorer/Finder/Nautilus), which terminals do not paste as text. Lets Ctrl+V attach a copied\nfile directly instead of requiring the user to paste a raw path.
Reads raw image data from the OS clipboard (e.g. an image copied from a browser, or a\nWin+Shift+S screenshot), which terminals cannot paste as text. Always returns PNG bytes:\nclipboard PNG data is passed through, clipboard bitmaps (CF_DIB) are re-encoded.
Encrypts cached room content keys at rest so the client config never holds them as\nplain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other\nplatforms the keys are AES-GCM encrypted with a per-user master key file stored next\nto the config with 0600 permissions (prefix "k1:") — without an OS keychain that is\nfile-permission-level protection, not zero-knowledge: anyone who can read both the\nconfig and the key file can recover the room keys. Values with no recognized prefix\nare legacy plain-base64 keys from older clients; they load once and are re-encrypted.\nThe room passphrase itself is never stored in any form.
Holds room content keys for end-to-end encrypted channels: in-memory for the\nactive session, persisted per-server in the client config (like saved sessions)\nso users don't retype the passphrase every launch. Keys never leave this machine.
\n"
+ "Summary": "
Holds room content keys for end-to-end encrypted channels: in-memory for the\nactive session, persisted per-server in the client config (like saved sessions)\nso users don't retype the passphrase every launch. Keys never leave this machine\nand are encrypted at rest by . Also tracks which\nchannels are known to be end-to-end encrypted, so senders can refuse to emit\nplaintext into a room whose key isn't cached yet.
Thrown when sending into an end-to-end encrypted channel whose room key isn't cached:\nwithout the key the message would leave the client as plaintext, which must never happen.