mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 23:34:10 +02:00
docs: Update documentation for 145 files
Generated by AurionDocs
Job ID: c99fff50-67a3-4294-b4df-3e73f4f12de9
Source commit: 4dcb480
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
# AsyncRunner
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/AsyncRunner.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class AsyncRunner
|
||||
```
|
||||
|
||||
|
||||
Eliminates repeated Task.Run/try/catch/app.Invoke(ShowError) boilerplate by consolidating the common pattern of running background work and surfacing errors to the UI. It runs the provided async work on a background thread and routes any exceptions to the UI thread for user notification.
|
||||
|
||||
## Remarks
|
||||
AsyncRunner encapsulates a cross-cutting concern: performing asynchronous work without blocking the UI and centralizing error reporting. It uses Task.Run to execute work off the calling thread and app.Invoke to marshal the error surface back to the UI. When an exception occurs, it logs the failure with the provided context (logContext if supplied, otherwise errorPrefix) and shows a UI message using showError prefixed by errorPrefix. Because Run is fire-and-forget (it returns void), callers should not rely on it for completion or exception propagation; choose a different pattern if you need to observe results.
|
||||
|
||||
## Notes
|
||||
- This method is fire-and-forget; exceptions are caught and surfaced but not propagated to the caller.
|
||||
- The UI update and logging rely on the provided IApplication and showError delegate; ensure they are safe to call from a background thread; app.Invoke is used to marshal to the UI thread.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AudioPlaybackService
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/AudioPlaybackService.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public class AudioPlaybackService
|
||||
```
|
||||
|
||||
|
||||
AudioPlaybackService is a thread-safe wrapper around an underlying audio player that exposes asynchronous playback controls and a finished event surface. Use it when you need serialized access to play, pause, resume, or stop audio and a consistent event interface without managing locks and state machines yourself.
|
||||
|
||||
## Remarks
|
||||
To prevent concurrent calls from interfering with playback state, the class serializes all operations using a SemaphoreSlim. When PlayAsync is invoked while something is already playing, it stops the current track before starting the new file; PauseAsync, ResumeAsync, and StopAsync perform their actions only when appropriate states are detected. The PlaybackFinished event is forwarded from the internal player, so callers can react to completion without depending on the concrete implementation of the _player. Exceptions raised by the underlying player are caught and logged with a warning, ensuring playback issues do not crash the application.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example usage
|
||||
var audio = new AudioPlaybackService();
|
||||
await audio.PlayAsync("path/to/file.mp3");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- This wrapper serializes calls to avoid race conditions; however, it is not cancellation-aware. If you need to cancel an in-flight operation, extend the class with cancellation support or a dedicated cancellation mechanism.
|
||||
@@ -0,0 +1,33 @@
|
||||
# AvatarHelper
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/AvatarHelper.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
internal static class AvatarHelper
|
||||
```
|
||||
|
||||
|
||||
AvatarHelper centralizes the shared logic for uploading avatars by accepting either a local file path or an HTTP(S) URL, resolving the input to a stream, and uploading it via ApiClient.UploadAvatarAsync. It returns the ASCII art response from the server, providing a straightforward way to obtain the server-side representation of the uploaded avatar without duplicating local-file or network-handling code.
|
||||
|
||||
## Remarks
|
||||
|
||||
By supporting both local and remote sources behind a single UploadAsync entry point, AvatarHelper hides the mechanics of data retrieval and stream management from call sites and ensures consistent disposal of the stream. The actual upload is delegated to ApiClient, keeping concerns separated between data acquisition and server interaction. The class is internal, reinforcing its role as a reusable utility within the client layer rather than a public API.
|
||||
|
||||
The method propagates errors from file access, HTTP fetch, or the server upload to the caller, which is appropriate for a small, focused helper that prioritizes simplicity over internal retries or resilience policies.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
// Example usage within the same assembly
|
||||
var client = new ApiClient("https://api.example.org");
|
||||
string? artFromFile = await AvatarHelper.UploadAsync(client, @"C:\avatars\user.png");
|
||||
string? artFromUrl = await AvatarHelper.UploadAsync(client, "https://example.org/avatars/user.png");
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Creating a new HttpClient per invocation can lead to socket exhaustion in high-throughput scenarios; consider reusing a shared HttpClient instance or using HttpClientFactory in production code.
|
||||
- If the local path does not exist, a FileNotFoundException is thrown.
|
||||
- When targeting a URL, if the URL's file name is missing or lacks an extension, the code defaults to using avatar.png as the upload file name.
|
||||
- Exceptions from the HTTP request or the server upload propagate to the caller; there is no retry logic within this helper.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ClientEncryptionService
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/ClientEncryptionService.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class ClientEncryptionService : IMessageEncryptionService
|
||||
```
|
||||
|
||||
|
||||
ClientEncryptionService provides client-side encryption for messages by applying AES-256-GCM using a key supplied by the server. It mirrors the server’s encryption format so messages are encrypted end-to-end between client and server. When no key has been set, Encrypt is a no-op and returns the plaintext to preserve compatibility with unauthenticated flows; once initialized, Encrypt produces a prefixed, base64-encoded payload containing the nonce and ciphertext+tag, and Decrypt reverses this process. If decryption fails due to a missing or mismatched key or corrupted data, a sentinel message is returned to indicate the failure and prompt re-authentication to refresh the key.
|
||||
|
||||
## Remarks
|
||||
This abstraction isolates cryptography behind a single, testable service that can be swapped or disabled without changing business logic. It enforces a clear security boundary: encryption only happens after a server-provided key is loaded, reducing the risk of leaking plaintext. The pre-key pass-through behavior preserves compatibility with existing flows during login or in environments where the key has not yet been fetched.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example: encrypt and decrypt with a server-provided key
|
||||
var client = new ClientEncryptionService();
|
||||
|
||||
// Create a 32-byte key for demonstration (replace with real server-provided key)
|
||||
var keyBytes = new byte[32];
|
||||
var base64Key = Convert.ToBase64String(keyBytes);
|
||||
client.SetKey(base64Key);
|
||||
|
||||
string plaintext = "Secret message";
|
||||
string encrypted = client.Encrypt(plaintext);
|
||||
string decrypted = client.Decrypt(encrypted);
|
||||
// decrypted should equal plaintext
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Encrypt and Decrypt only work after a 32-byte key has been provided via SetKey; otherwise Encrypt returns plaintext and Decrypt returns content unchanged.
|
||||
- If the encrypted content is tampered with, the key is wrong, or the payload is malformed, Decrypt returns the special placeholder: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
|
||||
- The key is held in memory and is not rotated automatically; ensure proper key management and re-fetch after key rotation on the server.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ClipboardFiles
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/ClipboardFiles.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class ClipboardFiles
|
||||
```
|
||||
|
||||
|
||||
ClipboardFiles reads file paths from the clipboard when the clipboard contains a file-list (such as after copying files in Explorer/Finder). Use TryGetFiles to retrieve those paths so you can attach copied files directly without pasting textual paths; this works on Windows and Linux, while macOS and other platforms do not expose a file-list clipboard.
|
||||
|
||||
## Remarks
|
||||
ClipboardFiles encapsulates platform differences behind a single API. It isolates Windows-specific CF_HDROP handling and Linux's text/uri-list retrieval, performing path existence checks and filtering out non-file entries to return a clean list of existing paths. It returns true only when at least one file is found; otherwise false, letting callers gracefully fall back to other input methods.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
if (ClipboardFiles.TryGetFiles(out var files))
|
||||
{
|
||||
Console.WriteLine($"Clipboard contains {files.Count} file(s): {string.Join(", ", files)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Clipboard does not contain a file-list or contains only non-existent paths.");
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Returns only existing files; non-existent or inaccessible paths are ignored.
|
||||
- Windows implementation relies on CF_HDROP with a brief retry loop to tolerate clipboard contention.
|
||||
- Linux implementation uses wl-paste or xclip (one must be available for success).
|
||||
- macOS and other platforms do not provide file-list clipboard support.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ClipboardImage
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/ClipboardImage.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class ClipboardImage
|
||||
```
|
||||
|
||||
|
||||
Reads raw image data from the OS clipboard and returns PNG-encoded bytes suitable for saving, embedding, or transmitting. Use this when you need a single, consistent PNG representation of whatever image the user has copied (browser-copied PNGs, screenshots, editor bitmaps) so callers don't need per-OS or per-format handling.
|
||||
|
||||
## Remarks
|
||||
This class normalizes multiple clipboard image formats into PNG. It prefers native clipboard PNG formats when available (preserving transparency) and falls back to platform clipboard bitmaps (CF_DIB on Windows) by wrapping the DIB bytes in a minimal BMP file header and decoding/re-encoding them as PNG. TryGetPng routes to OS-specific helpers and catches/logs errors, returning false on failure rather than throwing.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Save whatever image is on the clipboard to a file named clipboard.png
|
||||
if (ClipboardImage.TryGetPng(out var png))
|
||||
{
|
||||
System.IO.File.WriteAllBytes("clipboard.png", png);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- DibToPng returns null for malformed or undecodable DIB input; TryGetPng propagates that as a failure (false).
|
||||
- The implementation prefers registered PNG clipboard formats to preserve alpha; CF_DIB bitmaps are re-encoded and may lose or change metadata.
|
||||
- Re-encoding a bitmap to PNG allocates memory and does CPU work; callers should avoid doing this in a tight loop.
|
||||
- TryGetPng checks the platform (Windows/Linux/macOS) and will return false on unsupported platforms; failures are logged rather than thrown.
|
||||
@@ -0,0 +1,63 @@
|
||||
# ConnectionManager.cs
|
||||
|
||||
> **Source:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||
|
||||
## Contents
|
||||
|
||||
- [ConnectionManager](#connectionmanager)
|
||||
- [ConnectResult](#connectresult)
|
||||
|
||||
---
|
||||
|
||||
## ConnectionManager
|
||||
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
internal sealed class ConnectionManager : IAsyncDisposable
|
||||
```
|
||||
|
||||
|
||||
Manages the full lifecycle of a live chat connection: authenticating with the server, establishing end-to-end encryption keys, creating and wiring the SignalR (EchoHub) connection, tracking joined channels, and exposing a thin event surface that the UI (AppOrchestrator) can subscribe to. Use this when you want a single, high-level component to own connection state and SignalR event forwarding instead of manipulating ApiClient and EchoHubConnection directly.
|
||||
|
||||
## Remarks
|
||||
This class centralizes the responsibilities that would otherwise be scattered across UI code: authentication and token rotation, attempting to fetch and apply the E2E encryption key, instantiating and wiring an EchoHubConnection, and keeping track of which channels have been joined. It forwards SignalR events as simple .NET events so the UI layer can react without needing to know SignalR details. ConnectionManager also implements IAsyncDisposable so callers can cleanly tear down both the EchoHubConnection and the underlying ApiClient.
|
||||
|
||||
## Notes
|
||||
- ConnectionManager may raise forwarded events from background threads (SignalR callbacks). UI handlers should marshal to the UI thread if required by the UI framework.
|
||||
- ConnectAsync reports progress via the onStatus callback and will throw on authentication failure; callers are expected to handle expired saved sessions or retry logic.
|
||||
- Failure to fetch the encryption key is treated as non-fatal: the manager logs a warning and proceeds without message encryption.
|
||||
- Dispose of the manager (DisposeAsync) when the app shuts down to ensure the hub connection and ApiClient are cleaned up.
|
||||
|
||||
---
|
||||
|
||||
## ConnectResult
|
||||
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||
> **Kind:** record
|
||||
|
||||
```csharp
|
||||
internal record ConnectResult(
|
||||
LoginResponse Login,
|
||||
List<ChannelDto> Channels,
|
||||
Dictionary<string, List<MessageDto>> Histories)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default |
|
||||
|-----------|------|---------|
|
||||
| `Login` | [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) | — |
|
||||
| `Channels` | `List<ChannelDto>` | — |
|
||||
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
|
||||
|
||||
|
||||
Represents the outcome of a successful connection, returned to AppOrchestrator for UI updates. It bundles the authentication result, the current set of channels, and the initial histories for every auto-joined channel (keyed by channel name and including the default channel). As an immutable record, it serves as a single, self-contained snapshot that the UI can bootstrap from after a connect.
|
||||
|
||||
## Remarks
|
||||
This object centralizes the data needed to render the initial connected state, decoupling the connection logic from the UI orchestration. By passing a single ConnectResult, the AppOrchestrator can immediately populate channel lists and histories without issuing additional fetches, promoting a clean separation between connection handling and presentation concerns.
|
||||
|
||||
## Notes
|
||||
- ConnectResult is immutable; to reflect changes (e.g., new messages or channels), construct and pass a new instance rather than mutating the existing one.
|
||||
- Histories is a dictionary keyed by channel name that contains the initial per-channel histories; ensure channel names in the dictionary align with the Channels list to avoid inconsistencies.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,158 @@
|
||||
# EchoHubConnection.cs
|
||||
|
||||
> **Source:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||
|
||||
## Contents
|
||||
|
||||
- [ChannelPasswordRequiredException](#channelpasswordrequiredexception)
|
||||
- [EchoHubConnection](#echohubconnection)
|
||||
- [RoomLockedException](#roomlockedexception)
|
||||
- [JoinOutcome](#joinoutcome)
|
||||
|
||||
---
|
||||
|
||||
## ChannelPasswordRequiredException
|
||||
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class ChannelPasswordRequiredException : Exception
|
||||
```
|
||||
|
||||
|
||||
Thrown when joining a channel fails because a password is required or the provided password is incorrect. The UI catches this to prompt the user for credentials and retry the join, using ChannelName to provide channel context.
|
||||
|
||||
## Remarks
|
||||
ChannelPasswordRequiredException provides a precise signal for a password-related join failure. By carrying the ChannelName, it enables the UI to present a meaningful prompt and retry flow without inspecting lower-level errors. This focused exception helps keep join logic cohesive and testable by separating password-entry concerns from generic failure handling.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
// Code that attempts to join a channel and may throw ChannelPasswordRequiredException
|
||||
}
|
||||
catch (ChannelPasswordRequiredException ex)
|
||||
{
|
||||
Console.WriteLine($"Password is required to join channel '{ex.ChannelName}'.");
|
||||
// Prompt the user for a password and retry the join using the provided channel name
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Be mindful that ChannelName may be null if constructed with null; guard accordingly before displaying it to users.
|
||||
|
||||
---
|
||||
|
||||
## EchoHubConnection
|
||||
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class EchoHubConnection : IAsyncDisposable
|
||||
```
|
||||
|
||||
|
||||
A SignalR-backed client wrapper that manages a HubConnection to the Echo chat hub, integrates client-side encryption/room-key lookup, and exposes simple event callbacks for incoming messages, presence and channel events. Reach for EchoHubConnection when you need a higher-level, event-driven connection to the server that automatically handles authentication token provisioning and reconnect behavior while decrypting incoming payloads for the UI.
|
||||
|
||||
## Remarks
|
||||
EchoHubConnection encapsulates the SignalR HubConnection lifecycle and maps server callbacks onto plain .NET events (e.g. OnMessageReceived, OnUserJoined, OnChannelUpdated). It supplies the HubConnectionBuilder with an AccessTokenProvider using the provided ApiClient so calls are authenticated, and it wires automatic-reconnect handlers that surface connection state changes via OnConnectionStateChanged and OnReconnected. Incoming MessageDto instances are passed through the client-side encryption pipeline (ClientEncryptionService and RoomKeyStore) so the UI sees decrypted content or a locked placeholder when a room key is not available.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Subscribe to events and inspect connection state
|
||||
var echo = new EchoHubConnection(serverUrl, apiClient, encryptionService, roomKeyStore);
|
||||
|
||||
echo.OnMessageReceived += message =>
|
||||
{
|
||||
// MessageDto is provided by the library; content may be the LockedMessagePlaceholder
|
||||
Console.WriteLine($"Message received in {message.ChannelName}: {message.Content}");
|
||||
};
|
||||
|
||||
if (echo.IsConnected)
|
||||
{
|
||||
Console.WriteLine("Currently connected to the chat hub.");
|
||||
}
|
||||
|
||||
// Remember to dispose when finished
|
||||
await echo.DisposeAsync();
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Events are raised directly from SignalR callbacks; handlers may not run on a UI thread — marshal to the UI thread if required.
|
||||
- Encrypted messages for channels without a stored key are replaced with LockedMessagePlaceholder; supply the channel passphrase (through the app's key store flow) to see decrypted content.
|
||||
- Call DisposeAsync to release the underlying HubConnection and related resources to avoid background network activity.
|
||||
|
||||
---
|
||||
|
||||
## RoomLockedException
|
||||
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class RoomLockedException : Exception
|
||||
```
|
||||
|
||||
|
||||
Thrown to signal a security-sensitive condition when attempting to send a message into an end-to-end encrypted channel whose room key isn't cached. The operation is blocked to prevent sending plaintext; catching this exception lets the UI prompt for the channel's passphrase and unlock the room before retrying.
|
||||
|
||||
## Remarks
|
||||
|
||||
RoomLockedException acts as a clear boundary between encryption policy and transport logic. By exposing the ChannelName, callers can present a channel-scoped unlock prompt without parsing the error text, and the sealed Exception type communicates a concrete, expected failure mode that downstream code can handle distinctly from generic errors.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
// Simulated scenario: an attempt to send into a locked E2E channel
|
||||
throw new RoomLockedException("Lobby");
|
||||
}
|
||||
catch (RoomLockedException ex)
|
||||
{
|
||||
// Use the information to drive the unlock UX
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine($"Unlock channel: {ex.ChannelName} by entering its passphrase.");
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Do not swallow this as a generic error; catch RoomLockedException to trigger the unlock UX and use ex.ChannelName to identify the affected channel. The displayed message is user-facing and not localized.
|
||||
|
||||
---
|
||||
|
||||
## JoinOutcome
|
||||
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||
> **Kind:** record
|
||||
|
||||
```csharp
|
||||
public sealed record JoinOutcome(List<MessageDto> History, string? EncryptionSalt, string? WrappedRoomKey)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default |
|
||||
|-----------|------|---------|
|
||||
| `History` | `List<MessageDto>` | — |
|
||||
| `EncryptionSalt` | `string?` | — |
|
||||
| `WrappedRoomKey` | `string?` | — |
|
||||
|
||||
|
||||
JoinOutcome is a sealed record that represents the result of joining a channel: it includes the decrypted history (History) and, for end-to-end encrypted channels, the key envelope necessary to unlock the room's content key (WrappedRoomKey). EncryptionSalt is the salt used to derive the encryption key when applicable. This type is typically produced by the join logic and consumed by the UI to render messages and initialize decryption if needed.
|
||||
|
||||
## Remarks
|
||||
This abstraction centralizes the outcome of a join into a single, immutable value that downstream components can rely on. The History is always present (even if empty), while EncryptionSalt and WrappedRoomKey are nullable to reflect that some rooms are not end-to-end encrypted or that keys may not be provisioned yet. By grouping history and encryption metadata together, the join logic can separate concerns: rendering chat versus handling cryptographic setup.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
|
||||
List<MessageDto> history = new List<MessageDto>();
|
||||
var joinResult = new JoinOutcome(history, null, null);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- EncryptionSalt and WrappedRoomKey can be null; callers should verify non-null before attempting decryption-related steps.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,96 @@
|
||||
# NativeFolderPicker.cs
|
||||
|
||||
> **Source:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||
|
||||
## Contents
|
||||
|
||||
- [NativeFolderPicker](#nativefolderpicker)
|
||||
- [FolderPickResult](#folderpickresult)
|
||||
- [PickerOutcome](#pickeroutcome)
|
||||
|
||||
---
|
||||
|
||||
## NativeFolderPicker
|
||||
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class NativeFolderPicker
|
||||
```
|
||||
|
||||
|
||||
Opens the OS-native folder picker by shelling out to the host OS, keeping the TUI free of GUI toolkit dependencies. It supports Windows, macOS, and Linux by delegating to platform-specific helpers and returns a FolderPickResult that communicates whether a folder was chosen, the dialog was cancelled, or the native picker is unavailable so the caller can fall back to a configured path.
|
||||
|
||||
## Remarks
|
||||
|
||||
NativeFolderPicker centralizes cross-platform behavior for obtaining a folder path without pulling in a GUI toolkit. It hides OS differences behind a single entry point, PickFolderAsync, and exposes a uniform result type (FolderPickResult with a PickerOutcome) that callers can inspect to either proceed with the chosen path or fall back to defaults. Failures are caught and logged, ensuring graceful degradation rather than exceptions propagating to the UI.
|
||||
|
||||
## Notes
|
||||
- Linux will not attempt a graphical picker if no graphical session is detected (DISPLAY or WAYLAND_DISPLAY are missing); in that case, the method returns Unavailable.
|
||||
- On Windows, the initial directory is sanitized (apostrophes are doubled) to safely embed the path in the PowerShell script, and PowerShell is invoked via an encoded command to avoid quoting issues.
|
||||
- If the platform-specific helper cannot be started, the code falls back to returning Unavailable instead of throwing, allowing callers to implement their own fallback strategy.
|
||||
|
||||
---
|
||||
|
||||
## FolderPickResult
|
||||
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||
> **Kind:** record
|
||||
|
||||
```csharp
|
||||
public sealed record FolderPickResult(PickerOutcome Outcome, string? Path)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default |
|
||||
|-----------|------|---------|
|
||||
| `Outcome` | `PickerOutcome` | — |
|
||||
| `Path` | `string?` | — |
|
||||
|
||||
|
||||
FolderPickResult is an immutable data carrier that represents the outcome of a native folder-picking operation and, when successful, the path of the selected folder.
|
||||
|
||||
## Remarks
|
||||
Because FolderPickResult is a record, it benefits from value-based equality and straightforward pattern matching when consumed by calling code. The Path member is nullable to reflect that a folder may not be selected; always check the Outcome before using Path. This abstraction decouples application logic from platform-specific picker implementations, promoting testability and cross-platform compatibility.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var result = new FolderPickResult(PickerOutcome.Success, @"C:\Projects");
|
||||
if (result.Outcome == PickerOutcome.Success && result.Path is not null)
|
||||
{
|
||||
Console.WriteLine(result.Path);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Path may be null when Outcome indicates cancellation or failure; always verify Outcome before accessing Path.
|
||||
|
||||
---
|
||||
|
||||
## PickerOutcome
|
||||
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||
> **Kind:** enum
|
||||
|
||||
```csharp
|
||||
public enum PickerOutcome
|
||||
{
|
||||
Chosen,
|
||||
|
||||
Cancelled,
|
||||
|
||||
Unavailable,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
PickerOutcome encodes the result of attempting to display a native folder picker. It defines three mutually exclusive states: Chosen (the user picked a folder and FolderPickResult.Path is set), Cancelled (the native dialog ran but no selection was made), and Unavailable (no native picker is available on the current machine).
|
||||
|
||||
Use this enum to drive post-pick logic without scattering platform checks or error handling across call sites.
|
||||
|
||||
## Remarks
|
||||
This enum serves as a lightweight sum type for the outcome of a folder-picking operation. It centralizes decision points and pairs with FolderPickResult to obtain the actual path when Chosen is returned. Consumers can implement a fallback flow for Unavailable and provide a smooth user experience when Cancelled.
|
||||
|
||||
## Notes
|
||||
- Unavailable is not an error; it indicates the absence of a native picker and warrants a fallback strategy (e.g., a non-native picker or manual path entry).
|
||||
|
||||
---
|
||||
@@ -0,0 +1,19 @@
|
||||
# NotificationSoundService
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/NotificationSoundService.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public class NotificationSoundService
|
||||
```
|
||||
|
||||
|
||||
NotificationSoundService centralizes the playback of the notification sound. It resolves the sound file from configuration (if specified and found) or falls back to a bundled default, then plays the sound at a configurable volume when requested. The service exposes SetEnabled and SetVolume for simple runtime tuning, and PlayAsync for normal operation or PlayTestAsync for QA scenarios where playback should occur regardless of the Enabled flag. Internally it uses a semaphore to serialize concurrent playback, and a 10-second timeout to prevent a stuck caller if the sound does not finish.
|
||||
|
||||
## Remarks
|
||||
The class isolates all concerns around audio playback: path resolution, volume handling, concurrency, and fault tolerance. By hiding these details behind a single service, higher-level notification logic can simply request a sound without worrying about file presence, logging, or synchronization. The design anticipates environments where a sound file might be missing or playback might stall, and it ensures resources are released and the system remains responsive.
|
||||
|
||||
## Notes
|
||||
- Silent fallback if a sound file cannot be found; production environments should ensure the asset exists if audible alerts are required.
|
||||
- The PlaybackFinished event and the 10-second timeout guard the system against hangs; the lock may be released before the sound finishes, which means subsequent playback requests can start while a prior one is still playing.
|
||||
- PlayAsync respects the Enabled flag, while PlayTestAsync allows testing the sound regardless of Enabled.
|
||||
@@ -0,0 +1,46 @@
|
||||
# OutgoingAttachment
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/OutgoingAttachment.cs`
|
||||
> **Kind:** record
|
||||
|
||||
```csharp
|
||||
public sealed record OutgoingAttachment(
|
||||
Stream Stream,
|
||||
string FileName,
|
||||
string? DeclaredKind = null,
|
||||
string? EncryptedPreview = null)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default |
|
||||
|-----------|------|---------|
|
||||
| `Stream` | `Stream` | — |
|
||||
| `FileName` | `string` | — |
|
||||
| `DeclaredKind` | `string?` | `null` |
|
||||
| `EncryptedPreview` | `string?` | `null` |
|
||||
|
||||
|
||||
OutgoingAttachment is a transport object that represents a single file to upload as part of a message. It bundles the data Stream and FileName, and optionally carries DeclaredKind and EncryptedPreview for encrypted channels, while non-encrypted channels typically set only Stream and FileName.
|
||||
|
||||
## Remarks
|
||||
OutgoingAttachment serves as a compact, immutable data carrier that travels through the sending pipeline. As a record, it uses value-based equality which helps comparisons and deduplication when attachments are tracked across requests. It also clarifies ownership: the record does not manage the lifetime of the underlying Stream; callers are responsible for opening and disposing streams as appropriate.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
using System.IO;
|
||||
|
||||
// Normal channel usage: only Stream and FileName are provided
|
||||
var data = new byte[] { 0x01, 0x02, 0x03 };
|
||||
var stream = new MemoryStream(data);
|
||||
var attachment = new OutgoingAttachment(stream, "data.bin");
|
||||
|
||||
// End-to-end encrypted channel usage: DeclaredKind and EncryptedPreview are set
|
||||
var ciphertext = new MemoryStream(new byte[] { 0xAA, 0xBB, 0xCC });
|
||||
var asciiPreview = @"ASCII_ART_PREVIEW";
|
||||
var encryptedAttachment = new OutgoingAttachment(ciphertext, "image.png", "image", asciiPreview);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The lifetime of the underlying Stream is not managed by OutgoingAttachment; the caller must ensure the stream is disposed when appropriate.
|
||||
- DeclaredKind and EncryptedPreview are intended for encrypted channels; in normal channels these values are typically null.
|
||||
@@ -0,0 +1,24 @@
|
||||
# PathSetup
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/PathSetup.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class PathSetup
|
||||
```
|
||||
|
||||
|
||||
PathSetup is a cross-platform helper that ensures the application's directory is present on the system PATH, enabling commands like echohub to be run from any terminal session without specifying the full path. EnsureOnPath checks for the directory and, if missing, updates PATH in a platform-appropriate way: Windows updates the user PATH; Unix-like systems append an export line to common shell profile files.
|
||||
|
||||
## Remarks
|
||||
By centralizing PATH manipulation, this abstraction reduces code duplication and the risk of divergent PATH states across platforms. It uses a lightweight, best-effort approach and logs outcomes to aid diagnostics when PATH updates fail or are skipped. The addition is clearly marked by a PathMarker to avoid duplicating lines in shell profiles.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
PathSetup.EnsureOnPath();
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The method swallows exceptions and logs at debug level, so callers should not rely on exceptions to signal failure.
|
||||
- Unix updates affect the user's shell environment; new terminal sessions are typically required to observe changes.
|
||||
- Windows updates are done at the per-user level; system-wide PATH is not modified.
|
||||
@@ -0,0 +1,30 @@
|
||||
# RoomKeyProtector
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/RoomKeyProtector.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class RoomKeyProtector
|
||||
```
|
||||
|
||||
|
||||
Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with 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.
|
||||
|
||||
The RoomKeyProtector class provides a single API surface to protect and unprotect per-user room keys across platforms. The Protect method returns a string suitable for storage in the config, automatically selecting the appropriate protection mechanism for the current OS (DPAPI on Windows, file-based AES-GCM on others). TryUnprotect decodes a stored value back into a room key, reporting whether the value was a legacy (unencrypted) entry and whether the decryption succeeded. The implementation intentionally hides platform differences behind a consistent interface, so callers can persist and reload keys without worrying about the underlying cryptosystem.
|
||||
|
||||
The constructor accepts a directory that holds the master key file and an optional flag to override the OS-provided protection path (useful for tests). The key file path is derived from the directory by appending the fixed file name roomkeys.key. Key loading is guarded by a small lock and the master key is cached after the first read. The Protect path prefixes the output to indicate how the data is protected ("dp1:" or "k1:").
|
||||
|
||||
The class ensures the room passphrase itself is never persisted, and it gracefully tolerates missing or unreadable key material by returning false from TryUnprotect (leaving the caller to prompt the user for action).
|
||||
|
||||
````csharp
|
||||
// Typical usage
|
||||
var protector = new RoomKeyProtector("/config");
|
||||
byte[] roomKey = new byte[32]; // obtain from a secure source
|
||||
string stored = protector.Protect(roomKey);
|
||||
|
||||
if (protector.TryUnprotect(stored, out var recovered, out bool wasLegacy))
|
||||
{
|
||||
// recovered contains the room key if the value was decryptable
|
||||
// wasLegacy is true only if the input was a legacy base64 key without a prefix
|
||||
}
|
||||
````
|
||||
@@ -0,0 +1,43 @@
|
||||
# RoomKeyStore
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/RoomKeyStore.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class RoomKeyStore
|
||||
```
|
||||
|
||||
|
||||
Holds and manages end-to-end encrypted room keys for a single client instance: it keeps a decrypted, in-memory cache for the active session and a per-server persisted, encrypted copy so users do not have to re-enter passphrases each launch. Use RoomKeyStore when you need a thread-safe local store that provides room keys to the runtime and ensures keys are encrypted at rest via RoomKeyProtector.
|
||||
|
||||
## Remarks
|
||||
RoomKeyStore links transient runtime state with the client's persisted configuration. It binds to a server (LoadForServer), loads that server's saved ChannelKeys (unprotecting them with RoomKeyProtector), and exposes methods to read, add, replace, or remove keys while persisting changes back to the SavedServer entry. It also records which channels are known to be encrypted so callers can avoid emitting plaintext into rooms without a cached key. The class performs a one-way upgrade of legacy unprotected entries to the protected format when possible and logs unreadable entries rather than failing.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var store = new RoomKeyStore();
|
||||
store.LoadForServer("https://chat.example.com");
|
||||
|
||||
// Generate and store a new room key for a channel
|
||||
byte[] newKey = RoomCrypto.GenerateRoomKey();
|
||||
store.StoreKey("#team-room", newKey);
|
||||
|
||||
// Retrieve a key for sending encrypted messages
|
||||
if (store.TryGetKey("#team-room", out var key))
|
||||
{
|
||||
// Use `key` with RoomCrypto API to encrypt message content
|
||||
}
|
||||
|
||||
// Accept an encrypted envelope and store the unwrapped key only if the KEK opens it
|
||||
string wrapped = "..."; // envelope string received
|
||||
byte[] kek = /* key-encryption-key */ new byte[RoomCrypto.KeySizeBytes];
|
||||
if (store.TryStoreFromEnvelope("#other-room", wrapped, kek))
|
||||
{
|
||||
// successfully unwrapped and cached
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Call LoadForServer(serverUrl) before persisting or retrieving server-scoped keys; the store clears and reinitializes its cache when bound to a server.
|
||||
- Legacy (plain/base64) saved entries are upgraded to the protector-backed format when possible; entries that cannot be unprotected are ignored and logged.
|
||||
- The class uses an internal lock for basic thread-safety of the in-memory cache; avoid holding returned keys while performing long synchronous work that might race with store mutations.
|
||||
@@ -0,0 +1,86 @@
|
||||
# UpdateBackupService.cs
|
||||
|
||||
> **Source:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||
|
||||
## Contents
|
||||
|
||||
- [BackupJsonContext](#backupjsoncontext)
|
||||
- [UpdateBackupService](#updatebackupservice)
|
||||
- [BackupInfo](#backupinfo)
|
||||
|
||||
---
|
||||
|
||||
## BackupJsonContext
|
||||
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
[System.Text.Json.Serialization.JsonSerializable(typeof(BackupInfo))]
|
||||
internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSerializerContext
|
||||
```
|
||||
|
||||
|
||||
BackupJsonContext is an internal partial class that provides the source-generated JSON serialization metadata for the BackupInfo type. It plugs into System.Text.Json’s source generator, enabling reflection-free serialization of BackupInfo when you configure a JsonSerializerOptions with this context.
|
||||
|
||||
## Remarks
|
||||
This symbol acts as the concrete carrier of serialization metadata for BackupInfo within the JSON pipeline of EchoHub’s client. By centralizing the generated type information in a single context, it keeps serialization concerns isolated from business logic and allows the type to evolve without scattering attributes across multiple call sites. The pattern here—one generated context per data contract—supports predictable performance improvements while preserving a clean, minimal public surface.
|
||||
|
||||
## Notes
|
||||
- The symbol is internal; it is intended for use within the containing assembly, not by external callers.
|
||||
- The class is generated and partial; do not edit it by hand, as changes will be overwritten by the source generator.
|
||||
- If you modify the BackupInfo shape, you must re-run code generation to keep the context in sync with the data contract.
|
||||
|
||||
---
|
||||
|
||||
## UpdateBackupService
|
||||
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public static class UpdateBackupService
|
||||
```
|
||||
|
||||
|
||||
Manages pre-update backups for the auto-updater and provides rollback support by snapshotting the running application prior to an update. Backups are stored under ~/.echohub/update-backup/ as backup.zip with a companion backup-info.json that records the version, application directory, and UTC timestamp. Use CreateBackup before applying an update; verify presence with BackupExists and inspect metadata with GetBackupInfo to drive a rollback if needed. The IsPostUpdate flag signals that a backup from a recent update exists, allowing startup logic to react accordingly.
|
||||
|
||||
---
|
||||
|
||||
## BackupInfo
|
||||
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||
> **Kind:** record
|
||||
|
||||
```csharp
|
||||
public record BackupInfo(
|
||||
string Version,
|
||||
string AppDirectory,
|
||||
DateTimeOffset CreatedAt)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default |
|
||||
|-----------|------|---------|
|
||||
| `Version` | `string` | — |
|
||||
| `AppDirectory` | `string` | — |
|
||||
| `CreatedAt` | `DateTimeOffset` | — |
|
||||
|
||||
|
||||
BackupInfo is a lightweight, value-like record that encapsulates metadata about a created backup. It carries the backup Version, the AppDirectory that was backed up, and the CreatedAt timestamp, enabling complete backup metadata to be passed around as a single unit.
|
||||
|
||||
## Remarks
|
||||
BackupInfo, being a record with positional parameters, is immutable and benefits from value-based equality. This makes it ideal as a canonical data carrier when the UpdateBackupService reports or persists backup information, or when UI/logging layers need to compare or display backup entries.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var backup = new BackupInfo(
|
||||
Version: "1.2.3",
|
||||
AppDirectory: "/opt/MyApp",
|
||||
CreatedAt: DateTimeOffset.UtcNow
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Records provide structural equality; two instances with the same Version, AppDirectory, and CreatedAt compare as equal.
|
||||
- CreatedAt uses DateTimeOffset to preserve offset information; prefer UTC (DateTimeOffset.UtcNow) when constructing backups to avoid timezone ambiguities.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,36 @@
|
||||
# UpdateChecker
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/UpdateChecker.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
public sealed class UpdateChecker : IDisposable
|
||||
```
|
||||
|
||||
|
||||
Checks for application updates in the background, presents a TUI confirmation dialog when a new version is available, and defers the actual download/extract/restart work until after the terminal UI has been shut down. Use this class when the host application runs a Terminal.Gui main loop and needs a safe way to offer in-place updates without deadlocking the console or performing heavy I/O while the TUI still owns the terminal.
|
||||
|
||||
## Remarks
|
||||
This class encapsulates polling and manual update checks via an internal Updater instance and marshals user interaction back onto the provided IApplication main loop using _app.Invoke. When the user confirms an update, UpdateChecker does not perform the network/download work immediately; instead it sets PendingUpdate to an awaitable callback (ApplyUpdateAsync), stores the selected version, and requests the TUI to stop. The host is expected to call PendingUpdate after the main loop exits and the console has been restored so the update process can safely run headless (the Updater's update flow may restart the process and call Environment.Exit).
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// During application startup
|
||||
var updateChecker = new UpdateChecker(app);
|
||||
updateChecker.Start(); // starts periodic checks in RELEASE builds
|
||||
|
||||
// ... run Terminal.Gui main loop ...
|
||||
|
||||
// After the main loop exits and the console is restored, run any pending update
|
||||
if (updateChecker.PendingUpdate != null)
|
||||
{
|
||||
await updateChecker.PendingUpdate();
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Start only activates the background poller in RELEASE builds (the Start method is no-op in non-RELEASE builds).
|
||||
- PendingUpdate is deliberately set to a Task-returning delegate and intended to be invoked by the host after the TUI has fully stopped; running it while the TUI still owns the console can deadlock the restart flow.
|
||||
- ApplyUpdateAsync attempts to create a pre-update backup with UpdateBackupService.CreateBackup; backup creation failures are logged and the update continues.
|
||||
- ApplyUpdateAsync sets Console.OutputEncoding = UTF8 but swallows exceptions (useful when stdout is redirected or non-interactive).
|
||||
- CurrentVersion reads the assembly version and falls back to "0.0.0" if unavailable.
|
||||
@@ -0,0 +1,19 @@
|
||||
# UserSession
|
||||
|
||||
> **File:** `src/EchoHub.Client/Services/UserSession.cs`
|
||||
> **Kind:** class
|
||||
|
||||
```csharp
|
||||
internal sealed class UserSession
|
||||
```
|
||||
|
||||
|
||||
Stores the current user's session state on the client, including username, online status, and an optional status message. Use this type as a lightweight, centralized container when you need to read or mutate the ephemeral session data for the active user, and call Reset to return all fields to their defaults (empty username, Online status, and no status message).
|
||||
|
||||
## Remarks
|
||||
Internally sealed and non-public, this class keeps the session representation stable within the client service layer and prevents inheritance. It relies on the UserStatus enum from the core models to express the user's current state consistently across the application.
|
||||
|
||||
## Notes
|
||||
- Not thread-safe by default; coordinate concurrent access if used from multiple threads.
|
||||
- Reset mutates state in place; if you require preserving data, capture it before calling Reset.
|
||||
- StatusMessage is nullable; null indicates that no message is provided.
|
||||
Reference in New Issue
Block a user