Files
RpgRoller/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs

304 lines
10 KiB
C#

using Microsoft.JSInterop;
using RpgRoller.Contracts;
namespace RpgRoller.Components.Pages;
public sealed class WorkspaceSessionCoordinator
{
public WorkspaceSessionCoordinator(
WorkspaceState state,
WorkspaceFeedbackService feedback,
IJSRuntime js,
RpgRollerApiClient apiClient,
WorkspaceQueryService workspaceQuery,
Func<Guid?, Task> reloadCampaignsAsync,
Func<Task> reloadCharacterCampaignOptionsAsync,
Func<Task> refreshCampaignScopeAsync,
Func<Task> syncStateEventsAsync,
Func<Task> stopStateEventsAsync,
Func<Task> ensureAdminUsersLoadedAsync,
Action resetCampaignLogDetailState,
Func<Task> requestRefreshAsync,
Func<string?, Task> onLoggedOutAsync)
{
m_State = state;
m_Feedback = feedback;
m_JS = js;
m_ApiClient = apiClient;
m_WorkspaceQuery = workspaceQuery;
m_ReloadCampaignsAsync = reloadCampaignsAsync;
m_ReloadCharacterCampaignOptionsAsync = reloadCharacterCampaignOptionsAsync;
m_RefreshCampaignScopeAsync = refreshCampaignScopeAsync;
m_SyncStateEventsAsync = syncStateEventsAsync;
m_StopStateEventsAsync = stopStateEventsAsync;
m_EnsureAdminUsersLoadedAsync = ensureAdminUsersLoadedAsync;
m_ResetCampaignLogDetailState = resetCampaignLogDetailState;
m_RequestRefreshAsync = requestRefreshAsync;
m_OnLoggedOutAsync = onLoggedOutAsync;
}
public async Task InitializeAsync()
{
var storedScreen = await m_JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", ScreenSessionKey);
m_State.CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay;
var storedPanel = await m_JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
m_State.MobilePanel = "log";
var storedRollVisibility = await m_JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", RollVisibilitySessionKey);
m_State.RollVisibility = NormalizeRollVisibility(storedRollVisibility);
Guid? preferredCampaignId = null;
var storedCampaignId = await m_JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
preferredCampaignId = parsedCampaignId;
await CheckHealthAsync();
await LoadRulesetsAsync();
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
if (!reloaded)
await m_OnLoggedOutAsync("Session expired. Please log in again.");
}
public async Task RetryAfterHealthIssueAsync()
{
await CheckHealthAsync();
if (!m_State.HasHealthIssue && m_State.User is not null)
{
var reloaded = await ReloadAuthenticatedSessionAsync(m_State.SelectedCampaignId);
if (!reloaded)
await m_OnLoggedOutAsync("Session expired. Please log in again.");
}
}
public async Task LoadKnownUsernamesAsync()
{
try
{
var usernames = await m_WorkspaceQuery.GetUsernamesAsync();
m_State.KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList();
}
catch (ApiRequestException ex)
{
m_State.KnownUsernames = [];
m_Feedback.SetStatus(ex.Message, true);
}
}
public async Task LogoutAsync()
{
if (m_State.IsMutating)
return;
m_State.IsMutating = true;
try
{
await m_ApiClient.RequestWithoutPayloadAsync("POST", "/api/auth/logout");
}
catch (ApiRequestException)
{
}
finally
{
m_State.IsMutating = false;
}
ClearAuthenticatedState();
await m_StopStateEventsAsync();
await m_OnLoggedOutAsync("Logged out.");
}
public async Task SwitchScreenAsync(string screen)
{
var targetScreen = NormalizeRequestedScreen(screen) ?? ScreenPlay;
if (string.Equals(targetScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase) && !m_State.IsCurrentUserAdmin)
targetScreen = ScreenPlay;
m_State.CurrentScreen = targetScreen;
m_State.IsScreenMenuOpen = false;
await PersistScreenPreferenceAsync(m_State.CurrentScreen);
await m_RequestRefreshAsync();
if (m_State.User is not null)
{
await m_RefreshCampaignScopeAsync();
await m_SyncStateEventsAsync();
}
if (m_State.IsAdminScreen)
{
await m_EnsureAdminUsersLoadedAsync();
await m_RequestRefreshAsync();
}
}
public async Task OnRollVisibilityChangedAsync(string visibility)
{
m_State.RollVisibility = NormalizeRollVisibility(visibility);
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, m_State.RollVisibility);
}
public void ClearAuthenticatedState()
{
m_State.User = null;
m_State.ActiveCharacterId = null;
m_State.SelectedCampaignId = null;
m_State.SelectedCampaign = null;
m_State.Campaigns = [];
m_State.CharacterCampaignOptions = [];
m_State.SelectedCharacterSkills = [];
m_State.SelectedCharacterSkillGroups = [];
m_State.CampaignLog = [];
m_State.CampaignLogCursor = null;
m_ResetCampaignLogDetailState();
m_State.SelectedCharacterId = null;
m_State.LastRoll = null;
m_State.KnownUsernames = [];
m_State.ShowCreateCharacterModal = false;
m_State.ShowEditCharacterModal = false;
m_State.CanEditCharacterOwner = false;
m_State.CreateCharacterInitialModel = new();
m_State.EditCharacterInitialModel = new();
m_State.CreateCharacterFormVersion = 0;
m_State.EditCharacterFormVersion = 0;
m_State.AdminUsers = [];
m_State.HasLoadedAdminUsers = false;
m_State.IsAdminDataLoading = false;
m_Feedback.ClearToasts();
}
private async Task CheckHealthAsync()
{
m_State.HasHealthIssue = false;
m_State.HealthIssueMessage = string.Empty;
await Task.CompletedTask;
}
private async Task LoadRulesetsAsync()
{
try
{
m_State.Rulesets = (await m_WorkspaceQuery.GetRulesetsAsync()).ToList();
}
catch (ApiRequestException ex)
{
m_Feedback.SetStatus(ex.Message, true);
}
}
private async Task<bool> ReloadAuthenticatedSessionAsync(Guid? preferredCampaignId)
{
var me = await TryGetMeAsync();
if (me is null)
{
ClearAuthenticatedState();
await m_StopStateEventsAsync();
return false;
}
m_State.User = me.User;
m_State.ActiveCharacterId = me.ActiveCharacterId;
await EnsureScreenAccessAsync();
await m_ReloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
await m_ReloadCharacterCampaignOptionsAsync();
await m_RefreshCampaignScopeAsync();
await m_SyncStateEventsAsync();
if (m_State.IsAdminScreen)
await m_EnsureAdminUsersLoadedAsync();
return true;
}
private async Task<MeResponse?> TryGetMeAsync()
{
try
{
return await m_WorkspaceQuery.GetMeAsync();
}
catch (ApiRequestException ex) when (ex.StatusCode == 401)
{
return null;
}
}
private async Task EnsureScreenAccessAsync()
{
if (m_State.IsCurrentUserAdmin)
return;
m_State.AdminUsers = [];
m_State.HasLoadedAdminUsers = false;
if (!m_State.IsAdminScreen)
return;
m_State.CurrentScreen = ScreenPlay;
await PersistScreenPreferenceAsync(m_State.CurrentScreen);
}
private async Task PersistScreenPreferenceAsync(string screen)
{
try
{
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, screen);
}
catch (JSDisconnectedException)
{
}
catch (InvalidOperationException ex) when (IsStaticRenderInteropException(ex))
{
}
}
private static string NormalizeRollVisibility(string? visibility)
{
return string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
}
private static string? NormalizeRequestedScreen(string? screen)
{
if (string.Equals(screen, ScreenAdmin, StringComparison.OrdinalIgnoreCase))
return ScreenAdmin;
if (string.Equals(screen, ScreenManagement, StringComparison.OrdinalIgnoreCase))
return ScreenManagement;
if (string.Equals(screen, ScreenPlay, StringComparison.OrdinalIgnoreCase))
return ScreenPlay;
return null;
}
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
{
return exception.Message.Contains("JavaScript interop calls cannot be issued", StringComparison.OrdinalIgnoreCase);
}
private readonly RpgRollerApiClient m_ApiClient;
private readonly WorkspaceFeedbackService m_Feedback;
private readonly Func<Task> m_EnsureAdminUsersLoadedAsync;
private readonly IJSRuntime m_JS;
private readonly Func<string?, Task> m_OnLoggedOutAsync;
private readonly Func<Task> m_ReloadCharacterCampaignOptionsAsync;
private readonly Func<Guid?, Task> m_ReloadCampaignsAsync;
private readonly Action m_ResetCampaignLogDetailState;
private readonly Func<Task> m_RefreshCampaignScopeAsync;
private readonly Func<Task> m_RequestRefreshAsync;
private readonly WorkspaceState m_State;
private readonly Func<Task> m_StopStateEventsAsync;
private readonly Func<Task> m_SyncStateEventsAsync;
private readonly WorkspaceQueryService m_WorkspaceQuery;
private const string ScreenPlay = "play";
private const string ScreenManagement = "management";
private const string ScreenAdmin = "admin";
private const string ScreenSessionKey = "screen";
private const string CampaignSessionKey = "campaign";
private const string MobilePanelSessionKey = "play-panel";
private const string RollVisibilitySessionKey = "roll-visibility";
}