Ajoute les amis et les invitations temps reel
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject HttpClient Http
|
||||
@inject SocialRealtimeService Realtime
|
||||
|
||||
<PageTitle>ChessCubing Arena | Utilisateur</PageTitle>
|
||||
<PageBody BodyClass="home-body" />
|
||||
@@ -18,12 +19,11 @@
|
||||
<header class="hero hero-home user-hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Espace utilisateur</p>
|
||||
<h1>Gerer les donnees du site pour chaque joueur</h1>
|
||||
<h1>Profil joueur, amis et invitations</h1>
|
||||
<p class="lead">
|
||||
Cette page relie le compte connecte a une fiche utilisateur stockee
|
||||
cote serveur en MySQL. L'authentification reste geree par Keycloak,
|
||||
mais les informations metier du site sont maintenant pretes pour des
|
||||
evolutions futures.
|
||||
Cette page regroupe le profil du site, la gestion des amis ChessCubing,
|
||||
ainsi que les invitations recues ou envoyees. Les confirmations de partie
|
||||
sont ensuite synchronisees en direct avec SignalR.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button secondary" href="application.html">Ouvrir l'application</a>
|
||||
@@ -33,11 +33,11 @@
|
||||
|
||||
<aside class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Persistance</p>
|
||||
<strong>Profil du site stocke en MySQL</strong>
|
||||
<p class="micro-label">Temps reel</p>
|
||||
<strong>Presence et invitations synchronisees</strong>
|
||||
<p>
|
||||
Username et email restent lies au compte authentifie, pendant que
|
||||
le profil ChessCubing ajoute les donnees utiles au site.
|
||||
Les amis connectes apparaissent en direct, et les invitations de
|
||||
partie peuvent etre confirmees depuis un autre device.
|
||||
</p>
|
||||
</div>
|
||||
<div class="preview-banner">
|
||||
@@ -55,11 +55,12 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Connexion requise</p>
|
||||
<h2>Connecte-toi pour gerer ton profil</h2>
|
||||
<h2>Connecte-toi pour gerer ton profil et tes amis</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Utilise les boutons Se connecter ou Creer un compte dans le menu
|
||||
en haut de page, puis reviens ici pour enregistrer tes informations.
|
||||
en haut de page, puis reviens ici pour enregistrer tes informations
|
||||
et inviter d'autres joueurs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -86,15 +87,15 @@
|
||||
{
|
||||
<section class="panel panel-wide">
|
||||
<p class="eyebrow">Chargement</p>
|
||||
<h2>Recuperation du profil utilisateur</h2>
|
||||
<p class="section-copy">Le serveur recharge les donnees enregistrees pour ce compte.</p>
|
||||
<h2>Recuperation de l'espace utilisateur</h2>
|
||||
<p class="section-copy">Le serveur recharge le profil, les amis et les invitations de ce compte.</p>
|
||||
</section>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(LoadError))
|
||||
{
|
||||
<section class="panel panel-wide">
|
||||
<p class="eyebrow">Serveur</p>
|
||||
<h2>Impossible de charger le profil</h2>
|
||||
<h2>Impossible de charger l'espace utilisateur</h2>
|
||||
<p class="profile-feedback error">@LoadError</p>
|
||||
<div class="hero-actions">
|
||||
<button class="button secondary" type="button" @onclick="LoadProfileAsync">Reessayer</button>
|
||||
@@ -217,6 +218,235 @@
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide user-social-panel">
|
||||
<div class="section-heading user-profile-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Reseau joueur</p>
|
||||
<h2>Amis et invitations</h2>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions">
|
||||
<button class="button ghost small icon-button" type="button" title="Rafraichir" aria-label="Rafraichir" @onclick="ReloadSocialAsync">
|
||||
<span class="material-icons action-icon" aria-hidden="true">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="user-profile-note">Invite des joueurs du site, suis les invitations en cours et retrouve rapidement les amis connectes.</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SocialLoadError))
|
||||
{
|
||||
<p class="profile-feedback error">@SocialLoadError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SocialActionError))
|
||||
{
|
||||
<p class="profile-feedback error">@SocialActionError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SocialActionMessage))
|
||||
{
|
||||
<p class="profile-feedback success">@SocialActionMessage</p>
|
||||
}
|
||||
|
||||
<form class="social-search-form" @onsubmit="HandleSearchSubmit" @onsubmit:preventDefault="true">
|
||||
<label class="field">
|
||||
<span>Rechercher un joueur</span>
|
||||
<input @bind="SearchQuery" @bind:event="oninput" type="text" maxlength="80" placeholder="Nom, pseudo, club, ville..." />
|
||||
</label>
|
||||
|
||||
<div class="social-search-actions">
|
||||
<button class="button secondary small" type="submit" disabled="@IsSearching">
|
||||
@(IsSearching ? "Recherche..." : "Chercher")
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SearchError))
|
||||
{
|
||||
<p class="profile-feedback error">@SearchError</p>
|
||||
}
|
||||
|
||||
@if (SearchResults.Length > 0)
|
||||
{
|
||||
<div class="social-search-results">
|
||||
@foreach (var result in SearchResults)
|
||||
{
|
||||
<article class="social-item">
|
||||
<div class="social-item-meta">
|
||||
<div class="social-item-title-row">
|
||||
<strong>@result.DisplayName</strong>
|
||||
<span class="@BuildPresenceClass(result.Subject, result.IsOnline)">@BuildPresenceLabel(result.Subject, result.IsOnline)</span>
|
||||
</div>
|
||||
<span class="social-item-subtitle">@result.Username</span>
|
||||
@if (!string.IsNullOrWhiteSpace(result.Club) || !string.IsNullOrWhiteSpace(result.City))
|
||||
{
|
||||
<span class="social-item-caption">@JoinNonEmpty(result.Club, result.City)</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="social-item-actions">
|
||||
@if (result.IsFriend)
|
||||
{
|
||||
<span class="mini-chip admin-chip-neutral">Deja ami</span>
|
||||
}
|
||||
else if (FindReceivedInvitation(result.Subject) is { } receivedInvitation)
|
||||
{
|
||||
<button class="button secondary small" type="button" @onclick="() => AcceptInvitationAsync(receivedInvitation.InvitationId)">
|
||||
Accepter
|
||||
</button>
|
||||
<button class="button ghost small" type="button" @onclick="() => DeclineInvitationAsync(receivedInvitation.InvitationId)">
|
||||
Refuser
|
||||
</button>
|
||||
}
|
||||
else if (FindSentInvitation(result.Subject) is { } sentInvitation)
|
||||
{
|
||||
<span class="mini-chip admin-chip-outline">Invitation envoyee</span>
|
||||
<button class="button ghost small" type="button" @onclick="() => CancelInvitationAsync(sentInvitation.InvitationId)">
|
||||
Annuler
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="button secondary small" type="button" @onclick="() => SendInvitationAsync(result.Subject)">
|
||||
Inviter
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="social-columns">
|
||||
<section class="social-card">
|
||||
<div class="social-card-head">
|
||||
<strong>Amis</strong>
|
||||
<span class="mini-chip admin-chip-outline">@FriendCountLabel</span>
|
||||
</div>
|
||||
|
||||
@if (IsSocialLoading)
|
||||
{
|
||||
<p class="social-empty">Chargement des amis...</p>
|
||||
}
|
||||
else if (SocialOverview?.Friends.Length > 0)
|
||||
{
|
||||
<div class="social-list">
|
||||
@foreach (var friend in SocialOverview.Friends.OrderByDescending(friend => ResolveOnlineStatus(friend.Subject, friend.IsOnline)).ThenBy(friend => friend.DisplayName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
<article class="social-item">
|
||||
<div class="social-item-meta">
|
||||
<div class="social-item-title-row">
|
||||
<strong>@friend.DisplayName</strong>
|
||||
<span class="@BuildPresenceClass(friend.Subject, friend.IsOnline)">@BuildPresenceLabel(friend.Subject, friend.IsOnline)</span>
|
||||
</div>
|
||||
<span class="social-item-subtitle">@friend.Username</span>
|
||||
@if (!string.IsNullOrWhiteSpace(friend.Club) || !string.IsNullOrWhiteSpace(friend.City))
|
||||
{
|
||||
<span class="social-item-caption">@JoinNonEmpty(friend.Club, friend.City)</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="social-item-actions">
|
||||
<button class="button ghost small" type="button" @onclick="() => RemoveFriendAsync(friend.Subject)">
|
||||
Retirer
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="social-empty">Aucun ami pour le moment. Utilise la recherche ci-dessus pour commencer.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="social-card">
|
||||
<div class="social-card-head">
|
||||
<strong>Invitations recues</strong>
|
||||
<span class="mini-chip admin-chip-outline">@ReceivedCountLabel</span>
|
||||
</div>
|
||||
|
||||
@if (IsSocialLoading)
|
||||
{
|
||||
<p class="social-empty">Chargement des invitations...</p>
|
||||
}
|
||||
else if (SocialOverview?.ReceivedInvitations.Length > 0)
|
||||
{
|
||||
<div class="social-list">
|
||||
@foreach (var invitation in SocialOverview.ReceivedInvitations.OrderByDescending(invitation => invitation.CreatedUtc))
|
||||
{
|
||||
<article class="social-item">
|
||||
<div class="social-item-meta">
|
||||
<div class="social-item-title-row">
|
||||
<strong>@invitation.DisplayName</strong>
|
||||
<span class="@BuildPresenceClass(invitation.Subject, invitation.IsOnline)">@BuildPresenceLabel(invitation.Subject, invitation.IsOnline)</span>
|
||||
</div>
|
||||
<span class="social-item-subtitle">@invitation.Username</span>
|
||||
<span class="social-item-caption">Recue le @FormatDate(invitation.CreatedUtc)</span>
|
||||
</div>
|
||||
|
||||
<div class="social-item-actions">
|
||||
<button class="button secondary small" type="button" @onclick="() => AcceptInvitationAsync(invitation.InvitationId)">
|
||||
Accepter
|
||||
</button>
|
||||
<button class="button ghost small" type="button" @onclick="() => DeclineInvitationAsync(invitation.InvitationId)">
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="social-empty">Aucune invitation recue pour l'instant.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="social-card">
|
||||
<div class="social-card-head">
|
||||
<strong>Invitations envoyees</strong>
|
||||
<span class="mini-chip admin-chip-outline">@SentCountLabel</span>
|
||||
</div>
|
||||
|
||||
@if (IsSocialLoading)
|
||||
{
|
||||
<p class="social-empty">Chargement des invitations...</p>
|
||||
}
|
||||
else if (SocialOverview?.SentInvitations.Length > 0)
|
||||
{
|
||||
<div class="social-list">
|
||||
@foreach (var invitation in SocialOverview.SentInvitations.OrderByDescending(invitation => invitation.CreatedUtc))
|
||||
{
|
||||
<article class="social-item">
|
||||
<div class="social-item-meta">
|
||||
<div class="social-item-title-row">
|
||||
<strong>@invitation.DisplayName</strong>
|
||||
<span class="@BuildPresenceClass(invitation.Subject, invitation.IsOnline)">@BuildPresenceLabel(invitation.Subject, invitation.IsOnline)</span>
|
||||
</div>
|
||||
<span class="social-item-subtitle">@invitation.Username</span>
|
||||
<span class="social-item-caption">Envoyee le @FormatDate(invitation.CreatedUtc)</span>
|
||||
</div>
|
||||
|
||||
<div class="social-item-actions">
|
||||
<button class="button ghost small" type="button" @onclick="() => CancelInvitationAsync(invitation.InvitationId)">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="social-empty">Aucune invitation envoyee pour l'instant.</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
@@ -225,12 +455,22 @@
|
||||
private readonly UserProfileFormModel Form = new();
|
||||
|
||||
private UserProfileResponse? Profile;
|
||||
private SocialOverviewResponse? SocialOverview;
|
||||
private SocialSearchUserResponse[] SearchResults = [];
|
||||
private bool IsAuthenticated;
|
||||
private bool IsLoading = true;
|
||||
private bool IsSaving;
|
||||
private bool IsSocialLoading;
|
||||
private bool IsSearching;
|
||||
private int _knownSocialVersion;
|
||||
private string? LoadError;
|
||||
private string? SaveError;
|
||||
private string? SaveMessage;
|
||||
private string? SocialLoadError;
|
||||
private string? SocialActionError;
|
||||
private string? SocialActionMessage;
|
||||
private string? SearchError;
|
||||
private string SearchQuery = string.Empty;
|
||||
|
||||
private string HeroStatusTitle
|
||||
=> !IsAuthenticated
|
||||
@@ -241,20 +481,55 @@
|
||||
|
||||
private string HeroStatusDescription
|
||||
=> !IsAuthenticated
|
||||
? "Le profil du site apparait des qu'un compte joueur est connecte."
|
||||
? "Le profil du site et le reseau d'amis apparaissent des qu'un compte joueur est connecte."
|
||||
: IsLoading
|
||||
? "Le serveur verifie la fiche utilisateur associee a ce compte."
|
||||
: $"Compte lie a {Profile?.Username ?? "l'utilisateur connecte"} et stocke en base MySQL.";
|
||||
? "Le serveur verifie la fiche utilisateur et les relations sociales associees a ce compte."
|
||||
: $"Compte lie a {Profile?.Username ?? "l'utilisateur connecte"} avec synchronisation sociale en direct.";
|
||||
|
||||
private string FriendCountLabel => $"{SocialOverview?.Friends.Length ?? 0} ami(s)";
|
||||
|
||||
private string ReceivedCountLabel => $"{SocialOverview?.ReceivedInvitations.Length ?? 0} recue(s)";
|
||||
|
||||
private string SentCountLabel => $"{SocialOverview?.SentInvitations.Length ?? 0} envoyee(s)";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
|
||||
Realtime.Changed += HandleRealtimeChanged;
|
||||
await Realtime.EnsureStartedAsync();
|
||||
await LoadProfileAsync();
|
||||
}
|
||||
|
||||
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
|
||||
=> _ = InvokeAsync(LoadProfileAsync);
|
||||
|
||||
private void HandleRealtimeChanged()
|
||||
=> _ = InvokeAsync(HandleRealtimeChangedAsync);
|
||||
|
||||
private async Task HandleRealtimeChangedAsync()
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
{
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_knownSocialVersion != Realtime.SocialVersion)
|
||||
{
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
await LoadSocialOverviewAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SearchQuery))
|
||||
{
|
||||
await SearchUsersAsync();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadProfileAsync()
|
||||
{
|
||||
LoadError = null;
|
||||
@@ -268,6 +543,7 @@
|
||||
if (authState.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
ResetProfileState();
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,6 +553,7 @@
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
ResetProfileState();
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,6 +561,7 @@
|
||||
{
|
||||
LoadError = await ReadErrorAsync(response, "Le profil utilisateur n'a pas pu etre charge.");
|
||||
Profile = null;
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -291,20 +569,24 @@
|
||||
if (Profile is null)
|
||||
{
|
||||
LoadError = "Le serveur a retourne une reponse vide.";
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
FillForm(Profile);
|
||||
await LoadSocialOverviewAsync();
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
LoadError = "Le service utilisateur est temporairement indisponible.";
|
||||
Profile = null;
|
||||
ResetSocialState();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
LoadError = "La reponse du service utilisateur a pris trop de temps.";
|
||||
Profile = null;
|
||||
ResetSocialState();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -313,6 +595,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSocialOverviewAsync()
|
||||
{
|
||||
SocialLoadError = null;
|
||||
IsSocialLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync("api/social/overview");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
SocialLoadError = await ReadErrorAsync(response, "Le reseau social n'a pas pu etre charge.");
|
||||
SocialOverview = null;
|
||||
SearchResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
SocialOverview = await response.Content.ReadFromJsonAsync<SocialOverviewResponse>();
|
||||
SocialOverview ??= new SocialOverviewResponse();
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
SocialLoadError = "Le service social est temporairement indisponible.";
|
||||
SocialOverview = null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
SocialLoadError = "Le chargement du reseau social a pris trop de temps.";
|
||||
SocialOverview = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSocialLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveProfileAsync()
|
||||
{
|
||||
if (IsSaving || !IsAuthenticated)
|
||||
@@ -367,6 +686,165 @@
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleSearchSubmit()
|
||||
=> SearchUsersAsync();
|
||||
|
||||
private async Task SearchUsersAsync()
|
||||
{
|
||||
SearchError = null;
|
||||
SocialActionError = null;
|
||||
SocialActionMessage = null;
|
||||
|
||||
var normalizedQuery = SearchQuery.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedQuery))
|
||||
{
|
||||
SearchResults = [];
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
IsSearching = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync($"api/social/search?query={Uri.EscapeDataString(normalizedQuery)}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
SearchError = await ReadErrorAsync(response, "La recherche de joueurs a echoue.");
|
||||
SearchResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
SearchResults = await response.Content.ReadFromJsonAsync<SocialSearchUserResponse[]>() ?? [];
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
SearchError = "Le service social est temporairement indisponible.";
|
||||
SearchResults = [];
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
SearchError = "La recherche a pris trop de temps.";
|
||||
SearchResults = [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSearching = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadSocialAsync()
|
||||
{
|
||||
await LoadSocialOverviewAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SearchQuery))
|
||||
{
|
||||
await SearchUsersAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendInvitationAsync(string subject)
|
||||
{
|
||||
await RunSocialActionAsync(
|
||||
async () =>
|
||||
{
|
||||
var payload = new SendFriendInvitationRequest { TargetSubject = subject };
|
||||
var response = await Http.PostAsJsonAsync("api/social/invitations", payload);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre envoyee."));
|
||||
}
|
||||
},
|
||||
"Invitation d'ami envoyee.");
|
||||
}
|
||||
|
||||
private async Task AcceptInvitationAsync(long invitationId)
|
||||
{
|
||||
await RunSocialActionAsync(
|
||||
async () =>
|
||||
{
|
||||
var response = await Http.PostAsync($"api/social/invitations/{invitationId}/accept", content: null);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre acceptee."));
|
||||
}
|
||||
},
|
||||
"Invitation acceptee. Le joueur a ete ajoute aux amis.");
|
||||
}
|
||||
|
||||
private async Task DeclineInvitationAsync(long invitationId)
|
||||
{
|
||||
await RunSocialActionAsync(
|
||||
async () =>
|
||||
{
|
||||
var response = await Http.PostAsync($"api/social/invitations/{invitationId}/decline", content: null);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre refusee."));
|
||||
}
|
||||
},
|
||||
"Invitation refusee.");
|
||||
}
|
||||
|
||||
private async Task CancelInvitationAsync(long invitationId)
|
||||
{
|
||||
await RunSocialActionAsync(
|
||||
async () =>
|
||||
{
|
||||
var response = await Http.DeleteAsync($"api/social/invitations/{invitationId}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre annulee."));
|
||||
}
|
||||
},
|
||||
"Invitation annulee.");
|
||||
}
|
||||
|
||||
private async Task RemoveFriendAsync(string friendSubject)
|
||||
{
|
||||
await RunSocialActionAsync(
|
||||
async () =>
|
||||
{
|
||||
var response = await Http.DeleteAsync($"api/social/friends/{Uri.EscapeDataString(friendSubject)}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(await ReadErrorAsync(response, "L'ami n'a pas pu etre retire."));
|
||||
}
|
||||
},
|
||||
"Relation d'amitie retiree.");
|
||||
}
|
||||
|
||||
private async Task RunSocialActionAsync(Func<Task> action, string successMessage)
|
||||
{
|
||||
SocialActionError = null;
|
||||
SocialActionMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
await action();
|
||||
SocialActionMessage = successMessage;
|
||||
await LoadSocialOverviewAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SearchQuery))
|
||||
{
|
||||
await SearchUsersAsync();
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
SocialActionError = exception.Message;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
SocialActionError = "Le service social est temporairement indisponible.";
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
SocialActionError = "L'action sociale a pris trop de temps.";
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetProfileState()
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
@@ -374,6 +852,18 @@
|
||||
Form.Reset();
|
||||
}
|
||||
|
||||
private void ResetSocialState()
|
||||
{
|
||||
SocialOverview = null;
|
||||
SearchResults = [];
|
||||
SearchQuery = string.Empty;
|
||||
SearchError = null;
|
||||
SocialLoadError = null;
|
||||
SocialActionError = null;
|
||||
SocialActionMessage = null;
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
}
|
||||
|
||||
private void FillForm(UserProfileResponse profile)
|
||||
{
|
||||
Form.DisplayName = profile.DisplayName;
|
||||
@@ -384,6 +874,28 @@
|
||||
Form.Bio = profile.Bio;
|
||||
}
|
||||
|
||||
private bool ResolveOnlineStatus(string subject, bool fallbackStatus)
|
||||
=> Realtime.GetKnownOnlineStatus(subject) ?? fallbackStatus;
|
||||
|
||||
private string BuildPresenceClass(string subject, bool fallbackStatus)
|
||||
=> ResolveOnlineStatus(subject, fallbackStatus)
|
||||
? "presence-badge online"
|
||||
: "presence-badge";
|
||||
|
||||
private string BuildPresenceLabel(string subject, bool fallbackStatus)
|
||||
=> ResolveOnlineStatus(subject, fallbackStatus)
|
||||
? "En ligne"
|
||||
: "Hors ligne";
|
||||
|
||||
private SocialInvitationResponse? FindReceivedInvitation(string subject)
|
||||
=> SocialOverview?.ReceivedInvitations.FirstOrDefault(invitation => string.Equals(invitation.Subject, subject, StringComparison.Ordinal));
|
||||
|
||||
private SocialInvitationResponse? FindSentInvitation(string subject)
|
||||
=> SocialOverview?.SentInvitations.FirstOrDefault(invitation => string.Equals(invitation.Subject, subject, StringComparison.Ordinal));
|
||||
|
||||
private static string JoinNonEmpty(params string?[] values)
|
||||
=> string.Join(" • ", values.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response, string fallbackMessage)
|
||||
{
|
||||
try
|
||||
@@ -409,7 +921,10 @@
|
||||
=> value.ToLocalTime().ToString("dd MMM yyyy 'a' HH:mm", CultureInfo.GetCultureInfo("fr-FR"));
|
||||
|
||||
public void Dispose()
|
||||
=> AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
Realtime.Changed -= HandleRealtimeChanged;
|
||||
}
|
||||
|
||||
private sealed class ApiErrorMessage
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user