diff --git a/CHANGELOG.md b/CHANGELOG.md
index 658ae46ec..5bfc336dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,71 +2,19 @@
## [1.0.8-rc20] - unstable
-Chat SignalR fonctionnel de bout en bout (connexion, salons multiples,
-envoi/réception de messages), enregistreur audio et localisation dans
-PostIt, intégration Live.Avalonia démarrée, et reverse-proxy nginx
-apprêté pour WebSocket.
-
### Added
-* [PostIt] Interface de chat : page `ChatPage` (connexion au hub,
- saisie du nom de salon, liste des messages et des membres présents),
- entrée depuis la page d'accueil.
-* [PostIt] Envoi de messages dans un salon joint : DTO `ChatMessage`
- (Name/Room/Message) pour l'événement `ReceiveMessage`, saisie avec
- envoi sur `Enter`, commande inactive tant qu'aucune salle n'est en
- focus (le hub refuse l'envoi hors salon, `LabNoJoinNoSend`).
-* [PostIt] Modèle multi-salles côté client : `JoinedRooms`,
- `SelectedRoom` (salle en focus), messages et utilisateurs indexés par
- salle (`FocusedMessages`/`FocusedUsers`), ComboBox de choix de la
- salle active ; un `Join` sur une salle déjà jointe ne fait que
- changer le focus.
-* [PostIt] Enregistreur audio (`AudioRecorder`, interface
- `IPCMProcessor`) et ressources de localisation
- (`Resources.{resx,en-EN,pt-PT}`).
-* [PostIt][WIP] Début d'intégration Live.Avalonia (preview XAML à
- chaud ; encore KO).
-* [contrib] Bloc `location /chathub` dans `template.vhost` : upgrade
- WebSocket (`Upgrade`/`Connection`), `proxy_read_timeout 3600s` et
- `proxy_buffering off` — sans quoi la connexion SignalR tombait au
- premier silence ou l'upgrade échouait derrière nginx.
+* [PostIt][WIP] Chat user interface
-### Changed
-
-* [PostIt] `StatusNotice` déplacé de `Models` vers `ViewModels` et
- enrichi d'une commande d'action optionnelle (`CheckCommand` +
- `CheckCommandLabel`, interface `IWithLabel`) : le bandeau de statut
- propose désormais l'action contextuelle (Connecter / Déconnecter /
- Reconnecter / Vérifier la connexion).
-* [PostIt] `ChatViewModel` suit le cycle de vie complet de la
- connexion : handlers `Reconnecting` / `Reconnected` / `Closed` de
- `WithAutomaticReconnect`, pour refléter l'état réel au lieu d'un
- `IsConnected` optimiste.
-* [Yavsc.Api] `AddSignalR().AddJsonProtocol(IncludeFields = true)` :
- `ChatRoomInfo` n'expose que des champs publics, que System.Text.Json
- ne sérialise pas par défaut — le client recevait des objets vides
- (`Name` null, `Users` vide) et la liste des salons joints n'était
- jamais alimentée.
-* [Yavsc.Server] `IConnexionManager.OnConnected` prend le `userName`
- en paramètre (résolu depuis les claims dans `ChatHub`) au lieu de le
- relire depuis un dictionnaire jamais alimenté.
-* [PostIt.Android] `network_security_config.xml` déplacé sous
- `Resources/xml/` (chemin canonique Android).
+* [PostIt][WIP] Live.Avalonia start of integration
### Fixed
-* [Yavsc.Server] `KeyNotFoundException` dans
- `HubConnectionManager.OnConnected` à chaque connexion SignalR :
- `ChatUserNames` (et `ChatCxIds`) n'étaient jamais alimentés — relique
- d'un `SetUserName` disparu lors d'une refactorisation. L'exception
- dans `OnConnectedAsync` faisait fermer la connexion par le hub, et
- PostIt affichait « Déconnecté (The server closed the connection…) »
- immédiatement après la connexion.
-* [PostIt] URL du hub SignalR : retrait du préfixe `/api/v1` de
- `ApiUrl` avant d'apposer `Constants.ChatHubPath` (le hub est mappé à
- la racine du service, pas sous le préfixe versionné).
-* [PostIt] La `StatusBar` de la page chat référençait `Grid.Row="4"`
- sur une grille à 3 rangées (rangée hors limites, bandeau invisible).
+nothing
+
+### Changed
+
+nothçing
## [1.0.8-rc19] - unstable
diff --git a/contrib/Makefile b/contrib/Makefile
index 5ccf773a1..deb55bcc6 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -14,7 +14,7 @@ reinstall: copy-binaries
@echo "💯 backend started"
reinstall-prod:
- make reinstall APP_INSTALL_PREFIX= APP_INSTALL_ENV=production
+ make reinstall APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production
generated/$(APP_INSTALL_PREFIX)yavscOrg.service:
generated/$(APP_INSTALL_PREFIX)yavscBlogs.service:
diff --git a/contrib/template.vhost b/contrib/template.vhost
index b4c9c139e..ea2c2cb6d 100644
--- a/contrib/template.vhost
+++ b/contrib/template.vhost
@@ -57,25 +57,5 @@ server {
proxy_read_timeout 100s;
proxy_buffering off;
}
-
- # SignalR (ChatHub) : WebSocket exige Upgrade/Connection et un timeout
- # long, sinon la connexion tombe au premier silence ou l'upgrade échoue.
- # Connection est fixé à "upgrade" (pas de map $connection_upgrade, qui
- # exigerait une directive map {} globale hors de ce vhost) : SignalR
- # négocie toujours un upgrade sur ce chemin, et la négociation initiale
- # (POST /negotiate, sans en-tête Upgrade) n'est pas affectée par cet
- # en-tête côté Kestrel.
- location /chathub {
- proxy_pass $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_read_timeout 3600s;
- proxy_buffering off;
- }
include server-status.conf;
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
index 256555df1..46f0bc9bb 100644
--- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
+++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
@@ -4,7 +4,6 @@
+ android:networkSecurityConfig="@xml/network_security_config">
diff --git a/src/PostIt/PostIt.Android/Resources/xml/network_security_config.xml b/src/PostIt/PostIt.Android/Resources/network_security_config.xml
similarity index 100%
rename from src/PostIt/PostIt.Android/Resources/xml/network_security_config.xml
rename to src/PostIt/PostIt.Android/Resources/network_security_config.xml
diff --git a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs
index 31c82387d..17efd391b 100644
--- a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs
@@ -1,5 +1,4 @@
using System.Net.Http;
-using PostIt.Models;
using PostIt.ViewModels;
using Yavsc;
using Yavsc.Api.Client;
diff --git a/src/PostIt/PostIt.Tests/StatusBarTests.cs b/src/PostIt/PostIt.Tests/StatusBarTests.cs
index 15889e0b0..4053dbaf3 100644
--- a/src/PostIt/PostIt.Tests/StatusBarTests.cs
+++ b/src/PostIt/PostIt.Tests/StatusBarTests.cs
@@ -2,7 +2,6 @@ using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;
using PostIt.Controls;
-using PostIt.Models;
using PostIt.ViewModels;
namespace PostIt.Tests;
@@ -22,9 +21,7 @@ public class StatusBarTests
[AvaloniaFact]
public void Status_message_is_rendered_by_a_SelectableTextBlock()
{
- var bar = new StatusBar {
- DataContext =
- StatusNotice.Error("Échec de la suppression : boom") };
+ var bar = new StatusBar { DataContext = StatusNotice.Error("Échec de la suppression : boom") };
var window = new Window { Content = bar };
window.Show();
@@ -35,4 +32,4 @@ public class StatusBarTests
Assert.NotNull(message);
Assert.Equal("Échec de la suppression : boom", message!.Text);
}
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/Controls/StatusBar.axaml b/src/PostIt/PostIt/Controls/StatusBar.axaml
index 755ff7d3e..2f46fee33 100644
--- a/src/PostIt/PostIt/Controls/StatusBar.axaml
+++ b/src/PostIt/PostIt/Controls/StatusBar.axaml
@@ -10,19 +10,16 @@
Padding="8">
+ FontWeight="Bold"
+ Foreground="{Binding Foreground}"
+ VerticalAlignment="Top"/>
+
-
+ Foreground="{Binding Foreground}"
+ TextWrapping="Wrap"
+ VerticalAlignment="Center"/>
diff --git a/src/PostIt/PostIt/Models/ChatMessage.cs b/src/PostIt/PostIt/Models/ChatMessage.cs
deleted file mode 100644
index 52733311a..000000000
--- a/src/PostIt/PostIt/Models/ChatMessage.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace PostIt.ViewModels.Chat
-{
- ///
- /// DTO de message de salon, calqué sur la charge envoyée par
- /// ChatHub.Send 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.
- ///
- public class ChatMessage
- {
- public string? Name { get; set; }
- public string? Room { get; set; }
- public string? Message { get; set; }
- }
-}
diff --git a/src/PostIt/PostIt/Models/ChatMessageDto.cs b/src/PostIt/PostIt/Models/ChatMessageDto.cs
deleted file mode 100644
index b93ccbe29..000000000
--- a/src/PostIt/PostIt/Models/ChatMessageDto.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace PostIt.ViewModels.Chat
-{
- ///
- /// DTO minimal calqué sur l'objet anonyme émis par le hub
- /// ({ Name, Room, Message }) via l'événement
- /// ReceiveMessage.
- ///
- public class ChatMessageDto
- {
- public string? Name { get; set; }
- public string? Room { get; set; }
- public string? Message { get; set; }
- }
-}
diff --git a/src/PostIt/PostIt/Models/IWithLabel.cs b/src/PostIt/PostIt/Models/IWithLabel.cs
deleted file mode 100644
index b76f912db..000000000
--- a/src/PostIt/PostIt/Models/IWithLabel.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace PostIt.ViewModels;
-
-internal interface IWithLabel
-{
- string Label { get; }
-}
diff --git a/src/PostIt/PostIt/Models/StatusNotice.cs b/src/PostIt/PostIt/Models/StatusNotice.cs
new file mode 100644
index 000000000..c50ac3503
--- /dev/null
+++ b/src/PostIt/PostIt/Models/StatusNotice.cs
@@ -0,0 +1,35 @@
+namespace PostIt.ViewModels;
+
+public enum StatusSeverity
+{
+ Info,
+ Warning,
+ Error
+}
+
+public sealed class StatusNotice
+{
+ public string Message { get; }
+ public StatusSeverity Severity { get; }
+ public string Glyph { get; }
+ public string Background { get; }
+ public string BorderBrush { get; }
+ public string Foreground { get; }
+
+ public StatusNotice(string message, StatusSeverity severity)
+ {
+ Message = string.IsNullOrWhiteSpace(message) ? "Pret." : message;
+ Severity = severity;
+
+ (Glyph, Background, BorderBrush, Foreground) = severity switch
+ {
+ StatusSeverity.Error => ("!", "#7F1D1D", "#C62828", "#e1f0f6"),
+ StatusSeverity.Warning => ("~", "#7C4A03", "#E6A700", "#eaeaea"),
+ _ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
+ };
+ }
+
+ public static StatusNotice Info(string message) => new(message, StatusSeverity.Info);
+ public static StatusNotice Warning(string message) => new(message, StatusSeverity.Warning);
+ public static StatusNotice Error(string message) => new(message, StatusSeverity.Error);
+}
diff --git a/src/PostIt/PostIt/Models/StatusSeverity.cs b/src/PostIt/PostIt/Models/StatusSeverity.cs
deleted file mode 100644
index eb6ef4502..000000000
--- a/src/PostIt/PostIt/Models/StatusSeverity.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace PostIt.Models;
-
-public enum StatusSeverity
-{
- Info,
- Warning,
- Error
-}
diff --git a/src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs
index 6201c061b..cd068b4f4 100644
--- a/src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs
@@ -5,7 +5,7 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
-using PostIt.Models;
+using PostIt.Helpers;
using PostIt.Services;
using Yavsc.Api.Client;
using Yavsc.Api.Client.Dtos;
diff --git a/src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs
index 43ed60823..be1b220fa 100644
--- a/src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs
@@ -12,7 +12,6 @@ using Yavsc.Api.Client.Dtos;
using Yavsc.Abstract.BlogSpot;
using Yavsc.Abstract.Identity.Security;
using System.Net.Http;
-using PostIt.Models;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs
index 1890309ec..bd13faf9b 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs
@@ -8,7 +8,6 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Helpers;
-using PostIt.Models;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs
index 5cd373708..f1b05b749 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs
@@ -12,7 +12,6 @@ using PostIt.Helpers;
using Yavsc;
using Yavsc.Api.Client;
using Yavsc.Abstract.Workflow;
-using PostIt.Models;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs
index 0df05b21a..c9f122dce 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs
@@ -388,4 +388,4 @@ public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionSt
QueryStatus.Success => "V",
_ => "i",
};
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs
index 14e7e74b9..533fbc380 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs
@@ -6,7 +6,6 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Helpers;
-using PostIt.Models;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs
index f8f94436a..1f1e4b4be 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs
@@ -10,7 +10,6 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Helpers;
-using PostIt.Models;
using Yavsc.Api.Client;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs
index 766dda903..c90a6d884 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs
@@ -9,7 +9,6 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Helpers;
-using PostIt.Models;
using Yavsc.Api.Client;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs
index 360bf54ad..7fd84fd88 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs
@@ -13,7 +13,6 @@ using PostIt.Helpers;
using Yavsc;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
-using PostIt.Models;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs
index 6fe6eccc9..a1e016e7f 100644
--- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs
@@ -12,7 +12,6 @@ using Yavsc.Api.Client;
using Yavsc.Abstract.Files;
using PostIt.Helpers;
using AvaloniaEdit.Document;
-using PostIt.Models;
namespace PostIt.ViewModels;
diff --git a/src/PostIt/PostIt/Models/ChatRoomInfo.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs
similarity index 100%
rename from src/PostIt/PostIt/Models/ChatRoomInfo.cs
rename to src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs
diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs
index ba452cb4a..85392cfb1 100644
--- a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs
@@ -1,42 +1,21 @@
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.AspNetCore.SignalR.Client;
-using Microsoft.Extensions.DependencyInjection;
-using PostIt.Models;
-using PostIt.Services;
-using Yavsc;
namespace PostIt.ViewModels.Chat
{
- ///
- /// ViewModel pour la gestion du chat,
- /// incluant la connexion au Hub SignalR,
- /// la gestion des salons et des utilisateurs connectés.
- ///
public partial class ChatViewModel : ViewModelBase
{
- private readonly Settings _settings;
- private readonly YavscApiClient? _api;
-
- // Le paramètre optionnel permet l'instanciation design-time
- // (Design.DataContext dans ChatPage.axaml) sans conteneur DI.
- public ChatViewModel(YavscApiClient? api = null)
- {
- var app = App.Current as App;
- _settings = app!.ServiceProvider!.GetRequiredService(); // Assurez-vous que _settings est initialisé avant utilisation.
- _api = api;
- Status = new StatusNotice("Déconnecté", StatusSeverity.Info,
- new RelayCheckCommand("Connecter", async () => await ConnectAsync()));
- FocusedMessages = _systemMessages;
- }
-
private HubConnection? _connection;
+ [ObservableProperty]
+ public partial string ServerUrl { get; protected set; } = "http://localhost:5000/chathub";
+
+
[ObservableProperty]
public partial string CurrentRoom { get; set; } = "General";
@@ -45,56 +24,14 @@ namespace PostIt.ViewModels.Chat
[ObservableProperty]
public partial StatusNotice Status { get; set; }
+ = new StatusNotice("Déconnecté", StatusSeverity.Info);
[ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
public partial bool IsConnected { get; set; } = false;
- [ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
- public partial string MessageToSend { get; set; } = "";
-
- // Salles jointes (choix de la salle en focus dans la vue).
- public ObservableCollection JoinedRooms { get; } = new();
-
- // Salle en focus ; null = aucune salle, le journal affiche
- // alors les notifications système.
- [ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(SendMessageCommand))]
- public partial string? SelectedRoom { get; set; }
-
- // Notifications globales (connexion, erreurs de join, ...)
- private readonly ObservableCollection _systemMessages = new();
-
- // Messages et utilisateurs présents, indexés par salle.
- private readonly Dictionary> _messagesByRoom = new();
- private readonly Dictionary> _usersByRoom = new();
- private static readonly ObservableCollection _noUsers = new();
-
- // Vues "focus" exposées à la page : pointent vers la salle
- // sélectionnée, ou vers le journal système / une liste vide.
- [ObservableProperty]
- public partial ObservableCollection FocusedMessages { get; set; }
-
- [ObservableProperty]
- public partial ObservableCollection FocusedUsers { get; set; } = _noUsers;
-
- partial void OnSelectedRoomChanged(string? value)
- {
- FocusedMessages = value != null && _messagesByRoom.TryGetValue(value, out var msgs)
- ? msgs : _systemMessages;
- FocusedUsers = value != null && _usersByRoom.TryGetValue(value, out var users)
- ? users : _noUsers;
- }
-
- // Trace une notification système dans la vue courante
- // (et dans le journal global pour consultation ultérieure).
- private void Log(string line)
- {
- _systemMessages.Add(line);
- if (!ReferenceEquals(FocusedMessages, _systemMessages))
- FocusedMessages.Add(line);
- }
+ // Listes dynamiques pour l'UI
+ public ObservableCollection Messages { get; } = new();
+ public ObservableCollection ConnectedUsers { get; } = new();
// Commandes pour les boutons
[RelayCommand]
@@ -103,88 +40,20 @@ namespace PostIt.ViewModels.Chat
[RelayCommand]
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 qu'aucune salle n'est en focus.
- private bool CanSendMessage() =>
- IsConnected && SelectedRoom != null && !string.IsNullOrWhiteSpace(MessageToSend);
-
- [RelayCommand(CanExecute = nameof(CanSendMessage))]
- private async Task SendMessageAsync()
- {
- if (_connection == null || SelectedRoom == null) return;
- var text = MessageToSend.Trim();
- try
- {
- await _connection.InvokeAsync("Send", SelectedRoom, text);
- MessageToSend = string.Empty;
- }
- catch (Exception ex)
- {
- Log($"Erreur d'envoi : {ex.Message}");
- }
- }
+ [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 DisconnectAsync()
- {
- if (_connection != null)
- {
- await _connection.StopAsync();
- await _connection.DisposeAsync();
- _connection = null;
- }
- IsConnected = false;
- SelectedRoom = null;
- JoinedRooms.Clear();
- _messagesByRoom.Clear();
- _usersByRoom.Clear();
- _systemMessages.Clear();
- Status = new StatusNotice("Déconnecté", StatusSeverity.Info,
- new RelayCheckCommand("Connecter", async () => await ConnectAsync()));
- }
-
private async Task ConnectAsync()
{
try
{
- Status = new StatusNotice("Connexion en cours...", StatusSeverity.Info,
- new RelayCheckCommand("Vérifier la connexion", async () => await CheckConnectionAsync()));
-
- if (_api == null || !_api.HasValidSession)
- {
- Status = new StatusNotice("Session expirée : reconnectez-vous avant d'ouvrir le chat.", StatusSeverity.Error,
- new RelayCheckCommand("Reconnecter", async () => await ConnectAsync()));
- return;
- }
-
- // Le hub est mappé à la racine du service ("/chathub"),
- // pas sous le préfixe versionné "/api/v1" : on le retire.
- var hubBase = _settings.ApiUrl.TrimEnd('/');
- if (hubBase.EndsWith("/api/v1", StringComparison.OrdinalIgnoreCase))
- hubBase = hubBase[..^"/api/v1".Length];
- var hubUrl = hubBase + "/" + Constants.ChatHubPath;
+ Status = new StatusNotice("Connexion en cours...", StatusSeverity.Info);
_connection = new HubConnectionBuilder()
- .WithUrl(hubUrl,
-
- options =>
- {
- // Le hub est [Authorize] : on attache le Bearer OIDC.
- // AccessTokenProvider est rappelé à chaque reconnexion,
- // donc on tente un refresh silencieux à chaque appel.
- // Un refresh mort (invalid_grant) ne doit pas faire
- // échouer StartAsync avec une exception interne : on
- // renvoie le token courant (éventuellement expiré) et
- // le serveur répondra un 401 propre, surfacé à l'UI.
- options.AccessTokenProvider = async () =>
- {
- await _api.TrySilentLoginAsync();
- return _api.CurrentAccessToken;
- };
- })
+ .WithUrl(ServerUrl)
.WithAutomaticReconnect()
.Build();
@@ -192,7 +61,7 @@ namespace PostIt.ViewModels.Chat
_connection.On("notifyUser", (type, user, msg) =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
- Log($"[{type}] {user}: {msg}"));
+ Messages.Add($"[{type}] {user}: {msg}"));
});
// 2. Ecoute des notifications de salon (notifyRoom)
@@ -200,153 +69,66 @@ namespace PostIt.ViewModels.Chat
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
- if (_messagesByRoom.TryGetValue(room, out var roomLog))
- roomLog.Add($"*{user} a interagi avec le salon {room} (Action: {type})");
- else
- Log($"*{user} a interagi avec le salon {room} (Action: {type})");
- if (type == "UserJoin" && _usersByRoom.TryGetValue(room, out var users)
- && !users.Contains(user))
- users.Add(user);
+ 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(() => OnRoomJoined(roomInfo));
- });
-
- // 4. Réception des messages de salon (diffusés par ChatHub.Send)
- _connection.On("ReceiveMessage", (msg) =>
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
- var room = msg.Room ?? "?";
- if (!_messagesByRoom.TryGetValue(room, out var roomLog))
- roomLog = _messagesByRoom[room] = new();
- roomLog.Add($"{msg.Name} : {msg.Message}");
+ 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, 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;
- };
+ Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info);
}
catch (Exception ex)
{
- Status = new StatusNotice($"Erreur : {ex.Message}", StatusSeverity.Error, new RelayCheckCommand("Reconnecter", async () => await ConnectAsync()));
+ Status = new StatusNotice($"Erreur : {ex.Message}", StatusSeverity.Error);
IsConnected = false;
}
}
- // Alimente JoinedRooms et les collections par salle à partir
- // du ChatRoomInfo renvoyé par le hub (valeur de retour du Join
- // et/ou événement "joint"). Idempotent.
- private void OnRoomJoined(ChatRoomInfo roomInfo)
- {
- // ChatHub.Join renseigne toujours Name (salle existante ou
- // créée) ; un Name nul signalerait un payload inattendu.
- var roomName = roomInfo.Name ?? CurrentRoom;
- if (roomInfo.Name == null)
- Log("OnRoomJoined : nom de salle absent du payload, repli sur la salle demandée.");
- if (!_messagesByRoom.TryGetValue(roomName, out var roomLog))
- roomLog = _messagesByRoom[roomName] = new();
- roomLog.Add($"Salon rejoint avec succès : {roomName}");
- var users = _usersByRoom[roomName] = new();
- foreach (var u in roomInfo.Users) users.Add(u);
- if (!JoinedRooms.Contains(roomName))
- JoinedRooms.Add(roomName);
- SelectedRoom = roomName;
- }
-
- private Task CheckConnectionAsync()
- {
- var state = _connection?.State.ToString() ?? "aucune connexion";
- Log($"État de la connexion : {state}");
- return Task.CompletedTask;
- }
-
private async Task JoinRoomAsync()
{
if (_connection == null || string.IsNullOrWhiteSpace(CurrentRoom)) return;
- // Salle déjà jointe : on se contente de la mettre en focus.
- if (JoinedRooms.Contains(CurrentRoom))
- {
- SelectedRoom = 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)
- {
- Log($"Connexion non active ({_connection.State}), tentative de reconnexion...");
- try
- {
- await _connection.StartAsync();
- }
- catch (Exception ex)
- {
- Log($"Reconnexion impossible : {ex.Message}");
- return;
- }
- }
-
try
{
// Appel de la méthode publique 'Join' du Hub (renvoie un ChatRoomInfo)
var info = await _connection.InvokeAsync("Join", CurrentRoom);
if (info == null)
{
- Log("Erreur lors de la validation du salon par le serveur.");
- }
- else
- {
- // On alimente le modèle depuis la valeur de retour,
- // sans attendre l'événement "joint".
- OnRoomJoined(info);
+ Messages.Add("Erreur lors de la validation du salon par le serveur.");
}
}
catch (Exception ex)
{
- Log($"Erreur Join: {ex.Message}");
+ 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/Chat/RelayCheckCommand.cs b/src/PostIt/PostIt/ViewModels/Chat/RelayCheckCommand.cs
deleted file mode 100644
index 909d7a859..000000000
--- a/src/PostIt/PostIt/ViewModels/Chat/RelayCheckCommand.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using System.Windows.Input;
-
-namespace PostIt.ViewModels.Chat
-{
- internal class RelayCheckCommand : ICommand, IWithLabel
- {
- public string Label { get; }
- private Func value;
-
- public RelayCheckCommand(string v, Func value)
- {
- this.Label = v;
- this.value = value;
- }
-
- public event EventHandler? CanExecuteChanged;
-
- public bool CanExecute(object? parameter)
- {
- return value != null;
- }
-
- public void Execute(object? parameter)
- {
- if (value != null)
- {
- _ = value();
- }
- }
- }
-}
diff --git a/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs
index 4898c9560..5ad2b08f4 100644
--- a/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs
@@ -2,7 +2,6 @@ using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using PostIt.Models;
using Yavsc;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
diff --git a/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs
index 1b1847e96..72d9ce8ea 100644
--- a/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs
@@ -6,7 +6,6 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
-using PostIt.Models;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
using Yavsc.Models.Billing;
diff --git a/src/PostIt/PostIt/ViewModels/Layout/ActionStatusViewModelExtensions.cs b/src/PostIt/PostIt/ViewModels/Layout/ActionStatusViewModelExtensions.cs
index 9fe286ac0..614d477c8 100644
--- a/src/PostIt/PostIt/ViewModels/Layout/ActionStatusViewModelExtensions.cs
+++ b/src/PostIt/PostIt/ViewModels/Layout/ActionStatusViewModelExtensions.cs
@@ -1,5 +1,3 @@
-using PostIt.Models;
-
namespace PostIt.ViewModels;
public interface IActionStatusViewModel
@@ -31,4 +29,4 @@ public static class ActionStatusViewModelExtensions
_ => StatusNotice.Info(normalizedMessage),
};
}
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs b/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs
index 169b29b16..cc4b46828 100644
--- a/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs
@@ -7,7 +7,6 @@ using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Helpers;
-using PostIt.Models;
using Yavsc.Api.Client;
namespace PostIt.ViewModels;
@@ -227,4 +226,4 @@ public partial class MyFilesViewModel : ViewModelBase, IActionStatusViewModel
var idx = current.LastIndexOf('/');
return idx < 0 ? string.Empty : current[..idx];
}
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
index 72eea5d35..c034437b8 100644
--- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
@@ -9,7 +9,6 @@ using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
-using PostIt.Models;
[assembly: InternalsVisibleTo("PostIt.Tests")]
diff --git a/src/PostIt/PostIt/ViewModels/StatusNotice.cs b/src/PostIt/PostIt/ViewModels/StatusNotice.cs
deleted file mode 100644
index 404287dbe..000000000
--- a/src/PostIt/PostIt/ViewModels/StatusNotice.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Windows.Input;
-using PostIt.Models;
-
-namespace PostIt.ViewModels;
-
-public sealed class StatusNotice
-{
- public string Message { get; }
- public StatusSeverity Severity { get; }
- public string Glyph { get; }
- public string Background { get; }
- public string BorderBrush { get; }
- public string Foreground { get; }
-
- public bool HasCheckCommand { get => CheckCommand != null; }
-
- public ICommand? CheckCommand { get; }
-
- public string CheckCommandLabel { get; }
-
- public StatusNotice(string message, StatusSeverity severity, ICommand? checkCommand)
- {
- Message = string.IsNullOrWhiteSpace(message) ? "Pret." : message;
- Severity = severity;
-
- (Glyph, Background, BorderBrush, Foreground) = severity switch
- {
- StatusSeverity.Error => ("!", "#7F1D1D", "#C62828", "#e1f0f6"),
- StatusSeverity.Warning => ("~", "#7C4A03", "#E6A700", "#eaeaea"),
- _ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
- };
- CheckCommand = checkCommand;
- 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);
- public static StatusNotice Warning(string message, ICommand? checkCommand = null) => new(message, StatusSeverity.Warning, checkCommand);
- public static StatusNotice Error(string message, ICommand? checkCommand = null) => new(message, StatusSeverity.Error, checkCommand);
-}
diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage.axaml b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml
index 4749736a7..0ac7e818c 100644
--- a/src/PostIt/PostIt/Views/Chat/ChatPage.axaml
+++ b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml
@@ -1,5 +1,4 @@
-
-
-
+ x:Class="PostIt.Views.Chat.ChatPage"
+ x:DataType="vm:ChatViewModel" >
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs
index f73e1f827..5be61b86c 100644
--- a/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs
@@ -7,5 +7,4 @@ public partial class ChatPage : ContentPage
{
InitializeComponent();
}
-
}
diff --git a/src/PostIt/PostIt/Views/Layout/MainView.axaml b/src/PostIt/PostIt/Views/Layout/MainView.axaml
index 94b1d98bc..92f53eb54 100644
--- a/src/PostIt/PostIt/Views/Layout/MainView.axaml
+++ b/src/PostIt/PostIt/Views/Layout/MainView.axaml
@@ -16,7 +16,6 @@
diff --git a/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs b/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs
index 9df94960d..b50245919 100644
--- a/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs
@@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Microsoft.Extensions.DependencyInjection;
-using PostIt.Models;
using PostIt.Services;
using PostIt.ViewModels;
diff --git a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
index e51e77e63..c673e6fae 100644
--- a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
+++ b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
@@ -4,8 +4,10 @@ namespace Yavsc.Abstract.Chat
{
public static class ChatHubConstants
{
+ public const string HubGroupAuthenticated = "authenticated";
+ public const string HubGroupAnonymous = "anonymous";
public const string HubGroupCops= "cops";
- public const string HubGroupRoomsPrefix = "room_";
+ public const string HubGroupRomsPrefix = "room_";
public const int MaxChanelName = 255;
public const string HubGroupFollowingPrefix = "fol ";
@@ -19,6 +21,6 @@ namespace Yavsc.Abstract.Chat
public const string HopWontKickOp = "Half operator cannot kick any operator";
public const string LabAuthChatUser = "Authenticated chat user";
public const string NoKickOnCop = "No, you won´t, you´ĺl never do kick a cop, it is the bad.";
- public const string LabNoJoinNoSend = "LabNoJoinNoSend";
+ public const string LabNoJoinNoSend = "LabnoJoinNoSend";
}
}
diff --git a/src/Yavsc.Abstract/Constants.cs b/src/Yavsc.Abstract/Constants.cs
index ca871f9d5..a2649491c 100644
--- a/src/Yavsc.Abstract/Constants.cs
+++ b/src/Yavsc.Abstract/Constants.cs
@@ -13,7 +13,6 @@ namespace Yavsc
public const string BlogTagPath = "blogtag";
public const string CirclePath = "circle";
public const string CommentsPath = "blogcomments";
- public const string ChatHubPath = "chathub";
public static readonly Scope[] SiteScopes = {
new Scope { Id = "profile", Description = "Your profile informations" },
diff --git a/src/Yavsc.Api/Program.cs b/src/Yavsc.Api/Program.cs
index 23c0c77fd..8daff34d9 100644
--- a/src/Yavsc.Api/Program.cs
+++ b/src/Yavsc.Api/Program.cs
@@ -11,7 +11,6 @@ using Yavsc.Models;
using Yavsc;
using Yavsc.Server.Helpers;
using Yavsc.Services;
-using Yavsc.Server.Hubs;
internal class Program
{
@@ -66,12 +65,7 @@ internal class Program
services.AddAuthentication("Bearer")
.AddYavscJwtBearer(builder.Configuration);
- services.AddSignalR()
- // ChatRoomInfo et d'autres DTO de hub exposent des champs
- // publics (pas des propriétés) : STJ ne les sérialise pas
- // sans IncludeFields, et le client recevrait des objets vides.
- .AddJsonProtocol(options =>
- options.PayloadSerializerOptions.IncludeFields = true);
+ services.AddSignalR();
services.AddSingleton();
// DbContextBuilder
@@ -120,11 +114,13 @@ internal class Program
.UseRouting()
.UseAuthentication()
.UseAuthorization()
- .UseCors("default");
+ .UseCors("default")
+ ;
app.MapIdentityApi().RequireAuthorization("ApiScope");
app.MapDefaultControllerRoute();
+
+
app.UseSession();
- app.MapHub("/" + Constants.ChatHubPath);
await app.RunAsync();
}
}
diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
index 32357c2c5..e09bbb5a4 100644
--- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
+++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
@@ -400,7 +400,7 @@ namespace Yavsc.Controllers
public IActionResult SetUserName()
{
- return View(new SetUserNameViewModel() { UserName = User.GetUserName() });
+ return View(new SetUserNameViewModel() { UserName = User.Identity.Name });
}
[HttpPost]
diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs
index 563a74789..b5bb65cc5 100644
--- a/src/Yavsc.Org/Extensions/HostingExtensions.cs
+++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs
@@ -1053,6 +1053,8 @@ public static class HostingExtensions
app.UseCors("default");
app.MapDefaultControllerRoute();
app.MapStaticAssets(staticAssetsManifestPath);
+ //app.MapRazorPages();
+ app.MapHub("/chatHub");
WorkflowHelpers.ConfigureBillingService();
diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs
index db5dbe983..397eada85 100644
--- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs
+++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs
@@ -128,29 +128,7 @@ public static class ServiceExtensions
options.ClaimsIssuer = authority;
options.Audience = audiences[0];
- // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
- // due to a limitation in Browser APIs. We restrict it to only calls to the
- // SignalR hub in this code.
- // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
- // for more information about security considerations when using
- // the query string to transmit the access token.
- options.Events = new JwtBearerEvents
- {
- OnMessageReceived = context =>
- {
- var accessToken = context.Request.Query["access_token"];
- // If the request is for our hub...
- var path = context.HttpContext.Request.Path;
- if (!string.IsNullOrEmpty(accessToken) &&
- (path.StartsWithSegments("/hubs/chat")))
- {
- // Read the token out of the query string
- context.Token = accessToken;
- }
- return Task.CompletedTask;
- }
- };
// Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs,
// Yavsc.Org itself) validates JWTs against the OP that runs
// on https://localhost:5001 with a self-signed dev cert.
diff --git a/src/Yavsc.Server/Hubs/ChatHub.cs b/src/Yavsc.Server/Hubs/ChatHub.cs
index b9e54496c..799f7441c 100644
--- a/src/Yavsc.Server/Hubs/ChatHub.cs
+++ b/src/Yavsc.Server/Hubs/ChatHub.cs
@@ -24,7 +24,6 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Localization;
-
#pragma warning disable CS4014 // Dans la mesure où cet appel n'est pas attendu, l'exécution de la méthode actuelle continue avant la fin de l'appel
namespace Yavsc.Server.Hubs
@@ -37,8 +36,6 @@ namespace Yavsc.Server.Hubs
using Yavsc.Models.Chat;
using Yavsc.Server.Helpers;
using Yavsc.Services;
-
- [Authorize]
public partial class ChatHub : Hub, IDisposable
{
private readonly ApplicationDbContext _dbContext;
@@ -56,70 +53,100 @@ namespace Yavsc.Server.Hubs
_dbContext = dbContext;
_localizer = stringLocalizerFactory.Create(typeof(ChatHub));
- _cxManager = connexionManager;
- _cxManager.SetErrorHandler((context, error) =>
+ _cxManager = connexionManager;
+ _cxManager.SetErrorHandler ((context, error) =>
{
NotifyUser(NotificationTypes.Error, context, error);
});
_logger = loggerFactory.CreateLogger();
- InputValidator = new HubInputValidator(_localizer)
- {
+ InputValidator = new HubInputValidator(_localizer) {
- NotifyUser = async (type, target, msg) => await this.NotifyUser(type, target, msg)
- };
+ NotifyUser = async (type, target, msg) => await this.NotifyUser(type, target, msg) };
+ }
+
+ void SetUserName(string cxId, string userName)
+ {
+ _cxManager.SetUserName(cxId, userName);
}
public override async Task OnConnectedAsync()
{
- var user = Context.User;
+ bool isAuth = Context.User?.Identity?.IsAuthenticated ?? false;
bool isCop = false;
- string userName = user.GetUserName();
-
- _logger.LogInformation(_localizer.GetString(ChatHubConstants.LabAuthChatUser));
-
- var userId = _dbContext.Users.First(u => u.UserName == userName).Id;
-
- await Clients.Group(ChatHubConstants.HubGroupFollowingPrefix + userId).SendAsync("notifyUser", NotificationTypes.Connected, userName, null);
- isCop = Context.User.IsInMsRole(Constants.AdminGroupName);
- if (isCop)
+ string userName = setUserName();
+ if (isAuth)
{
- await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupCops);
- }
- foreach (var uid in _dbContext.CircleMembers.Select(m => m.MemberId))
- {
- await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupFollowingPrefix + uid);
+ var group = isAuth ?
+ ChatHubConstants.HubGroupAuthenticated : ChatHubConstants.HubGroupAnonymous;
+ // Log ("Cx: " + group);
+ await Groups.AddToGroupAsync(Context.ConnectionId, group);
+ _logger.LogInformation(_localizer.GetString(ChatHubConstants.LabAuthChatUser));
+
+ var userId = _dbContext.Users.First(u => u.UserName == Context.User.Identity.Name).Id;
+
+ await Clients.Group(ChatHubConstants.HubGroupFollowingPrefix + userId).SendAsync("notifyUser", NotificationTypes.Connected, userName, null);
+ isCop = Context.User.IsInMsRole(Constants.AdminGroupName) ;
+ if (isCop)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupCops);
+ }
+
+ foreach (var uid in _dbContext.CircleMembers.Select(m => m.MemberId))
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupFollowingPrefix + uid);
+ }
}
- _cxManager.OnConnected(Context.ConnectionId, userName, isCop);
+ else
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, ChatHubConstants.HubGroupAnonymous);
+ }
+ _cxManager.OnConnected(Context.ConnectionId, isCop);
await base.OnConnectedAsync();
}
+ string setUserName(string queryUname = "anon")
+ {
+ if (Context.User != null)
+ if (Context.User.Identity.IsAuthenticated)
+ {
+ SetUserName(Context.ConnectionId, Context.User.Identity.Name);
+ return Context.User.Identity.Name;
+ }
+ anonymousSequence++;
+ var anonymousName = $"{ChatHubConstants.AnonymousUserNamePrefix}{queryUname}{anonymousSequence}";
+ SetUserName(Context.ConnectionId, anonymousName);
+ return anonymousName;
+ }
+
+ static long anonymousSequence = 0;
+
public override async Task OnDisconnectedAsync(Exception ex)
{
- string userName = Context.User?.GetUserName();
+ string userName = Context.User?.Identity.Name;
if (userName != null)
{
var user = _dbContext.Users.FirstOrDefault(u => u.UserName == userName);
- if (user != null)
- {
- var userId = user.Id;
- await Clients.Group(ChatHubConstants.HubGroupFollowingPrefix + userId).SendAsync("notifyUser", NotificationTypes.DisConnected, userName, null);
- }
+ var userId = user.Id;
+ await Clients.Group(ChatHubConstants.HubGroupFollowingPrefix + userId).SendAsync("notifyUser", NotificationTypes.DisConnected, userName, null);
_cxManager.OnDisconnected(Context.ConnectionId);
}
- await base.OnDisconnectedAsync(ex);
+ await base.OnDisconnectedAsync(ex);
}
- ///
- /// Renvoie le login résolu côté serveur (claim name de
- /// l'organisation Yavsc.Org, via ).
- /// Permet au client d'afficher l'identité réellement authentifiée,
- /// sans jamais faire confiance à un pseudo fourni par le client.
- ///
- public string WhoAmI()
+
+ public async Task Nick(string nickName)
{
- return Context.User?.GetUserName() ?? string.Empty;
+ if (!InputValidator.ValidateUserName(nickName)) return;
+
+ var candidate = "?" + nickName;
+ if (_cxManager.IsConnected(candidate))
+ {
+ await NotifyUser(NotificationTypes.ExistingUserName, nickName, "aborting");
+ return;
+ }
+ _cxManager.SetUserName( Context.ConnectionId, candidate);
}
bool IsPresent(string roomName, string userName)
@@ -132,11 +159,11 @@ namespace Yavsc.Server.Hubs
_logger.LogInformation($"Join:{roomName}");
if (!InputValidator.ValidateRoomName(roomName))
{
- _logger.LogError("!InputValidator.ValidateRoomName(roomName)");
+ _logger.LogError("!InputValidator.ValidateRoomName(roomName)");
return null;
}
- var roomGroupName = ChatHubConstants.HubGroupRoomsPrefix + roomName;
+ var roomGroupName = ChatHubConstants.HubGroupRomsPrefix + roomName;
var user = _cxManager.GetUserName(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, roomGroupName);
ChatRoomInfo channelInfo;
@@ -145,18 +172,12 @@ namespace Yavsc.Server.Hubs
_logger.LogInformation($"Joining");
channelInfo = _cxManager.Join(roomName, Context.ConnectionId);
await Clients.Group(roomGroupName).SendAsync("notifyRoom", NotificationTypes.UserJoin, roomName, user);
- }
- else
- {
+ } else {
_logger.LogInformation($"already present");
// in case in an additional connection,
// one only send info on room without
// warning any other user.
- if (!_cxManager.TryGetChanInfo(roomName, out channelInfo))
- {
- _logger.LogError($"Failed to retrieve channel info for room: {roomName}");
- return null;
- }
+ _cxManager.TryGetChanInfo(roomName, out channelInfo);
}
_logger.LogInformation($"returning channel info");
@@ -164,13 +185,14 @@ namespace Yavsc.Server.Hubs
return channelInfo;
}
+ [Authorize]
public void Register(string room)
{
- if (!InputValidator.ValidateRoomName(room)) return;
+ if (!InputValidator.ValidateRoomName(room)) return ;
var existent = _dbContext.ChatRoom.Any(r => r.Name == room);
if (existent)
{
- NotifyUserInRoom(NotificationTypes.Error, room, "already registered.");
+ NotifyUserInRoom(NotificationTypes.Error, room, "already registered.");
return;
}
Debug.Assert(Context.User != null);
@@ -190,25 +212,27 @@ namespace Yavsc.Server.Hubs
_dbContext.SaveChanges(user.Id);
}
- public void KickBan(string roomName, string userName, string reason)
+ [Authorize]
+ public void KickBan(string roomName, string userName, string reason)
{
- if (!InputValidator.ValidateRoomName(roomName)) return;
- if (!InputValidator.ValidateUserName(userName)) return;
+ if (!InputValidator.ValidateRoomName(roomName)) return ;
+ if (!InputValidator.ValidateUserName(userName)) return ;
if (!InputValidator.ValidateReason(reason)) return;
Kick(roomName, userName, reason);
Ban(roomName, userName, reason);
}
- public async Task Kick(string roomName, string userName, string reason)
+ [Authorize]
+ public async Task Kick(string roomName, string userName, string reason)
{
- if (!InputValidator.ValidateRoomName(roomName)) return;
- if (!InputValidator.ValidateUserName(userName)) return;
+ if (!InputValidator.ValidateRoomName(roomName)) return ;
+ if (!InputValidator.ValidateUserName(userName)) return ;
if (!InputValidator.ValidateReason(reason)) return;
ChatRoomInfo channelInfo;
- var roomGroupName = ChatHubConstants.HubGroupRoomsPrefix + roomName;
+ var roomGroupName = ChatHubConstants.HubGroupRomsPrefix + roomName;
if (_cxManager.TryGetChanInfo(roomName, out channelInfo))
{
- if (!_cxManager.IsPresent(roomName, userName))
+ if (!_cxManager.IsPresent(roomName,userName))
{
NotifyErrorToCallerInRoom(roomName, $"{userName} was not found in {roomName}.");
return;
@@ -218,44 +242,44 @@ namespace Yavsc.Server.Hubs
if (!_cxManager.Kick(Context.ConnectionId, userName, roomName, reason)) return;
}
var cxIds = _cxManager.GetConnexionIds(userName);
- if (cxIds != null) foreach (var cx in cxIds)
+ if (cxIds!=null) foreach(var cx in cxIds)
await Groups.RemoveFromGroupAsync(cx, roomGroupName);
await Clients.Group(roomGroupName).SendAsync("notifyRoom", NotificationTypes.Kick, roomName, $"{userName}: {reason}");
}
- public void Ban(string roomName, string userName, string reason)
+ [Authorize]
+ public void Ban(string roomName, string userName, string reason)
{
- if (!InputValidator.ValidateRoomName(roomName)) return;
- if (!InputValidator.ValidateUserName(userName)) return;
+ if (!InputValidator.ValidateRoomName(roomName)) return ;
+ if (!InputValidator.ValidateUserName(userName)) return ;
if (!InputValidator.ValidateReason(reason)) return;
var cxIds = _cxManager.GetConnexionIds(userName);
throw new NotImplementedException();
}
- [Authorize(Constants.AdminGroupName)]
- public void GLine(string userName, string reason)
+ [Authorize]
+ public void GLine(string userName, string reason)
{
- if (!InputValidator.ValidateUserName(userName)) return;
+ if (!InputValidator.ValidateUserName(userName)) return ;
if (!InputValidator.ValidateReason(reason)) return;
throw new NotImplementedException();
}
- public void Part(string roomName, string reason)
+ public void Part(string roomName, string reason)
{
- if (!InputValidator.ValidateRoomName(roomName)) return;
+ if (!InputValidator.ValidateRoomName(roomName)) return ;
if (!InputValidator.ValidateReason(reason)) return;
- if (_cxManager.Part(Context.ConnectionId, roomName, reason))
- {
- var roomGroupName = ChatHubConstants.HubGroupRoomsPrefix + roomName;
+ if (_cxManager.Part(Context.ConnectionId, roomName, reason))
+ {
+ var roomGroupName = ChatHubConstants.HubGroupRomsPrefix + roomName;
var group = Clients.Group(roomGroupName);
var userName = _cxManager.GetUserName(Context.ConnectionId);
group.SendAsync("notifyRoom", NotificationTypes.UserPart, roomName, $"{userName}: {reason}");
Groups.RemoveFromGroupAsync(Context.ConnectionId, roomGroupName);
- }
- else
- {
- _logger.LogError("Could not part");
- }
+ }
+ else {
+ _logger.LogError("Could not part");
+ }
}
void NotifyErrorToCallerInRoom(string room, string reason)
@@ -264,21 +288,19 @@ namespace Yavsc.Server.Hubs
_logger.LogError($"NotifyErrorToCallerInRoom: {room}, {reason}");
}
- public async Task Send(string roomName, string message)
+ public async Task Send(string roomName, string message)
{
_logger.LogInformation($"Send {roomName} {message}");
- if (!InputValidator.ValidateRoomName(roomName))
- {
+ if (!InputValidator.ValidateRoomName(roomName)) {
_logger.LogError($"Invalid roomName : {roomName}");
- return;
+ return ;
}
- if (!InputValidator.ValidateMessage(message))
- {
+ if (!InputValidator.ValidateMessage(message)) {
_logger.LogError($"Invalid message : {message}");
- return;
+ return ;
}
- var groupName = ChatHubConstants.HubGroupRoomsPrefix + roomName;
- ChatRoomInfo channelInfo;
+ var groupName = ChatHubConstants.HubGroupRomsPrefix + roomName;
+ ChatRoomInfo channelInfo ;
if (!_cxManager.TryGetChanInfo(roomName, out channelInfo))
{
_logger.LogError($"No such room : {roomName}");
@@ -295,7 +317,7 @@ namespace Yavsc.Server.Hubs
return;
}
var group = Clients.Group(groupName);
- var msg = new { Name = userName, Room = roomName, Message = message };
+ var msg = new { Name = userName, Room = roomName, Message = message};
await group.SendAsync("ReceiveMessage", msg);
}
@@ -310,7 +332,8 @@ namespace Yavsc.Server.Hubs
await Clients.Caller.SendAsync("notifyUserInRoom", type, room, message);
}
- public async Task SendPV(string userName, string message)
+ [Authorize]
+ public async Task SendPV(string userName, string message)
{
// Authorized code
Debug.Assert(Context.User != null);
@@ -319,17 +342,17 @@ namespace Yavsc.Server.Hubs
if (!InputValidator.ValidateUserName(userName))
{
_logger.LogError($"Invalid username : {userName}");
- return;
+ return ;
}
if (!InputValidator.ValidateMessage(message))
{
_logger.LogError($"Invalid message : {message}");
- return;
+ return ;
}
_logger.LogInformation($"Message form is validated.");
- var identityUserName = Context.User.GetUserName();
+ var identityUserName = Context.User.GetUserName();
- if (userName[0] != '?' && Context.User != null)
+ if (userName[0] != '?' && Context.User!=null)
if (!Context.User.IsInMsRole(Constants.AdminGroupName))
{
@@ -350,7 +373,7 @@ namespace Yavsc.Server.Hubs
_logger.LogInformation("getting cx id´s");
var cxIds = _cxManager.GetConnexionIds(userName);
- if (cxIds == null || cxIds.Count() == 0)
+ if (cxIds==null || cxIds.Count()==0)
_logger.LogError($"No such connected user : {userName}");
else foreach (var connectionId in cxIds)
{
@@ -362,18 +385,18 @@ namespace Yavsc.Server.Hubs
}
}
- public async Task SendStream(string connectionId, long streamId, string message)
+ [Authorize]
+ public async Task SendStream(string connectionId, long streamId, string message)
{
// Authorized code
Debug.Assert(Context.User != null);
Debug.Assert(Context.User.Identity != null);
if (!InputValidator.ValidateMessage(message)) return;
- var sender = Context.User.GetUserName();
+ var sender = Context.User.Identity.Name;
var cli = Clients.Client(connectionId);
await cli.SendAsync("addStreamInfo", sender, streamId, message);
}
}
}
-
-#pragma warning restore CS4014
+#pragma warning restore CS4014 // Dans la mesure où cet appel n'est pas attendu, l'exécution de la méthode actuelle continue avant la fin de l'appel
diff --git a/src/Yavsc.Server/Interfaces/IConnexionManager.cs b/src/Yavsc.Server/Interfaces/IConnexionManager.cs
index 4e13a50a5..434054249 100644
--- a/src/Yavsc.Server/Interfaces/IConnexionManager.cs
+++ b/src/Yavsc.Server/Interfaces/IConnexionManager.cs
@@ -3,8 +3,10 @@ using Yavsc.ViewModels.Chat;
namespace Yavsc.Services
{
public interface IConnexionManager {
+ void SetUserName(string cxId, string userName);
+
string GetUserName (string cxId);
- void OnConnected(string cxId, string userName, bool isCop);
+ void OnConnected(string cxId, bool isCop);
bool IsConnected(string candidate);
void OnDisconnected (string cxId);
diff --git a/src/Yavsc.Server/Services/HubConnectionManager.cs b/src/Yavsc.Server/Services/HubConnectionManager.cs
index 98d57085e..05ed49dda 100644
--- a/src/Yavsc.Server/Services/HubConnectionManager.cs
+++ b/src/Yavsc.Server/Services/HubConnectionManager.cs
@@ -53,15 +53,30 @@ namespace Yavsc.Services
_localizer = stringLocFactory.Create(typeof(HubConnectionManager));
}
- public void OnConnected(string cxId, string userName, bool isCop)
+ public void SetUserName(string cxId, string userName)
{
+ string oldUname;
+ if (ChatUserNames.TryGetValue(cxId, out oldUname))
+ {
+ // this is a rename
+ if (oldUname == userName) return;
+ ChatCxIds[userName] = ChatCxIds[oldUname];
+ ChatCxIds[oldUname] = null;
+ }
+ else
+ {
+ // this is a connexion
+ ChatCxIds[userName] = new List() { cxId };
+ }
ChatUserNames[cxId] = userName;
- ChatCxIds.AddOrUpdate(userName,
- _ => new List { cxId },
- (_, list) => { lock (list) { if (!list.Contains(cxId)) list.Add(cxId); } return list; });
- if (!IsConnected(userName))
- ChatRoomPresence[userName] = new List();
- _isCop[userName] = isCop;
+ }
+ // Username must have been set before calling this method.
+ public void OnConnected(string cxId, bool isCop)
+ {
+ var username = ChatUserNames[cxId];
+ if (!IsConnected(username))
+ ChatRoomPresence[username] = new List();
+ _isCop[username] = isCop;
}
public bool IsConnected(string candidate)