feat(chat): envoi de messages dans un salon joint

- DTO ChatMessage (Name/Room/Message) pour l'evenement ReceiveMessage
- ChatViewModel : MessageToSend, HasJoinedRoom, handler ReceiveMessage,
  SendMessageCommand (CanExecute: connecte + salon joint + texte non vide)
- ChatPage : TextBox de saisie (Enter envoie) + bouton Envoyer,
  StatusBar repositionnee en Grid.Row=3
This commit is contained in:
Paul Schneider 2026-10-03 23:55:03 +01:00
commit 51e445b5ba
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
3 changed files with 81 additions and 2 deletions

View file

@ -0,0 +1,15 @@
namespace PostIt.ViewModels.Chat
{
/// <summary>
/// DTO de message de salon, calqué sur la charge envoyée par
/// <c>ChatHub.Send</c> côté serveur ({ Name, Room, Message }).
/// Le protocole JSON de SignalR sérialise en camelCase ; la
/// désérialisation est insensible à la casse par défaut.
/// </summary>
public class ChatMessage
{
public string? Name { get; set; }
public string? Room { get; set; }
public string? Message { get; set; }
}
}

View file

@ -45,8 +45,17 @@ namespace PostIt.ViewModels.Chat
public partial StatusNotice Status { get; set; } public partial StatusNotice Status { get; set; }
[ObservableProperty] [ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
public partial bool IsConnected { get; set; } = false; public partial bool IsConnected { get; set; } = false;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
public partial bool HasJoinedRoom { get; set; } = false;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
public partial string MessageToSend { get; set; } = "";
// Listes dynamiques pour l'UI // Listes dynamiques pour l'UI
public ObservableCollection<string> Messages { get; } = new(); public ObservableCollection<string> Messages { get; } = new();
public ObservableCollection<string> ConnectedUsers { get; } = new(); public ObservableCollection<string> ConnectedUsers { get; } = new();
@ -58,6 +67,28 @@ namespace PostIt.ViewModels.Chat
[RelayCommand] [RelayCommand]
public async Task JoinRoomCommand() => await JoinRoomAsync(); public async Task JoinRoomCommand() => await JoinRoomAsync();
// Le serveur (ChatHub.Send) refuse tout message d'un utilisateur
// qui n'a pas joint le salon ("NoJoinNoSend") : on garde la
// commande inactive tant que le Join n'a pas abouti.
private bool CanSendMessage() =>
IsConnected && HasJoinedRoom && !string.IsNullOrWhiteSpace(MessageToSend);
[RelayCommand(CanExecute = nameof(CanSendMessage))]
private async Task SendMessageAsync()
{
if (_connection == null) return;
var text = MessageToSend.Trim();
try
{
await _connection.InvokeAsync("Send", CurrentRoom, text);
MessageToSend = string.Empty;
}
catch (Exception ex)
{
Messages.Add($"Erreur d'envoi : {ex.Message}");
}
}
public override bool CanNavigateNext { get; protected set; } = false; public override bool CanNavigateNext { get; protected set; } = false;
public override bool CanNavigatePrevious { get; protected set; } = true; public override bool CanNavigatePrevious { get; protected set; } = true;
@ -70,6 +101,7 @@ namespace PostIt.ViewModels.Chat
_connection = null; _connection = null;
} }
IsConnected = false; IsConnected = false;
HasJoinedRoom = false;
Status = new StatusNotice("Déconnecté", StatusSeverity.Info, Status = new StatusNotice("Déconnecté", StatusSeverity.Info,
new RelayCheckCommand("Connecter", async () => await ConnectAsync())); new RelayCheckCommand("Connecter", async () => await ConnectAsync()));
} }
@ -140,11 +172,19 @@ namespace PostIt.ViewModels.Chat
Avalonia.Threading.Dispatcher.UIThread.Post(() => Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{ {
Messages.Add($"Salon rejoint avec succès : {roomInfo.Name}"); Messages.Add($"Salon rejoint avec succès : {roomInfo.Name}");
HasJoinedRoom = true;
ConnectedUsers.Clear(); ConnectedUsers.Clear();
foreach (var u in roomInfo.Users) ConnectedUsers.Add(u); foreach (var u in roomInfo.Users) ConnectedUsers.Add(u);
}); });
}); });
// 4. Réception des messages du salon (diffusés par ChatHub.Send)
_connection.On<ChatMessage>("ReceiveMessage", (msg) =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
Messages.Add($"[{msg.Room}] {msg.Name} : {msg.Message}"));
});
await _connection.StartAsync(); await _connection.StartAsync();
IsConnected = true; IsConnected = true;
Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info, new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync())); Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info, new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync()));

View file

@ -16,7 +16,7 @@
<vm:ChatViewModel /> <vm:ChatViewModel />
</Design.DataContext> </Design.DataContext>
<Grid Margin="10" RowDefinitions="Auto, *, Auto"> <Grid Margin="10" RowDefinitions="Auto, *, Auto, Auto">
<Grid <Grid
Grid.Row="1" Grid.Row="1"
@ -71,9 +71,33 @@
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
<!-- Saisie et envoi d'un message dans le salon joint -->
<Grid
Grid.Row="2"
Margin="0,0,0,10"
ColumnDefinitions="*, Auto">
<TextBox
Grid.Column="0"
VerticalAlignment="Center"
AcceptsReturn="False"
IsEnabled="{Binding HasJoinedRoom}"
PlaceholderText="Votre message..."
Text="{Binding MessageToSend}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding SendMessageCommand}" Gesture="Enter" />
</TextBox.KeyBindings>
</TextBox>
<Button
Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
Command="{Binding SendMessageCommand}"
Content="Envoyer" />
</Grid>
<postitControls:StatusBar <postitControls:StatusBar
x:Name="StatusBar" x:Name="StatusBar"
Grid.Row="4" Grid.Row="3"
DataContext="{Binding Status}" /> DataContext="{Binding Status}" />
</Grid> </Grid>