diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props
index c9e77bf1d..386d8924e 100644
--- a/src/PostIt/Directory.Packages.props
+++ b/src/PostIt/Directory.Packages.props
@@ -17,12 +17,14 @@
+
+
diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
index 8793aae8a..46f0bc9bb 100644
--- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
+++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
@@ -1,6 +1,9 @@
-
+
diff --git a/src/PostIt/PostIt.Android/Resources/network_security_config.xml b/src/PostIt/PostIt.Android/Resources/network_security_config.xml
new file mode 100644
index 000000000..6d2c97559
--- /dev/null
+++ b/src/PostIt/PostIt.Android/Resources/network_security_config.xml
@@ -0,0 +1,6 @@
+
+
+
+ 10.0.2.2
+
+
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index 0c4f0a7ca..4769fedf3 100644
--- a/src/PostIt/PostIt/App.axaml.cs
+++ b/src/PostIt/PostIt/App.axaml.cs
@@ -12,10 +12,14 @@ using PostIt.ViewModels;
using PostIt.Views;
using PostIt.Helpers;
using System.Globalization;
+using Live.Avalonia;
+using System.Diagnostics;
+using ReactiveUI;
+using System.Reactive;
namespace PostIt;
-public partial class App : Application
+public partial class App : Application, ILiveView
{
private int _bootStarted;
public string? CliConfigFileSpecification { get; private set; }
@@ -38,13 +42,29 @@ public partial class App : Application
{
AvaloniaXamlLoader.Load(this);
+#if DEBUG
+ this.AttachDeveloperTools();
+#endif
}
public override void OnFrameworkInitializationCompleted()
{
+
+#if DEBUG
+ if (Debugger.IsAttached && !IsProduction())
+ {
+ // Debugging requires pdb loading etc, so we disable live reloading
+ // during a test run with an attached debugger.
+ var window = new Window();
+ window.Content = CreateView(window);
+ window.Show();
+ }
+ else
+#endif
+ {
if (TryHandOffCustomSchemeUrl()) return;
- this.ServiceProvider = new ServiceCollection().BuildServices();
+ this.ServiceProvider = new ServiceCollection().BuildPostItServices();
var settings = ServiceProvider.GetRequiredService();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@@ -60,9 +80,30 @@ public partial class App : Application
InitializeSingleViewLifetime(settings, singleViewPlatform);
}
+ }
+
+#if DEBUG
+ // Here we subscribe to ReactiveUI default exception handler to avoid app
+ // termination in case if we do something wrong in our view models. See:
+ // https://www.reactiveui.net/docs/handbook/default-exception-handler/
+ //
+ // In case if you are using another MV* framework, please refer to its
+ // documentation explaining global exception handling.
+ RxApp.DefaultExceptionHandler = Observer.Create(Console.WriteLine);
+#endif
+
base.OnFrameworkInitializationCompleted();
}
+ private bool IsProduction()
+ {
+#if DEBUG
+ return false;
+#else
+ return true;
+#endif
+ }
+
public void InitializeSingleViewLifetime(Settings settings, ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = View = ServiceProvider!.GetRequiredService();
@@ -213,4 +254,22 @@ public partial class App : Application
{
this.CliConfigFileSpecification = configFile;
}
+
+ public object CreateView(Window window)
+ {
+ if (this.ServiceProvider==null)
+ {
+ var serviceCollection = new ServiceCollection();
+ this.ServiceProvider = serviceCollection.BuildPostItServices();
+ }
+ if (window.DataContext == null)
+ window.DataContext = ServiceProvider!.GetRequiredService();
+
+ // The AppView class will inherit the DataContext
+ // of the window. The AppView class can be a
+ // UserControl, a Grid, a TextBlock, whatever.
+ this.View = ServiceProvider!.GetRequiredService();
+ this.ConfigureRootView(this.View);
+ return this.View;
+ }
}
diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
index f6fcc3e3b..55dad555b 100644
--- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
+++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
@@ -2,8 +2,10 @@ using System;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Services;
using PostIt.ViewModels;
+using PostIt.ViewModels.Chat;
using PostIt.Views;
using PostIt.Views.Blogs;
+using PostIt.Views.Chat;
using PostIt.Views.Commands;
using Yavsc.Api.Client;
@@ -11,7 +13,7 @@ namespace PostIt.Helpers;
public static class ServiceCollectionHelpers
{
- public static IServiceProvider BuildServices(this ServiceCollection services)
+ public static IServiceProvider BuildPostItServices(this ServiceCollection services)
{
var settings = new Settings();
settings.Load();
@@ -40,6 +42,7 @@ public static class ServiceCollectionHelpers
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// SettingsPage is a singleton: there must be one and only one
// instance of the settings UI for the lifetime of the app.
@@ -59,6 +62,7 @@ public static class ServiceCollectionHelpers
services.AddSingleton();
services.AddSingleton();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs b/src/PostIt/PostIt/Models/StatusNotice.cs
similarity index 94%
rename from src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs
rename to src/PostIt/PostIt/Models/StatusNotice.cs
index c87af390c..c50ac3503 100644
--- a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs
+++ b/src/PostIt/PostIt/Models/StatusNotice.cs
@@ -16,7 +16,7 @@ public sealed class StatusNotice
public string BorderBrush { get; }
public string Foreground { get; }
- private StatusNotice(string message, StatusSeverity severity)
+ public StatusNotice(string message, StatusSeverity severity)
{
Message = string.IsNullOrWhiteSpace(message) ? "Pret." : message;
Severity = severity;
diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj
index 5057bde8c..89525f190 100644
--- a/src/PostIt/PostIt/PostIt.csproj
+++ b/src/PostIt/PostIt/PostIt.csproj
@@ -33,11 +33,13 @@
All
+
+
@@ -60,5 +62,6 @@
True
Lang/Resources.resx
+
diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs
index ae4662072..ffaaadc66 100644
--- a/src/PostIt/PostIt/ViewLocator.cs
+++ b/src/PostIt/PostIt/ViewLocator.cs
@@ -5,8 +5,10 @@ using Avalonia.Controls.Templates;
using Microsoft.Extensions.DependencyInjection;
using PostIt.ViewModels;
using PostIt.ViewModels.Commands;
+using PostIt.ViewModels.Chat;
using PostIt.Views;
using PostIt.Views.Blogs;
+using PostIt.Views.Chat;
using PostIt.Views.Commands;
namespace PostIt;
@@ -58,6 +60,7 @@ public class ViewLocator : IDataTemplate
EstimateListPageViewModel => services.GetRequiredService(),
EstimateValidationPageViewModel => services.GetRequiredService(),
MyFilesViewModel => services.GetRequiredService(),
+ ChatViewModel => services.GetRequiredService(),
null => new TextBlock { Text = "No view for " },
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
};
diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs
new file mode 100644
index 000000000..548371157
--- /dev/null
+++ b/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace PostIt.ViewModels.Chat
+{
+ // DTO minimal calqué sur votre logique serveur ChatRoomInfo
+ public class ChatRoomInfo
+ {
+ public string? Name { get; set; }
+ public List Users { get; set; } = new();
+ }
+}
diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs
new file mode 100644
index 000000000..85392cfb1
--- /dev/null
+++ b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace PostIt.ViewModels.Chat
+{
+
+ public partial class ChatViewModel : ViewModelBase
+ {
+ private HubConnection? _connection;
+
+ [ObservableProperty]
+ public partial string ServerUrl { get; protected set; } = "http://localhost:5000/chathub";
+
+
+ [ObservableProperty]
+ public partial string CurrentRoom { get; set; } = "General";
+
+ [ObservableProperty]
+ public partial string NickName { get; set; } = "";
+
+ [ObservableProperty]
+ public partial StatusNotice Status { get; set; }
+ = new StatusNotice("Déconnecté", StatusSeverity.Info);
+
+ [ObservableProperty]
+ public partial bool IsConnected { get; set; } = false;
+
+ // Listes dynamiques pour l'UI
+ public ObservableCollection Messages { get; } = new();
+ public ObservableCollection ConnectedUsers { get; } = new();
+
+ // Commandes pour les boutons
+ [RelayCommand]
+ public async Task ConnectCommand() => await ConnectAsync();
+
+ [RelayCommand]
+ public async Task JoinRoomCommand() => await JoinRoomAsync();
+
+ [RelayCommand]
+ public async Task ChangeNickCommand() => await ChangeNickAsync();
+
+ public override bool CanNavigateNext { get; protected set; } = false;
+ public override bool CanNavigatePrevious { get; protected set; } = true;
+
+ private async Task ConnectAsync()
+ {
+ try
+ {
+ Status = new StatusNotice("Connexion en cours...", StatusSeverity.Info);
+
+ _connection = new HubConnectionBuilder()
+ .WithUrl(ServerUrl)
+ .WithAutomaticReconnect()
+ .Build();
+
+ // 1. Ecoute des notifications globales (notifyUser)
+ _connection.On("notifyUser", (type, user, msg) =>
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ Messages.Add($"[{type}] {user}: {msg}"));
+ });
+
+ // 2. Ecoute des notifications de salon (notifyRoom)
+ _connection.On("notifyRoom", (type, room, user) =>
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ Messages.Add($"*{user} a interagi avec le salon {room} (Action: {type})");
+ if (type == "UserJoin" && !ConnectedUsers.Contains(user))
+ ConnectedUsers.Add(user);
+ });
+ });
+
+ // 3. Réception de l'état complet suite au Join réussi (joint)
+ _connection.On("joint", (roomInfo) =>
+ {
+ Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ Messages.Add($"Salon rejoint avec succès : {roomInfo.Name}");
+ ConnectedUsers.Clear();
+ foreach (var u in roomInfo.Users) ConnectedUsers.Add(u);
+ });
+ });
+
+ await _connection.StartAsync();
+ IsConnected = true;
+ Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info);
+ }
+ catch (Exception ex)
+ {
+ Status = new StatusNotice($"Erreur : {ex.Message}", StatusSeverity.Error);
+ IsConnected = false;
+ }
+ }
+
+ private async Task JoinRoomAsync()
+ {
+ if (_connection == null || string.IsNullOrWhiteSpace(CurrentRoom)) return;
+
+ try
+ {
+ // Appel de la méthode publique 'Join' du Hub (renvoie un ChatRoomInfo)
+ var info = await _connection.InvokeAsync("Join", CurrentRoom);
+ if (info == null)
+ {
+ Messages.Add("Erreur lors de la validation du salon par le serveur.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Messages.Add($"Erreur Join: {ex.Message}");
+ }
+ }
+
+ private async Task ChangeNickAsync()
+ {
+ if (_connection == null || string.IsNullOrWhiteSpace(NickName)) return;
+
+ try
+ {
+ // Appel de la méthode publique 'Nick' du Hub (void)
+ await _connection.InvokeAsync("Nick", NickName);
+ Messages.Add($"Demande de changement de pseudo vers '{NickName}' envoyée.");
+ }
+ catch (Exception ex)
+ {
+ Messages.Add($"Erreur Nick: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs
index 2c138a27d..19ed05833 100644
--- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Helpers;
using PostIt.Services;
+using PostIt.ViewModels.Chat;
using Yavsc.Api.Client;
namespace PostIt.ViewModels;
@@ -36,7 +37,11 @@ public class HomePageViewModel : ViewModelBase
OpenProviderEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Provider));
OpenBlogs = new AsyncRelayCommand(App.PushBlogsPageAsync);
OpenMyFiles = new AsyncRelayCommand(OpenMyFilesAsync);
+ OpenChat = new AsyncRelayCommand(OpenChatAsync);
}
+
+
+ public IAsyncRelayCommand OpenChat { get; }
public IAsyncRelayCommand OpenBlogs { get; }
public IAsyncRelayCommand OpenActivities { get; }
public IAsyncRelayCommand OpenProviderRequests { get; }
@@ -103,6 +108,14 @@ public class HomePageViewModel : ViewModelBase
await app.PushPageAsync(vm);
}
+ private async Task OpenChatAsync()
+ {
+ var app = (App?)Application.Current;
+ var vm = app!.ServiceProvider?.GetRequiredService();
+ await app.PushPageAsync(vm!);
+
+ }
+
///
/// Ouvre la page « Mes fichiers » (espace de stockage personnel),
/// servie par le host Blogs via api/v1/fs.
diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs
index 89743d0ad..629727870 100644
--- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs
@@ -5,15 +5,12 @@ using Avalonia.Platform.Storage;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Controls.Platform;
-using Avalonia.Input;
-using System;
-using Avalonia;
namespace PostIt.Views.Blogs;
public partial class BlogsPage : ContentPage
{
- private IInputPane _inputPane;
+ private IInputPane? _inputPane;
public BlogsPage()
{
@@ -28,7 +25,7 @@ public partial class BlogsPage : ContentPage
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
- _inputPane = topLevel.InputPane;
+ _inputPane = topLevel!.InputPane;
if (_inputPane != null)
{
// S'abonne aux changements d'état du clavier virtuel
diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage..axaml b/src/PostIt/PostIt/Views/Chat/ChatPage..axaml
new file mode 100644
index 000000000..927187de8
--- /dev/null
+++ b/src/PostIt/PostIt/Views/Chat/ChatPage..axaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs
new file mode 100644
index 000000000..5be61b86c
--- /dev/null
+++ b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs
@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+namespace PostIt.Views.Chat;
+public partial class ChatPage : ContentPage
+{
+
+ public ChatPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/PostIt/PostIt/Views/Layout/HomePage.axaml b/src/PostIt/PostIt/Views/Layout/HomePage.axaml
index 4172cb159..421aa10ee 100644
--- a/src/PostIt/PostIt/Views/Layout/HomePage.axaml
+++ b/src/PostIt/PostIt/Views/Layout/HomePage.axaml
@@ -15,6 +15,11 @@
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center"/>
+
+