Merge pull request #73 from HueByte/dev

Dev
This commit is contained in:
Hue
2026-07-23 23:07:58 +02:00
committed by GitHub
11 changed files with 106 additions and 23 deletions
+1
View File
@@ -22,6 +22,7 @@
".dev/**",
"docs/_site/**",
"docs/api/**",
"docs/auriondocs/**",
"node_modules/**",
"**/bin/**",
"**/obj/**",
+1
View File
@@ -4,6 +4,7 @@ Release history for EchoHub.
## Releases
- [v0.2.17](v0.2.17.md) - Server Version Reporting & IRC Multi-Line Fix
- [v0.2.16](v0.2.16.md) - Periodic Server-Stats Report, Upload & Moderation Logging & Quieter Connection Logs
- [v0.2.15](v0.2.15.md) - Invite Codes, Data Export & Deletion, /me, /banner, Replies, Open Images In Browser & IRC Image Links
- [v0.2.14](v0.2.14.md) - Clipboard Image & Multi-File Paste, E2E Room Unlock Fixes, Encrypted Key Cache & IRC Gateway Polish
+2
View File
@@ -1,5 +1,7 @@
- name: Overview
href: index.md
- name: v0.2.17
href: v0.2.17.md
- name: v0.2.16
href: v0.2.16.md
- name: v0.2.15
+9
View File
@@ -0,0 +1,9 @@
# v0.2.17
## New Features
- **Server version reporting** — `GET /api/server/info` now returns the server version. The client fetches it on connect and shows a warning dialog when the server and client versions don't match, with the option to continue or disconnect.
## Bug Fixes
- **IRC multi-line messages** — sending a message with line breaks from the TUI now correctly delivers each line as a separate PRIVMSG to IRC clients. Previously newlines were embedded raw in the IRC frame, so IRC clients silently dropped everything after the first line — most visibly, a link on its own line disappeared while its embed preview still showed.
+1 -1
View File
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.2.16</Version>
<Version>0.2.17</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
+16
View File
@@ -1373,6 +1373,22 @@ public sealed class AppOrchestrator : IDisposable
_mainWindow.FocusInput();
FetchAndUpdateOnlineUsers();
});
if (result.ServerInfo is not null && !string.IsNullOrEmpty(result.ServerInfo.Version)
&& result.ServerInfo.Version != UpdateChecker.CurrentVersion)
{
InvokeUI(() =>
{
var choice = MessageBox.Query(_app, "Version Mismatch",
$"Server version {result.ServerInfo.Version} does not match client version {UpdateChecker.CurrentVersion}.\n\n" +
"Some features may not work correctly. Consider updating both to the same version.",
"Continue", "Disconnect");
if (choice == 1)
HandleDisconnect();
});
}
SaveServerToConfig(dialogResult);
}, "Connection failed", "Connect");
}
@@ -15,7 +15,8 @@ namespace EchoHub.Client.Services;
internal record ConnectResult(
LoginResponse Login,
List<ChannelDto> Channels,
Dictionary<string, List<MessageDto>> Histories);
Dictionary<string, List<MessageDto>> Histories,
ServerStatusDto? ServerInfo = null);
/// <summary>
/// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking.
@@ -67,6 +68,17 @@ internal sealed class ConnectionManager : IAsyncDisposable
try
{
onStatus("Fetching server info...");
ServerStatusDto? serverInfo = null;
try
{
serverInfo = await _apiClient.GetServerInfoAsync();
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to fetch server info");
}
onStatus("Authenticating...");
LoginResponse loginResponse;
@@ -165,7 +177,7 @@ internal sealed class ConnectionManager : IAsyncDisposable
}
onStatus("Connected");
return new ConnectResult(loginResponse, channels, histories);
return new ConnectResult(loginResponse, channels, histories, serverInfo);
}
catch
{
+2 -1
View File
@@ -5,6 +5,7 @@ public record ServerStatusDto(
string? Description,
int OnlineUsers,
int TotalChannels,
string RegistrationMode = "open");
string RegistrationMode = "open",
string Version = "0.0.0");
public record EncryptionKeyResponse(string Key);
+12 -5
View File
@@ -138,18 +138,24 @@ public static class IrcMessageFormatter
}
/// <summary>
/// Split a message into chunks of approximately maxBytes (UTF-8), at word boundaries.
/// Split a message into chunks of approximately maxBytes (UTF-8), at line and word boundaries.
/// </summary>
public static List<string> SplitMessage(string content, int maxBytes)
{
if (Encoding.UTF8.GetByteCount(content) <= maxBytes)
return [content];
var chunks = new List<string>();
foreach (var line in content.Split('\n'))
{
if (Encoding.UTF8.GetByteCount(line) <= maxBytes)
{
chunks.Add(line);
continue;
}
var current = new StringBuilder();
var currentBytes = 0;
foreach (var word in content.Split(' '))
foreach (var word in line.Split(' '))
{
var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
@@ -166,6 +172,7 @@ public static class IrcMessageFormatter
if (current.Length > 0)
chunks.Add(current.ToString().TrimEnd());
}
return chunks;
}
@@ -1,3 +1,4 @@
using System.Reflection;
using System.Security.Claims;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
@@ -38,12 +39,15 @@ public class ServerController : ControllerBase
_ => "open",
};
var version = typeof(ServerController).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
var status = new ServerStatusDto(
_config["Server:Name"] ?? "EchoHub Server",
_config["Server:Description"],
userCount,
channelCount,
registrationMode);
registrationMode,
version);
return Ok(status);
}
@@ -242,6 +242,36 @@ public class IrcMessageFormatterTests
Assert.Equal("", chunks[0]);
}
[Fact]
public void FormatMessage_UrlOnOwnLine_EmitsUrlAsItsOwnPrivmsg()
{
// Regression: multi-line content used to go out as ONE line with a raw \n
// embedded. IRC clients drop everything after the newline as a malformed
// frame, so a URL on its own line silently vanished while the embed lines
// (separate, valid PRIVMSGs) still rendered.
var embeds = new List<EmbedDto>
{
new("Example Site", "Page Title", "Description", null, "https://example.com")
};
var msg = CreateTextMessage("check this out\nhttps://example.com", embeds: embeds);
var lines = IrcMessageFormatter.FormatMessage(msg);
Assert.Contains(lines, l => l.EndsWith("PRIVMSG #general :check this out"));
Assert.Contains(lines, l => l.EndsWith("PRIVMSG #general :https://example.com"));
Assert.All(lines, l => Assert.DoesNotContain('\n', l));
Assert.All(lines, l => Assert.DoesNotContain('\r', l));
}
[Fact]
public void SplitMessage_MultiLineContent_SplitsOnNewlines()
{
var chunks = IrcMessageFormatter.SplitMessage("line one\nhttps://example.com/page", 400);
Assert.Equal(2, chunks.Count);
Assert.Equal("line one", chunks[0]);
Assert.Equal("https://example.com/page", chunks[1]);
}
[Fact]
public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
{