OnSetNick
diff --git a/api/client/EchoHub.Client.Config.ClientConfig.html b/api/client/EchoHub.Client.Config.ClientConfig.html
index 542225c..e934077 100644
--- a/api/client/EchoHub.Client.Config.ClientConfig.html
+++ b/api/client/EchoHub.Client.Config.ClientConfig.html
@@ -213,6 +213,39 @@ Class ClientConfig
+
+
+ DefaultAsciiSize
+
+
+
+
ASCII-art rendering size for images you attach: "s" (40×40), "m" (80×80), or "l" (120×120).
+Applies to copy-paste/drag-drop attachments, which have no per-file size flag.
Folder where downloaded attachments and saved images are written. When null, the
+OS Downloads folder is used. Set via the native folder picker or /downloadpath.
diff --git a/api/client/EchoHub.Client.Config.SavedServer.html b/api/client/EchoHub.Client.Config.SavedServer.html
index 2b017ea..c9c5d00 100644
--- a/api/client/EchoHub.Client.Config.SavedServer.html
+++ b/api/client/EchoHub.Client.Config.SavedServer.html
@@ -182,6 +182,40 @@ Class SavedServer
+
+
+
+ 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<string, string> ChannelKeys { get; set; }
Sends one message with optional text and one or more file attachments.
+For end-to-end encrypted channels each attachment carries a declared kind and a
+room-encrypted preview (empty when none); the caption is likewise room-encrypted.
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.
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.
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.
+
+
+
+
+
public sealed record OutgoingAttachment : IEquatable<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.
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.
+
+
+
+
+
+
+
diff --git a/api/client/EchoHub.Client.Services.html b/api/client/EchoHub.Client.Services.html
index 0641c38..ff392eb 100644
--- a/api/client/EchoHub.Client.Services.html
+++ b/api/client/EchoHub.Client.Services.html
@@ -112,24 +112,69 @@ Runs async work on a background thread and routes exceptions to the UI.
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.
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.
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.
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.
Recognizes a dragged-and-dropped file (or files) that a terminal delivers into the input as an
+absolute path. Terminals differ: some paste the whole path at once, others send it character by
+character; either way this checks whether the current input text resolves to existing file(s).
Cheap pre-check so callers only stat the filesystem when the input plausibly holds a path:
+a quoted path, a Windows drive path (X:</code>/X:/), a UNC path (\), or a
+POSIX absolute path (/). Normal chat text never starts this way.
+ TryGetFiles(string, out List<string>, Func<string, bool>?)
+
+
+
+
Returns true when text resolves to one or more existing files.
+Handles a single path (quoted or not, possibly containing spaces) and multiple
+space-separated (optionally quoted) paths. fileExists is injectable
+for testing; production passes Exists(string).
+
+
+
+
+
public static bool TryGetFiles(string text, out List<string> files, Func<string, bool>? fileExists = null)
Recognizes a dragged-and-dropped file (or files) that a terminal delivers into the input as an
+absolute path. Terminals differ: some paste the whole path at once, others send it character by
+character; either way this checks whether the current input text resolves to existing file(s).
Converts emoji grapheme clusters to text shortcodes for safe TUI rendering.
diff --git a/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html b/api/client/EchoHub.Client.UI.ListSources.ChannelListSource.html
index 83ce409..91e77d9 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
-
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<string> fileNames, string asciiSizeLabel)
A file attached to a message. AsciiPreview holds the color-tag art for
+images (null otherwise). For end-to-end encrypted channels the content behind
+Url and the preview are ciphertext the server cannot read.
+
+
+
+
+
public record AttachmentDto : IEquatable<AttachmentDto>
A file attached to a message. AsciiPreview holds the color-tag art for
+images (null otherwise). For end-to-end encrypted channels the content behind
+Url and the preview are ciphertext the server cannot read.
+
+
+
+
+
public AttachmentDto(AttachmentKind Kind, string Url, string FileName, long FileSize, string? AsciiPreview = null)
+ Deconstruct(out string, out string?, out bool, out string?, out string?, out string?)
+
+
+
+
+
+
+
+
public void Deconstruct(out string Name, out string? Topic, out bool IsPublic, out string? Password, out string? EncryptionSalt, out string? WrappedRoomKey)
Parameters
@@ -369,6 +468,12 @@ Class CreateChannelRequest bool
+
+ Deconstruct(out bool, out List<MessageDto>, out string?, out bool, out string?, out string?)
+
+
+
+
+
+
+
+
public void Deconstruct(out bool Success, out List<MessageDto> History, out string? Error, out bool PasswordRequired, out string? EncryptionSalt, out string? WrappedRoomKey)
Parameters
@@ -369,6 +468,12 @@ Class JoinChannelResult string
+
- Deconstruct(out Guid, out string, out string, out string?, out string, out MessageType, out string?, out string?, out DateTimeOffset, out long?, out List<EmbedDto>?)
+
+ Deconstruct(out Guid, out string, out string, out string?, out string, out DateTimeOffset, out List<AttachmentDto>?, out List<EmbedDto>?)
Passphrase change for an encrypted channel: the client proves knowledge of the old
+passphrase (old auth key), then supplies the re-wrapped room key under the new one.
+
+
+
+
+
public record RekeyChannelRequest : IEquatable<RekeyChannelRequest>
Passphrase change for an encrypted channel: the client proves knowledge of the old
+passphrase (old auth key), then supplies the re-wrapped room key under the new one.
+
+
+
+
+
public RekeyChannelRequest(string OldPassword, string NewPassword, string NewEncryptionSalt, string NewWrappedRoomKey)
A file attached to a message. AsciiPreview holds the color-tag art for
+images (null otherwise). For end-to-end encrypted channels the content behind
+Url and the preview are ciphertext the server cannot read.
Passphrase change for an encrypted channel: the client proves knowledge of the old
+passphrase (old auth key), then supplies the re-wrapped room key under the new one.
Rendered ASCII-art preview for images (color-tag format). Null for audio/files.
+Stored encrypted-at-rest when database encryption is enabled, and room-encrypted
+for end-to-end encrypted channels.
The kind of a message attachment. Determines how the client renders it
+(ASCII preview for images, a play affordance for audio, a download line for files).
The kind of a message attachment. Determines how the client renders it
+(ASCII preview for images, a play affordance for audio, a download line for files).
Client-side envelope encryption for private (end-to-end encrypted) channels.
+
Design: at creation the client generates a random 256-bit room content key (RCK)
+that encrypts all room content. The RCK is stored on the server wrapped
+(AES-GCM encrypted) by a key derived from the passphrase, next to a BCrypt hash
+of a separately derived auth key used as the join gate. The passphrase, the
+key-encryption key, and the RCK never leave the client, so the server can gate
+joins and count/measure content without being able to read it. Changing the
+passphrase only re-wraps the RCK — history is never re-encrypted.
+
Derivation: PBKDF2-SHA256(passphrase, salt, 210000 iterations) → 64 bytes;
+first 32 bytes are the auth key (sent to the server as lowercase hex),
+last 32 bytes are the key-encryption key (never sent).
Client-side envelope encryption for private (end-to-end encrypted) channels.
+
Design: at creation the client generates a random 256-bit room content key (RCK)
+that encrypts all room content. The RCK is stored on the server wrapped
+(AES-GCM encrypted) by a key derived from the passphrase, next to a BCrypt hash
+of a separately derived auth key used as the join gate. The passphrase, the
+key-encryption key, and the RCK never leave the client, so the server can gate
+joins and count/measure content without being able to read it. Changing the
+passphrase only re-wraps the RCK — history is never re-encrypted.
+
Derivation: PBKDF2-SHA256(passphrase, salt, 210000 iterations) → 64 bytes;
+first 32 bytes are the auth key (sent to the server as lowercase hex),
+last 32 bytes are the key-encryption key (never sent).
@@ -152,11 +152,11 @@ Class FileValidationHelper
+
-
+
IsAudioFile(string)
-
+
Checks whether the file name has a recognized audio extension.
@@ -189,11 +189,11 @@ Class FileValidationHelper
+
-
+
IsValidImage(Stream)
-
+
Validates that a stream contains a recognized image format by checking magic bytes.
@@ -231,7 +231,7 @@ The stream position is reset to the beginning after validation.
Public crypto metadata for a channel: whether it is end-to-end encrypted and the
+PBKDF2 salt clients need to derive their join credential. Never returns the
+wrapped room key — that is only handed out after a successful join.
@@ -317,6 +357,95 @@ public Task<IActionResult> GetChannels(int offset = 0, int limit = 50)
+
+
+ RekeyChannel(string, RekeyChannelRequest)
+
+
+
+
Changes an encrypted channel's passphrase by re-wrapping its room key.
+The caller proves knowledge of the old passphrase via the old auth key;
+history is never re-encrypted (the room content key does not change).
Sends one message carrying optional text (content form field) plus zero or more
+file attachments (Discord-style). For non-encrypted channels the server sniffs each file's
+kind and renders ASCII previews for images. For end-to-end encrypted channels the client
+uploads ciphertext blobs and declares each file's kind (kind) and pre-rendered,
+room-encrypted preview (preview), aligned by file order — the server never inspects them.
Changes an encrypted channel's passphrase by swapping the join-gate hash and the
+wrapped room key. The room content key itself never changes, so history stays
+readable — the client re-wraps it under the new passphrase-derived key.
+Creator only: admins cannot rekey a room whose passphrase they don't know.
Sets, changes, or clears (null) a channel's join password. Creator or admin only.
+Not available on end-to-end encrypted channels — those change passphrase via
+RekeyChannelAsync(Guid, string, string, string, string, string) so the room key envelope stays consistent.
+
+
+
+
+
public Task<ChannelOperationResult> SetChannelPasswordAsync(Guid callerUserId, string channelName, string? password)
diff --git a/api/server/EchoHub.Server.Services.FileStorageService.html b/api/server/EchoHub.Server.Services.FileStorageService.html
index 5ab0190..e7c1856 100644
--- a/api/server/EchoHub.Server.Services.FileStorageService.html
+++ b/api/server/EchoHub.Server.Services.FileStorageService.html
@@ -254,6 +254,39 @@ Class FileStorageService
+
+
+ GetStoredFileIds()
+
+
+
+
Returns the set of stored file ids (filenames without extension) currently on disk.
+One directory scan, so callers can bulk-check many attachments without a glob per file.
Private channels are now genuinely private: password-protected channels are end-to-end encrypted, so the server (and its operators) can gate joins and measure storage but cannot read message or file contents. The IRC gateway grows real MODE/TOPIC support and channel keys, and the client gets image "save original", a transparent-light theme, drag-and-drop file sending, Ctrl+V paste, and a fix for the intermittent Ctrl+W crash.
+
New Features
+
+
End-to-end encrypted channels — creating a channel with a password now provisions a zero-knowledge room:
+
+
The passphrase never leaves the client. It derives (PBKDF2-SHA256, 210k iterations) two keys: an auth key sent to the server as the join credential, and a key-encryption key that never leaves the machine.
+
A random room content key encrypts every message and file with AES-256-GCM. The server only ever stores the room key wrapped under the passphrase, so it can gate joins and report a channel's message count, storage size, and attachments — but cannot decrypt any of it. Even the server owner cannot read a private room's contents.
+
Members' clients cache the derived room key locally (in the per-server config, like saved sessions) so the passphrase isn't retyped every launch; joining on a new device prompts for it once.
+
Change the passphrase with /passwd <old> <new> (channel creator only). The room key is re-wrapped, not rotated, so existing history stays readable and members who join later with the new passphrase can still read older messages.
+
Files and images are encrypted client-side before upload; for images the ASCII-art preview is rendered on the client and stored room-encrypted too. Sending images by URL is disabled in encrypted channels (the server can't fetch-and-render without the key).
+
End-to-end encrypted channels cannot be joined over the IRC gateway (that would require the server to hold the room key) — IRC JOIN returns 475 directing users to the EchoHub client.
+
+
+
Password-protected channels — set an optional password when creating a channel (masked field in the Create Channel dialog, password on POST /api/channels). Passwords are BCrypt-hashed server-side; the join gate applies on first join only (existing members and the creator are unaffected). Protected channels show a * marker in the channel list and +k in the status bar
+
Save original images — image messages now show a clickable "[↓ save original]" line under the ASCII-art preview that downloads the full-resolution original to your Downloads folder (decrypting locally in encrypted channels)
+
Messages with attachments (Discord-style) — a message is now text plus a list of attachments instead of being either text or a single file. One message can carry a caption and several files (images, audio, docs) together:
+
+
Compose with a staging tray: /send <file> or dropping files onto the terminal stages them (shown on the input bar); the next Enter sends your typed caption and all staged files as one message. /clear drops staged files. /send <URL> still posts an image immediately.
+
Each image attachment renders its own ASCII preview with its own "save original" action; audio/file attachments each get their own play/download line.
+
In encrypted channels every attachment is encrypted individually (blob + ASCII preview), and the caption is room-encrypted — the server still stores only ciphertext and can report count/size but not contents.
+
Up to 10 attachments per message.
+
+
+
Right-click message menu — right-click any message for a context menu: save/download/play its attachment, mention the sender, view their profile, copy the text, copy the message ID (for linking or command arguments), or delete the message. (Keyboard: F6 focuses the message list for arrow-key selection + Delete.) The selected message is now highlighted while the list is focused.
+
Message deletion — press Delete on a selected message to remove it. You can always delete your own messages; moderators and above can delete others' messages, but only from users below their own role (a mod can't delete an admin's or owner's message). Deleting a message also removes its attachment blobs from server storage.
+
Customizable download folder — /downloadpath opens your OS-native folder picker (Windows Explorer / macOS Finder / Linux GTK or KDE) to choose where downloaded attachments and saved images go; /downloadpath <path> sets it directly (the fallback when no native picker is available). Downloaded files now land in that folder (with automatic (n) de-duplication) instead of a temp directory.
+
/join <channel> [password] — join protected channels inline, or let the client prompt: joining a protected channel without a password opens a masked prompt that re-prompts on a wrong password
+
IRC channel keys — JOIN #room <key> works against room passwords (RFC 1459 comma-paired key lists supported); keyless or wrong-key joins get 475 ERR_BADCHANNELKEY
+
IRC MODE implemented — MODE #chan reports +k/+, MODE #chan +k <key> sets and -k clears the room password (channel creator or admin only), ban-list probes get a clean empty reply, and CHANMODES is advertised in ISUPPORT
+
IRC TOPIC set support — the channel creator can change the topic from IRC; the change broadcasts to connected TUI clients (previously topic changes were rejected with a stub error)
+
Attach a file by drag & drop or by pasting — drop a file onto the terminal, or copy a file in your file manager and press Ctrl+V, to stage it as an attachment (the next Enter sends it with your caption). Multiple files at once are supported. Ctrl+V still pastes text when the clipboard holds text; Ctrl+Y is a paste alias. On Windows the copied-file paste reads the clipboard's file list directly (Windows Terminal never pastes copied files as text), with xclip/wl-paste used on Linux
+
Pick ASCII-art size for attached images — /size opens a Small/Medium/Large picker (40×40 / 80×80 / 120×120) with descriptions, /size <s|m|l> sets it directly, and /send <file> -l sets it for that message. The choice is a saved preference and applies to copy-paste/drag-drop images (which have no per-file flag); the current size is shown in the staging tray
+
New TransparentLight theme — dark characters on a transparent background, for light terminal color schemes (/theme transparentlight)
+
Timestamps in messages are now aware of the current culture and display the short time pattern for today's messages and the short date+time pattern for older messages.
+
+
Bug Fixes
+
+
Transparent themes no longer draw an opaque box behind the message input. The input TextView renders with the Editable visual role, which Terminal.Gui derives as an opaque color when a theme leaves it unset; the themes now pin Editable/ReadOnly to their base colors so the input matches its (transparent) background.
+
Attachments whose files have been pruned (retention cleanup deletes blobs older than Storage:RetentionDays but left the message rows) no longer render a dead download/preview. When channel history loads, the server checks which attachment blobs still exist: missing ones are dropped from the message, and an attachment-only message whose files are all gone is removed from the database.
+
Fixed intermittent crash on Ctrl+W — Terminal.Gui binds Ctrl+W to clipboard-cut, and Windows clipboard contention (another app holding the clipboard) threw an unhandled Win32Exception that took the app down. Ctrl+W now deletes the previous word (readline behavior, no clipboard), and all clipboard shortcuts (Ctrl+X/C/V/Y) are guarded so transient clipboard failures log a warning instead of crashing
+
Fixed emoji shortcode replacement permanently disabling itself if a cursor update threw mid-replacement
+
IRC LIST no longer leaks private channels; protected channels are marked [+k]
+
+
API Changes
+
+
ChannelDto gains isProtected and isEncrypted; CreateChannelRequest gains optional password, encryptionSalt, and wrappedRoomKey; SignalR JoinChannel takes an optional second password argument and JoinChannelResult gains passwordRequired, encryptionSalt, and wrappedRoomKey (older clients must update to join over SignalR)
+
New endpoints: GET /api/channels/{channel}/crypto (public crypto metadata — salt only, never the wrapped key) and POST /api/channels/{channel}/rekey (creator-only passphrase change)
+
The upload endpoint accepts type and content form fields for encrypted channels, where the client supplies the declared message type and room-encrypted content
+
ImageToAsciiService and FileValidationHelper moved from EchoHub.Server to EchoHub.Core so the client can render ASCII art and detect file types for encrypted uploads
+
Message shape change: MessageDto drops Type/AttachmentUrl/AttachmentFileName/AttachmentFileSize and gains Attachments (a list of AttachmentDto { Kind, Url, FileName, FileSize, AsciiPreview }, null/empty for plain text). New Attachment entity + table with a cascade FK to Message
+
New endpoint POST /api/channels/{channel}/messages (multipart: content + N files, plus kind/preview per file for encrypted channels) replaces the single-file upload endpoint; DELETE /api/moderation/messages/{id} now enforces the own-or-higher-role rule
+
New EF migrations AddChannelPasswordHash, AddChannelEncryptionEnvelope, and AddMessageAttachments (applied automatically on server start); a one-time startup data migration folds legacy single-attachment messages into the new model