diff --git a/src/EchoHub.Client/AppOrchestrator.cs b/src/EchoHub.Client/AppOrchestrator.cs index 829426b..9d2544b 100644 --- a/src/EchoHub.Client/AppOrchestrator.cs +++ b/src/EchoHub.Client/AppOrchestrator.cs @@ -28,6 +28,7 @@ public sealed class AppOrchestrator : IDisposable private UserStatus _currentStatus = UserStatus.Online; private string? _currentStatusMessage; private string _currentUsername = string.Empty; + private readonly HashSet _joinedChannels = []; private bool IsConnected => _connection is not null && _connection.IsConnected; private bool IsAuthenticated => _apiClient is not null; @@ -161,6 +162,7 @@ public sealed class AppOrchestrator : IDisposable try { + _joinedChannels.Add(channelName); var history = await _connection!.JoinChannelAsync(channelName); InvokeUI(() => { @@ -185,6 +187,7 @@ public sealed class AppOrchestrator : IDisposable try { await _connection!.LeaveChannelAsync(channel); + _joinedChannels.Remove(channel); InvokeUI(() => _mainWindow.AddSystemMessage(channel, $"You left #{channel}")); } catch (Exception ex) @@ -294,6 +297,8 @@ public sealed class AppOrchestrator : IDisposable _mainWindow.UpdateStatusBar("Connected"); }); + _joinedChannels.Clear(); + _joinedChannels.Add(HubConstants.DefaultChannel); await _connection.JoinChannelAsync(HubConstants.DefaultChannel); InvokeUI(() => _mainWindow.SwitchToChannel(HubConstants.DefaultChannel)); @@ -330,6 +335,7 @@ public sealed class AppOrchestrator : IDisposable _apiClient?.Dispose(); _apiClient = null; + _joinedChannels.Clear(); InvokeUI(() => { @@ -377,11 +383,12 @@ public sealed class AppOrchestrator : IDisposable RunAsync(async () => { - await _connection!.JoinChannelAsync(channelName); + if (_joinedChannels.Add(channelName)) + await _connection!.JoinChannelAsync(channelName); try { - var history = await _connection.GetHistoryAsync(channelName); + var history = await _connection!.GetHistoryAsync(channelName); InvokeUI(() => _mainWindow.LoadHistory(channelName, history)); } catch @@ -530,6 +537,7 @@ public sealed class AppOrchestrator : IDisposable var channel = await _apiClient!.CreateChannelAsync(result.Name, result.Topic); if (channel is null) return; + _joinedChannels.Add(channel.Name); var history = await _connection!.JoinChannelAsync(channel.Name); // Refresh the channel list @@ -579,13 +587,19 @@ public sealed class AppOrchestrator : IDisposable connection.OnReconnected += () => { - var channels = _mainWindow.GetChannelNames(); + // Server-side state is lost on reconnect — rejoin all channels + var channels = _joinedChannels.ToList(); if (channels.Count == 0) return; + _joinedChannels.Clear(); + RunAsync(async () => { foreach (var channel in channels) + { + _joinedChannels.Add(channel); await _connection!.JoinChannelAsync(channel); + } Log.Information("Rejoined {Count} channel(s) after reconnect", channels.Count); }, "Failed to rejoin channels after reconnect"); diff --git a/src/EchoHub.Client/UI/MainWindow.cs b/src/EchoHub.Client/UI/MainWindow.cs index ff2eadd..3cebdd3 100644 --- a/src/EchoHub.Client/UI/MainWindow.cs +++ b/src/EchoHub.Client/UI/MainWindow.cs @@ -544,18 +544,12 @@ public sealed class MainWindow : Runnable } /// - /// Load historical messages into a channel (prepend). + /// Load historical messages into a channel, replacing any existing messages. /// public void LoadHistory(string channelName, List messages) { - if (!_channelMessages.TryGetValue(channelName, out var existing)) - { - existing = []; - _channelMessages[channelName] = existing; - } - var formatted = messages.SelectMany(FormatMessage).ToList(); - existing.InsertRange(0, formatted); + _channelMessages[channelName] = formatted; if (channelName == _currentChannel) { diff --git a/src/EchoHub.Server/Hubs/ChatHub.cs b/src/EchoHub.Server/Hubs/ChatHub.cs index 31d5f44..c9c42de 100644 --- a/src/EchoHub.Server/Hubs/ChatHub.cs +++ b/src/EchoHub.Server/Hubs/ChatHub.cs @@ -112,12 +112,14 @@ public class ChatHub(EchoHubDbContext db, ILogger logger, PresenceTrack return []; } - presenceTracker.JoinChannel(CurrentUsername, channelName); + var isNewJoin = presenceTracker.JoinChannel(CurrentUsername, channelName); - await Groups.AddToGroupAsync(Context.ConnectionId, channelName); - await Clients.OthersInGroup(channelName).UserJoined(channelName, CurrentUsername); - - logger.LogInformation("{User} joined channel '{Channel}'", CurrentUsername, channelName); + if (isNewJoin) + { + await Groups.AddToGroupAsync(Context.ConnectionId, channelName); + await Clients.OthersInGroup(channelName).UserJoined(channelName, CurrentUsername); + logger.LogInformation("{User} joined channel '{Channel}'", CurrentUsername, channelName); + } var history = await GetChannelHistory(channelName, HubConstants.DefaultHistoryCount); return history; @@ -293,11 +295,10 @@ public class ChatHub(EchoHubDbContext db, ILogger logger, PresenceTrack statusMessage); var channels = presenceTracker.GetChannelsForUser(CurrentUsername); + var connections = presenceTracker.GetConnectionsInChannels(channels); - foreach (var channel in channels) - { - await Clients.Group(channel).UserStatusChanged(presence); - } + if (connections.Count > 0) + await Clients.Clients(connections).UserStatusChanged(presence); } catch (Exception ex) { diff --git a/src/EchoHub.Server/Services/PresenceTracker.cs b/src/EchoHub.Server/Services/PresenceTracker.cs index 1da4ecd..eccb9f4 100644 --- a/src/EchoHub.Server/Services/PresenceTracker.cs +++ b/src/EchoHub.Server/Services/PresenceTracker.cs @@ -50,7 +50,10 @@ public class PresenceTracker return username; } - public void JoinChannel(string username, string channelName) + /// + /// Returns true if this is a new join, false if the user was already in the channel. + /// + public bool JoinChannel(string username, string channelName) { lock (_lock) { @@ -60,7 +63,7 @@ public class PresenceTracker _userChannels[username] = channels; } - channels.Add(channelName); + return channels.Add(channelName); } } @@ -106,6 +109,34 @@ public class PresenceTracker return []; } + /// + /// Get all unique connection IDs for users who share any of the given channels. + /// + public List GetConnectionsInChannels(List channels) + { + lock (_lock) + { + var usernames = new HashSet(); + foreach (var channel in channels) + { + foreach (var (username, userChannels) in _userChannels) + { + if (userChannels.Contains(channel)) + usernames.Add(username); + } + } + + var connections = new List(); + foreach (var username in usernames) + { + if (_userConnections.TryGetValue(username, out var conns)) + connections.AddRange(conns); + } + + return connections; + } + } + public bool IsOnline(string username) { return _userConnections.TryGetValue(username, out var connections) && connections.Count > 0;