Ajoute les amis et les invitations temps reel
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject HttpClient Http
|
||||
@inject SocialRealtimeService Realtime
|
||||
|
||||
<PageTitle>ChessCubing Arena | Application</PageTitle>
|
||||
<PageBody Page="setup" BodyClass="@SetupBodyClass" />
|
||||
@@ -180,6 +181,93 @@
|
||||
<input @bind="Form.BlackName" @bind:event="oninput" name="blackName" type="text" maxlength="40" placeholder="Noir" />
|
||||
</label>
|
||||
|
||||
@if (IsAuthenticated)
|
||||
{
|
||||
<section class="setup-social-card span-2">
|
||||
<div class="social-card-head">
|
||||
<div>
|
||||
<span class="micro-label">Amis connectes</span>
|
||||
<strong>Inviter un ami a jouer</strong>
|
||||
</div>
|
||||
|
||||
<button class="button ghost small icon-button" type="button" title="Rafraichir les amis" aria-label="Rafraichir les amis" @onclick="LoadSocialOverviewAsync">
|
||||
<span class="material-icons action-icon" aria-hidden="true">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="social-empty">
|
||||
Choisis rapidement un ami en ligne et decide de quel cote il doit etre pre-rempli.
|
||||
</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SocialLoadError))
|
||||
{
|
||||
<p class="profile-feedback error">@SocialLoadError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(InviteActionError))
|
||||
{
|
||||
<p class="profile-feedback error">@InviteActionError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Realtime.LastInviteNotice))
|
||||
{
|
||||
<div class="social-inline-feedback">
|
||||
<span>@Realtime.LastInviteNotice</span>
|
||||
<button class="button ghost small" type="button" @onclick="Realtime.ClearInviteNotice">Masquer</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Realtime.OutgoingPlayInvite is not null)
|
||||
{
|
||||
<div class="play-invite-pending">
|
||||
<div>
|
||||
<span class="micro-label">En attente</span>
|
||||
<strong>@Realtime.OutgoingPlayInvite.RecipientDisplayName</strong>
|
||||
<p>Invitation envoyee pour jouer cote @(BuildColorLabel(Realtime.OutgoingPlayInvite.RecipientColor)).</p>
|
||||
</div>
|
||||
<button class="button ghost small" type="button" @onclick="CancelOutgoingPlayInviteAsync">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsSocialLoading)
|
||||
{
|
||||
<p class="social-empty">Chargement des amis connectes...</p>
|
||||
}
|
||||
else if (OnlineFriends.Length > 0)
|
||||
{
|
||||
<div class="play-friends-list">
|
||||
@foreach (var friend in OnlineFriends)
|
||||
{
|
||||
<article class="play-friend-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>
|
||||
</div>
|
||||
|
||||
<div class="play-friend-actions">
|
||||
<button class="button ghost small" type="button" disabled="@(Realtime.OutgoingPlayInvite is not null)" @onclick="() => InviteFriendAsWhiteAsync(friend.Subject)">
|
||||
Ami blanc
|
||||
</button>
|
||||
<button class="button ghost small" type="button" disabled="@(Realtime.OutgoingPlayInvite is not null)" @onclick="() => InviteFriendAsBlackAsync(friend.Subject)">
|
||||
Ami noir
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="social-empty">Aucun ami connecte. Gere tes amis sur la page utilisateur puis attends qu'ils se connectent.</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (Form.CompetitionMode)
|
||||
{
|
||||
<label class="field" id="arbiterField">
|
||||
@@ -287,12 +375,24 @@
|
||||
@code {
|
||||
private SetupFormModel Form { get; set; } = new();
|
||||
private bool _ready;
|
||||
private bool IsAuthenticated;
|
||||
private bool IsSocialLoading;
|
||||
private int _knownSocialVersion;
|
||||
private string? ConnectedPlayerName;
|
||||
private string? SocialLoadError;
|
||||
private string? InviteActionError;
|
||||
private SocialOverviewResponse? SocialOverview;
|
||||
|
||||
private MatchState? CurrentMatch => Store.Current;
|
||||
|
||||
private string SetupBodyClass => UsesMoveLimit ? string.Empty : "time-setup-mode";
|
||||
private bool CanUseConnectedPlayerName => !string.IsNullOrWhiteSpace(ConnectedPlayerName);
|
||||
private SocialFriendResponse[] OnlineFriends
|
||||
=> SocialOverview?.Friends
|
||||
.Where(friend => ResolveOnlineStatus(friend.Subject, friend.IsOnline))
|
||||
.OrderBy(friend => friend.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray()
|
||||
?? [];
|
||||
|
||||
private bool UsesMoveLimit => MatchEngine.UsesMoveLimit(Form.Mode);
|
||||
|
||||
@@ -324,7 +424,9 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
|
||||
await LoadConnectedPlayerAsync();
|
||||
Realtime.Changed += HandleRealtimeChanged;
|
||||
await Realtime.EnsureStartedAsync();
|
||||
await LoadApplicationContextAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -340,9 +442,26 @@
|
||||
}
|
||||
|
||||
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
|
||||
=> _ = InvokeAsync(LoadConnectedPlayerAsync);
|
||||
=> _ = InvokeAsync(LoadApplicationContextAsync);
|
||||
|
||||
private async Task LoadConnectedPlayerAsync()
|
||||
private void HandleRealtimeChanged()
|
||||
=> _ = InvokeAsync(HandleRealtimeChangedAsync);
|
||||
|
||||
private async Task HandleRealtimeChangedAsync()
|
||||
{
|
||||
ApplyAcceptedPlaySession();
|
||||
|
||||
if (IsAuthenticated && _knownSocialVersion != Realtime.SocialVersion)
|
||||
{
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
await LoadSocialOverviewAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadApplicationContextAsync()
|
||||
{
|
||||
string? fallbackName = null;
|
||||
|
||||
@@ -353,19 +472,30 @@
|
||||
|
||||
if (user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
ConnectedPlayerName = null;
|
||||
ResetSocialState();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
IsAuthenticated = true;
|
||||
fallbackName = BuildConnectedPlayerFallback(user);
|
||||
|
||||
var response = await Http.GetAsync("api/users/me");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
ConnectedPlayerName = response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden
|
||||
? null
|
||||
: fallbackName;
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
ConnectedPlayerName = null;
|
||||
ResetSocialState();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectedPlayerName = fallbackName;
|
||||
await LoadSocialOverviewAsync();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
@@ -375,15 +505,62 @@
|
||||
ConnectedPlayerName = !string.IsNullOrWhiteSpace(profile?.DisplayName)
|
||||
? profile.DisplayName
|
||||
: fallbackName;
|
||||
await LoadSocialOverviewAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ConnectedPlayerName = fallbackName;
|
||||
}
|
||||
|
||||
ApplyAcceptedPlaySession();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LoadSocialOverviewAsync()
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
{
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
IsSocialLoading = true;
|
||||
SocialLoadError = null;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync("api/social/overview");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
SocialLoadError = response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => "La session a expire. Reconnecte-toi puis recharge la page.",
|
||||
_ => "Le chargement des amis a echoue.",
|
||||
};
|
||||
SocialOverview = null;
|
||||
return;
|
||||
}
|
||||
|
||||
SocialOverview = await response.Content.ReadFromJsonAsync<SocialOverviewResponse>() ?? new SocialOverviewResponse();
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
SocialLoadError = "Le service social est temporairement indisponible.";
|
||||
SocialOverview = null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
SocialLoadError = "Le chargement des amis a pris trop de temps.";
|
||||
SocialOverview = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSocialLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
await Store.EnsureLoadedAsync();
|
||||
@@ -426,6 +603,48 @@
|
||||
Form.BlackName = ConnectedPlayerName!;
|
||||
}
|
||||
|
||||
private async Task InviteFriendToPlayAsync(string friendSubject, string recipientColor)
|
||||
{
|
||||
InviteActionError = null;
|
||||
|
||||
try
|
||||
{
|
||||
await Realtime.SendPlayInviteAsync(friendSubject, recipientColor);
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
InviteActionError = exception.Message;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
InviteActionError = string.IsNullOrWhiteSpace(exception.Message)
|
||||
? "L'invitation de partie n'a pas pu etre envoyee."
|
||||
: exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelOutgoingPlayInviteAsync()
|
||||
{
|
||||
InviteActionError = null;
|
||||
|
||||
try
|
||||
{
|
||||
await Realtime.CancelOutgoingPlayInviteAsync();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
InviteActionError = string.IsNullOrWhiteSpace(exception.Message)
|
||||
? "L'invitation de partie n'a pas pu etre annulee."
|
||||
: exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private Task InviteFriendAsWhiteAsync(string friendSubject)
|
||||
=> InviteFriendToPlayAsync(friendSubject, "white");
|
||||
|
||||
private Task InviteFriendAsBlackAsync(string friendSubject)
|
||||
=> InviteFriendToPlayAsync(friendSubject, "black");
|
||||
|
||||
private void SetMode(string mode)
|
||||
=> Form.Mode = mode;
|
||||
|
||||
@@ -470,6 +689,44 @@
|
||||
private string BuildPrefillTitle(string color)
|
||||
=> $"Utiliser mon nom cote {color}";
|
||||
|
||||
private void ApplyAcceptedPlaySession()
|
||||
{
|
||||
var session = Realtime.TakeAcceptedPlaySession();
|
||||
if (session is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Form.WhiteName = session.WhiteName;
|
||||
Form.BlackName = session.BlackName;
|
||||
}
|
||||
|
||||
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 static string BuildColorLabel(string color)
|
||||
=> string.Equals(color, "white", StringComparison.OrdinalIgnoreCase)
|
||||
? "blanc"
|
||||
: "noir";
|
||||
|
||||
private void ResetSocialState()
|
||||
{
|
||||
SocialOverview = null;
|
||||
SocialLoadError = null;
|
||||
InviteActionError = null;
|
||||
_knownSocialVersion = Realtime.SocialVersion;
|
||||
}
|
||||
|
||||
private static string? BuildConnectedPlayerFallback(ClaimsPrincipal user)
|
||||
=> FirstNonEmpty(
|
||||
user.FindFirst("name")?.Value,
|
||||
@@ -481,5 +738,8 @@
|
||||
=> candidates.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate));
|
||||
|
||||
public void Dispose()
|
||||
=> AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
Realtime.Changed -= HandleRealtimeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user