diff --git a/src/EchoHub.Client/AppOrchestrator.cs b/src/EchoHub.Client/AppOrchestrator.cs index 3c9c23d..a0fd696 100644 --- a/src/EchoHub.Client/AppOrchestrator.cs +++ b/src/EchoHub.Client/AppOrchestrator.cs @@ -414,6 +414,18 @@ public sealed class AppOrchestrator : IDisposable FetchAndUpdateOnlineUsers(); SaveServerToConfig(result); + + // Check for newer version in the background + _ = Task.Run(async () => + { + var newVersion = await UpdateChecker.CheckForUpdateAsync(); + if (newVersion is not null) + { + InvokeUI(() => _mainWindow.AddSystemMessage( + HubConstants.DefaultChannel, + $"A new version of EchoHub is available: v{newVersion} (current: v{MainWindow.AppVersion}). Visit https://github.com/HueByte/EchoHub/releases")); + } + }); }, "Connection failed", "Connect"); } diff --git a/src/EchoHub.Client/Services/UpdateChecker.cs b/src/EchoHub.Client/Services/UpdateChecker.cs new file mode 100644 index 0000000..d73c9df --- /dev/null +++ b/src/EchoHub.Client/Services/UpdateChecker.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; + +namespace EchoHub.Client.Services; + +public static class UpdateChecker +{ + private static readonly Uri ReleaseUrl = + new("https://api.github.com/repos/HueByte/EchoHub/releases/latest"); + + /// + /// Checks GitHub for a newer release. Returns the new version string if one exists, or null. + /// Never throws — all errors are silently swallowed. + /// + public static async Task CheckForUpdateAsync() + { + try + { + using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + http.DefaultRequestHeaders.UserAgent.ParseAdd("EchoHub-Client"); + + var release = await http.GetFromJsonAsync(ReleaseUrl); + if (release?.TagName is null) + return null; + + var tag = release.TagName.TrimStart('v', 'V'); + if (!Version.TryParse(tag, out var latest)) + return null; + + var currentStr = typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3); + if (currentStr is null || !Version.TryParse(currentStr, out var current)) + return null; + + return latest > current ? tag : null; + } + catch + { + return null; + } + } + + private sealed class GitHubRelease + { + [JsonPropertyName("tag_name")] + public string? TagName { get; set; } + } +} diff --git a/src/EchoHub.Client/UI/MainWindow.cs b/src/EchoHub.Client/UI/MainWindow.cs index 8f7d5c9..9fc8b14 100644 --- a/src/EchoHub.Client/UI/MainWindow.cs +++ b/src/EchoHub.Client/UI/MainWindow.cs @@ -34,7 +34,7 @@ public sealed class MainWindow : Runnable private const int UsersPanelWidth = 22; private static readonly Key F2Key = Key.F2; - private static readonly string AppVersion = + internal static readonly string AppVersion = typeof(MainWindow).Assembly.GetName().Version?.ToString(3) ?? "?"; // Cached Key constants — compare via .KeyCode to avoid Key.Equals (which also checks Handled) @@ -479,19 +479,33 @@ public sealed class MainWindow : Runnable /// public void AddSystemMessage(string channelName, string text) { - var time = DateTimeOffset.Now.ToString("HH:mm"); - var segments = new List - { - new($"[{time}] ", ChatColors.TimestampAttr), - new($"** {text}", ChatColors.SystemAttr) - }; - if (!_channelMessages.TryGetValue(channelName, out var messages)) { messages = []; _channelMessages[channelName] = messages; } - messages.Add(new ChatLine(segments)); + + var time = DateTimeOffset.Now.ToString("HH:mm"); + var textLines = text.Split('\n'); + + // First line gets timestamp prefix + messages.Add(new ChatLine( + [ + new($"[{time}] ", ChatColors.TimestampAttr), + new($"** {textLines[0].TrimEnd('\r')}", ChatColors.SystemAttr) + ])); + + // Continuation lines are indented to align + var indent = new string(' ', $"[{time}] ** ".Length); + for (int i = 1; i < textLines.Length; i++) + { + var line = textLines[i].TrimEnd('\r'); + if (string.IsNullOrWhiteSpace(line)) continue; + messages.Add(new ChatLine( + [ + new($"{indent}{line}", ChatColors.SystemAttr) + ])); + } if (channelName == _currentChannel) {