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/**", ".dev/**",
"docs/_site/**", "docs/_site/**",
"docs/api/**", "docs/api/**",
"docs/auriondocs/**",
"node_modules/**", "node_modules/**",
"**/bin/**", "**/bin/**",
"**/obj/**", "**/obj/**",
+1
View File
@@ -4,6 +4,7 @@ Release history for EchoHub.
## Releases ## 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.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.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 - [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 - name: Overview
href: index.md href: index.md
- name: v0.2.17
href: v0.2.17.md
- name: v0.2.16 - name: v0.2.16
href: v0.2.16.md href: v0.2.16.md
- name: v0.2.15 - 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> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.2.16</Version> <Version>0.2.17</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
+16
View File
@@ -1373,6 +1373,22 @@ public sealed class AppOrchestrator : IDisposable
_mainWindow.FocusInput(); _mainWindow.FocusInput();
FetchAndUpdateOnlineUsers(); 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); SaveServerToConfig(dialogResult);
}, "Connection failed", "Connect"); }, "Connection failed", "Connect");
} }
@@ -15,7 +15,8 @@ namespace EchoHub.Client.Services;
internal record ConnectResult( internal record ConnectResult(
LoginResponse Login, LoginResponse Login,
List<ChannelDto> Channels, List<ChannelDto> Channels,
Dictionary<string, List<MessageDto>> Histories); Dictionary<string, List<MessageDto>> Histories,
ServerStatusDto? ServerInfo = null);
/// <summary> /// <summary>
/// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking. /// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking.
@@ -67,6 +68,17 @@ internal sealed class ConnectionManager : IAsyncDisposable
try 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..."); onStatus("Authenticating...");
LoginResponse loginResponse; LoginResponse loginResponse;
@@ -165,7 +177,7 @@ internal sealed class ConnectionManager : IAsyncDisposable
} }
onStatus("Connected"); onStatus("Connected");
return new ConnectResult(loginResponse, channels, histories); return new ConnectResult(loginResponse, channels, histories, serverInfo);
} }
catch catch
{ {
+2 -1
View File
@@ -5,6 +5,7 @@ public record ServerStatusDto(
string? Description, string? Description,
int OnlineUsers, int OnlineUsers,
int TotalChannels, int TotalChannels,
string RegistrationMode = "open"); string RegistrationMode = "open",
string Version = "0.0.0");
public record EncryptionKeyResponse(string Key); public record EncryptionKeyResponse(string Key);
+12 -5
View File
@@ -138,18 +138,24 @@ public static class IrcMessageFormatter
} }
/// <summary> /// <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> /// </summary>
public static List<string> SplitMessage(string content, int maxBytes) public static List<string> SplitMessage(string content, int maxBytes)
{ {
if (Encoding.UTF8.GetByteCount(content) <= maxBytes)
return [content];
var chunks = new List<string>(); 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 current = new StringBuilder();
var currentBytes = 0; 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 var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
@@ -166,6 +172,7 @@ public static class IrcMessageFormatter
if (current.Length > 0) if (current.Length > 0)
chunks.Add(current.ToString().TrimEnd()); chunks.Add(current.ToString().TrimEnd());
}
return chunks; return chunks;
} }
@@ -1,3 +1,4 @@
using System.Reflection;
using System.Security.Claims; using System.Security.Claims;
using EchoHub.Core.DTOs; using EchoHub.Core.DTOs;
using EchoHub.Core.Models; using EchoHub.Core.Models;
@@ -38,12 +39,15 @@ public class ServerController : ControllerBase
_ => "open", _ => "open",
}; };
var version = typeof(ServerController).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
var status = new ServerStatusDto( var status = new ServerStatusDto(
_config["Server:Name"] ?? "EchoHub Server", _config["Server:Name"] ?? "EchoHub Server",
_config["Server:Description"], _config["Server:Description"],
userCount, userCount,
channelCount, channelCount,
registrationMode); registrationMode,
version);
return Ok(status); return Ok(status);
} }
@@ -242,6 +242,36 @@ public class IrcMessageFormatterTests
Assert.Equal("", chunks[0]); 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] [Fact]
public void SplitMessage_UnicodeContent_CountsUtf8Bytes() public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
{ {