fix(chat): KeyNotFoundException dans HubConnectionManager.OnConnected

ChatUserNames n'etait jamais alimente (relique d'un SetUserName disparu),
ce qui faisait lever KeyNotFoundException a chaque connexion SignalR et
fermait immediatement la connexion cote client ("Déconnecté (The server
closed the connection...)" dans PostIt).

- IConnexionManager.OnConnected prend desormais le userName en parametre
- HubConnectionManager.OnConnected alimente ChatUserNames et ChatCxIds
- ChatHub.OnConnectedAsync passe le userName resolu depuis les claims

Inclut aussi les changements PostIt lies (bandeau de statut avec
commande d'action, handlers Reconnecting/Reconnected/Closed).
This commit is contained in:
Paul Schneider 2026-10-03 23:45:22 +01:00
commit 75bb2600b5
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
6 changed files with 70 additions and 16 deletions

View file

@ -18,6 +18,7 @@
TextWrapping="Wrap"
VerticalAlignment="Center"/>
<Button Content="{Binding CheckCommandLabel}"
Foreground="{Binding Foreground}"
VerticalAlignment="Center"
IsVisible="{Binding HasCheckCommand}"
Command="{Binding CheckCommand}"

View file

@ -148,6 +148,41 @@ namespace PostIt.ViewModels.Chat
await _connection.StartAsync();
IsConnected = true;
Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info, new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync()));
// WithAutomaticReconnect ne lève pas d'exception en cas de
// perte réseau : l'état passe par Reconnecting/Closed. Sans
// ces handlers, IsConnected resterait optimiste et Join()
// échouerait avec "connection is not active".
_connection.Reconnecting += error =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsConnected = false;
Status = new StatusNotice($"Connexion perdue ({error?.Message ?? "réseau"}), reconnexion...", StatusSeverity.Error,
new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync()));
});
return Task.CompletedTask;
};
_connection.Reconnected += _ =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsConnected = true;
Status = new StatusNotice("Reconnecté au Hub SignalR", StatusSeverity.Info,
new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync()));
});
return Task.CompletedTask;
};
_connection.Closed += error =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsConnected = false;
Status = new StatusNotice($"Déconnecté ({error?.Message ?? "fin de connexion"})", StatusSeverity.Info,
new RelayCheckCommand("Connecter", async () => await ConnectAsync()));
});
return Task.CompletedTask;
};
}
catch (Exception ex)
{
@ -156,15 +191,34 @@ namespace PostIt.ViewModels.Chat
}
}
private async Task CheckConnectionAsync()
private Task CheckConnectionAsync()
{
throw new NotImplementedException();
var state = _connection?.State.ToString() ?? "aucune connexion";
Messages.Add($"État de la connexion : {state}");
return Task.CompletedTask;
}
private async Task JoinRoomAsync()
{
if (_connection == null || string.IsNullOrWhiteSpace(CurrentRoom)) return;
// InvokeCoreAsync exige une connexion active : avec la
// reconnexion automatique, l'état peut être Reconnecting au
// moment de l'appel. On tente une (re)connexion si nécessaire.
if (_connection.State != HubConnectionState.Connected)
{
Messages.Add($"Connexion non active ({_connection.State}), tentative de reconnexion...");
try
{
await _connection.StartAsync();
}
catch (Exception ex)
{
Messages.Add($"Reconnexion impossible : {ex.Message}");
return;
}
}
try
{
// Appel de la méthode publique 'Join' du Hub (renvoie un ChatRoomInfo)

View file

@ -12,7 +12,7 @@ public sealed class StatusNotice
public string BorderBrush { get; }
public string Foreground { get; }
public bool HasCheckCommand { get; }
public bool HasCheckCommand { get => CheckCommand != null; }
public ICommand? CheckCommand { get; }
@ -29,12 +29,9 @@ public sealed class StatusNotice
StatusSeverity.Warning => ("~", "#7C4A03", "#E6A700", "#eaeaea"),
_ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
};
HasCheckCommand = checkCommand != null;
CheckCommand = checkCommand;
CheckCommandLabel = HasCheckCommand ? "Check" :
checkCommand == null ?
string.Empty :
checkCommand is IWithLabel labeled ? labeled.Label : string.Empty;
CheckCommandLabel = HasCheckCommand ?
(checkCommand is IWithLabel labeled ? labeled.Label : "Check") : string.Empty;
}
public static StatusNotice Info(string message, ICommand? checkCommand = null) => new(message, StatusSeverity.Info, checkCommand);

View file

@ -90,7 +90,7 @@ namespace Yavsc.Server.Hubs
{
await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupFollowingPrefix + uid);
}
_cxManager.OnConnected(Context.ConnectionId, isCop);
_cxManager.OnConnected(Context.ConnectionId, userName, isCop);
await base.OnConnectedAsync();
}

View file

@ -4,7 +4,7 @@ namespace Yavsc.Services
{
public interface IConnexionManager {
string GetUserName (string cxId);
void OnConnected(string cxId, bool isCop);
void OnConnected(string cxId, string userName, bool isCop);
bool IsConnected(string candidate);
void OnDisconnected (string cxId);

View file

@ -53,13 +53,15 @@ namespace Yavsc.Services
_localizer = stringLocFactory.Create(typeof(HubConnectionManager));
}
// Username must have been set before calling this method.
public void OnConnected(string cxId, bool isCop)
public void OnConnected(string cxId, string userName, bool isCop)
{
var username = ChatUserNames[cxId];
if (!IsConnected(username))
ChatRoomPresence[username] = new List<string>();
_isCop[username] = isCop;
ChatUserNames[cxId] = userName;
ChatCxIds.AddOrUpdate(userName,
_ => new List<string> { cxId },
(_, list) => { lock (list) { if (!list.Contains(cxId)) list.Add(cxId); } return list; });
if (!IsConnected(userName))
ChatRoomPresence[userName] = new List<string>();
_isCop[userName] = isCop;
}
public bool IsConnected(string candidate)