mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-13 23:03:23 +00:00
Add WIP client
This commit is contained in:
@@ -1,42 +1,44 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="Any CPU"/>
|
||||
<Platform Name="x64"/>
|
||||
<Platform Name="x86"/>
|
||||
<Platform Name="Any CPU" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/DataMappings/">
|
||||
<Project Path="DataMappings/Spacebar.DataMappings.AdminApi/Spacebar.DataMappings.AdminApi.csproj"/>
|
||||
<Project Path="DataMappings/Spacebar.DataMappings.Generic/Spacebar.DataMappings.Generic.csproj"/>
|
||||
<Project Path="DataMappings/Spacebar.DataMappings.AdminApi/Spacebar.DataMappings.AdminApi.csproj" />
|
||||
<Project Path="DataMappings/Spacebar.DataMappings.Generic/Spacebar.DataMappings.Generic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Interop/">
|
||||
<Project Path="Interop/Spacebar.Interop.Authentication.AspNetCore/Spacebar.Interop.Authentication.AspNetCore.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Authentication/Spacebar.Interop.Authentication.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Cdn.Signing/Spacebar.Interop.Cdn.Signing.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.Abstractions/Spacebar.Interop.Replication.Abstractions.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.UnixSocket/Spacebar.Interop.Replication.UnixSocket.csproj"/>
|
||||
<Project Path="Interop/Spacebar.Interop.Authentication.AspNetCore/Spacebar.Interop.Authentication.AspNetCore.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Authentication/Spacebar.Interop.Authentication.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Cdn.Signing/Spacebar.Interop.Cdn.Signing.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.Abstractions/Spacebar.Interop.Replication.Abstractions.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj" />
|
||||
<Project Path="Interop/Spacebar.Interop.Replication.UnixSocket/Spacebar.Interop.Replication.UnixSocket.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Models/">
|
||||
<Project Path="Models/Spacebar.Models.AdminApi/Spacebar.Models.AdminApi.csproj"/>
|
||||
<Project Path="Models/Spacebar.Models.Config/Spacebar.Models.Config.csproj"/>
|
||||
<Project Path="Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj"/>
|
||||
<Project Path="Models/Spacebar.Models.Gateway/Spacebar.Models.Gateway.csproj"/>
|
||||
<Project Path="Models/Spacebar.Models.Generic/Spacebar.Models.Generic.csproj"/>
|
||||
<Project Path="Models/Spacebar.Models.AdminApi/Spacebar.Models.AdminApi.csproj" />
|
||||
<Project Path="Models/Spacebar.Models.Api/Spacebar.Models.Api.csproj" />
|
||||
<Project Path="Models/Spacebar.Models.Config/Spacebar.Models.Config.csproj" />
|
||||
<Project Path="Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj" />
|
||||
<Project Path="Models/Spacebar.Models.Gateway/Spacebar.Models.Gateway.csproj" />
|
||||
<Project Path="Models/Spacebar.Models.Generic/Spacebar.Models.Generic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Utilities/">
|
||||
<Project Path="Utilities/ConfigTest/ConfigTest.csproj"/>
|
||||
<Project Path="Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.Cdn.Migration/Spacebar.Cdn.Migration.csproj"/>
|
||||
<Project Path="Utilities/Spacebar.CleanSettingsRows/Spacebar.CleanSettingsRows.csproj"/>
|
||||
<Project Path="Utilities/ConfigTest/ConfigTest.csproj" />
|
||||
<Project Path="Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj" />
|
||||
<Project Path="Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj" />
|
||||
<Project Path="Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj" />
|
||||
<Project Path="Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj" />
|
||||
<Project Path="Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj" />
|
||||
<Project Path="Utilities/Spacebar.Cdn.Migration/Spacebar.Cdn.Migration.csproj" />
|
||||
<Project Path="Utilities/Spacebar.CleanSettingsRows/Spacebar.CleanSettingsRows.csproj" />
|
||||
<Project Path="Utilities/Spacebar.Client/Spacebar.Client.csproj" />
|
||||
</Folder>
|
||||
<Project Path="Spacebar.AdminApi/Spacebar.AdminApi.csproj"/>
|
||||
<Project Path="Spacebar.Cdn.Worker/Spacebar.Cdn.Worker.Q16-HDRI.x86_64.csproj" DisplayName="Spacebar.Cdn.Worker"/>
|
||||
<Project Path="Spacebar.Cdn/Spacebar.Cdn.csproj"/>
|
||||
<Project Path="Spacebar.Offload/Spacebar.Offload.csproj"/>
|
||||
<Project Path="Spacebar.UApi/Spacebar.UApi.csproj"/>
|
||||
<Project Path="Spacebar.AdminApi/Spacebar.AdminApi.csproj" />
|
||||
<Project Path="Spacebar.Cdn.Worker/Spacebar.Cdn.Worker.Q16-HDRI.x86_64.csproj" />
|
||||
<Project Path="Spacebar.Cdn/Spacebar.Cdn.csproj" />
|
||||
<Project Path="Spacebar.Offload/Spacebar.Offload.csproj" />
|
||||
<Project Path="Spacebar.UApi/Spacebar.UApi.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@using Spacebar.Client.Components
|
||||
|
||||
<SessionManager @ref="SessionManager"></SessionManager>
|
||||
<ClientManager @ref="ClientManager">
|
||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
</ClientManager>
|
||||
|
||||
|
||||
@code {
|
||||
public static SessionManager SessionManager { get; set; } = null!;
|
||||
public static ClientManager ClientManager { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@using ArcaneLibs.Blazor.Components.Services
|
||||
@using ArcaneLibs.Extensions
|
||||
@using Spacebar.Client.Core
|
||||
@using Spacebar.Client.WebCore
|
||||
@using Spacebar.Models.Gateway
|
||||
@inject SessionStore sessionStore
|
||||
@inject SpacebarClientProviderService clientProvider
|
||||
@inject JsConsoleService jsConsole
|
||||
|
||||
<DebugBanner Name="@GetType().Name" @ref="_dbgBanner"/>
|
||||
<CascadingValue TValue="AuthenticatedSpacebarClient" Value="@_client">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private DebugBanner _dbgBanner = null!;
|
||||
private AuthenticatedSpacebarClient? _client { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public required RenderFragment ChildContent { get; set; }
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (!firstRender) return;
|
||||
await _dbgBanner.SetStatus("Preparing for launch!");
|
||||
await Task.Delay(125);
|
||||
var session = await sessionStore.GetCurrentSessionAsync();
|
||||
if (session != null) {
|
||||
_client = await clientProvider.GetAuthenticatedClientAsync(session.ServerName, session.AccessToken);
|
||||
await _dbgBanner.SetStatus($"Got authenticated client for {session.ProfileCache.Username}#{session.ProfileCache.Discriminator} on {session.ServerName}! Connecting to gateway...");
|
||||
_client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() {
|
||||
HasClientMods = false,
|
||||
ApplicationArchitecture = "wasm"
|
||||
}.ToJsonNode().AsObject();
|
||||
StateHasChanged();
|
||||
await _client.Gateway.Connect();
|
||||
_ = _client.Gateway.Start().ContinueWith(ct => {
|
||||
jsConsole.Warn("[ClientManager] Heartbeat loop exited!");
|
||||
if (ct.IsFaulted) {
|
||||
jsConsole.Error("Unhandled exception during gateway connection:", ct.Exception.ToString());
|
||||
throw ct.Exception;
|
||||
}
|
||||
});
|
||||
_client.Gateway.OnceGatewayMessage.Add(async msg => {
|
||||
if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) {
|
||||
await _dbgBanner.SetStatus($"Got READY from gateway");
|
||||
await _dbgBanner.SetStatus(null, 1750);
|
||||
var content = msg.GetData<ReadyResponse>();
|
||||
await jsConsole.Info("Parsed ready payload:", content);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
else {
|
||||
await _dbgBanner.SetStatus("No session marked as current... :(");
|
||||
await _dbgBanner.SetStatus(null, 1750);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<span class="@("dbgBanner " + (_bannerVisible ? "" : "hidden"))"><b class="code">@(Name)</b>: @_status</span>
|
||||
|
||||
@code {
|
||||
|
||||
private bool _bannerVisible = true;
|
||||
private string? _status = "initializing...";
|
||||
|
||||
[Parameter]
|
||||
public required string Name { get; set; }
|
||||
|
||||
public async Task SetStatus(string? message, int closeDelay = 0) {
|
||||
_bannerVisible = !string.IsNullOrWhiteSpace(message);
|
||||
if (!_bannerVisible) {
|
||||
await Task.Delay(closeDelay);
|
||||
StateHasChanged();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
_status = message;
|
||||
StateHasChanged();
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@using ArcaneLibs.Blazor.Components
|
||||
@using ArcaneLibs.Blazor.Components.Services
|
||||
@using Spacebar.Client.WebCore
|
||||
@inject SessionStore sessionStore
|
||||
@inject JsConsoleService jsConsole
|
||||
|
||||
<DebugBanner Name="@GetType().Name" @ref="_dbgBanner"/>
|
||||
@if (_sessionPickerVisible) {
|
||||
<ModalWindow Title="Session manager" OnCloseClickedAsync="@(() => SetSessionPickerVisible(false))">
|
||||
@foreach (var (sessionId, session) in _sessionEntries) {
|
||||
<LinkButton
|
||||
OnClickAsync="@(() => SetCurrentSessionAsync(sessionId))">@(session.ProfileCache?.Username ?? "Unknown user")#@(session.ProfileCache?.Discriminator ?? "0000") <sub
|
||||
class="code">on @session.ServerName</sub></LinkButton>
|
||||
}
|
||||
<br/><br/>
|
||||
<LinkButton href="/login">Log in</LinkButton>
|
||||
<LinkButton href="/register">Register</LinkButton>
|
||||
</ModalWindow>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _sessionPickerVisible;
|
||||
private DebugBanner _dbgBanner;
|
||||
|
||||
private Dictionary<Guid, SessionEntry> _sessionEntries = [];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (!firstRender) return;
|
||||
await _dbgBanner.SetStatus("Crunching numbers...");
|
||||
_sessionPickerVisible = await sessionStore.GetCurrentSessionAsync() == null;
|
||||
await _dbgBanner.SetStatus("Ready!");
|
||||
await _dbgBanner.SetStatus(null, 1000);
|
||||
}
|
||||
|
||||
public async Task SetSessionPickerVisible(bool visible = true) {
|
||||
await jsConsole.Info("[SessionManager] showing session picker...");
|
||||
_sessionEntries = visible ? await sessionStore.GetAllSessionsAsync() : [];
|
||||
_sessionPickerVisible = visible;
|
||||
StateHasChanged();
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
private async Task SetCurrentSessionAsync(Guid sessionId) {
|
||||
_sessionPickerVisible = false;
|
||||
StateHasChanged();
|
||||
await sessionStore.SetCurrentSessionAsync(sessionId);
|
||||
var cs = await sessionStore.GetCurrentSessionAsync();
|
||||
await _dbgBanner.SetStatus($"Current session changed to {cs!.ProfileCache?.Username}#{cs.ProfileCache?.Discriminator} ({cs.ProfileCache.Id}) / {sessionId.ToString()}!");
|
||||
await _dbgBanner.SetStatus(null, 3000);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ArcaneLibs.Extensions;
|
||||
using Spacebar.Models.Api;
|
||||
using Spacebar.Models.Gateway;
|
||||
using Spacebar.Models.Generic;
|
||||
|
||||
namespace Spacebar.Client.Core;
|
||||
|
||||
public class UnauthenticatedSpacebarClient(ILogger<UnauthenticatedSpacebarClient> logger, SpacebarClientWellKnown wellKnown) {
|
||||
public async Task<LoginResponse> LoginAsync(LoginRequest request) {
|
||||
// TODO: rebase
|
||||
using var hc = new HttpClient();
|
||||
var resp = await hc.PostAsJsonAsync(new Uri(wellKnown.Api.GetApiBaseUrl(), "auth/login"), request);
|
||||
// TODO: abstract out
|
||||
if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync<JsonObject>())!);
|
||||
return (await resp.Content.ReadFromJsonAsync<LoginResponse>())!;
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthenticatedSpacebarClient {
|
||||
private readonly ILogger<AuthenticatedSpacebarClient> _logger;
|
||||
|
||||
public AuthenticatedSpacebarClient(ILogger<AuthenticatedSpacebarClient> logger, IServiceProvider sp, SpacebarClientWellKnown wellKnown, string token) {
|
||||
_logger = logger;
|
||||
ApiHttpClient = new HttpClient() {
|
||||
BaseAddress = wellKnown.Api.GetApiBaseUrl()
|
||||
};
|
||||
ApiHttpClient.DefaultRequestHeaders.Authorization = new("Bearer", token);
|
||||
Gateway = new(sp.GetRequiredService<ILogger<AuthenticatedSpacebarGatewayClient>>(), wellKnown, token);
|
||||
}
|
||||
|
||||
public HttpClient ApiHttpClient { get; set; }
|
||||
public AuthenticatedSpacebarGatewayClient Gateway { get; set; }
|
||||
|
||||
// TODO: write a proper full user model...
|
||||
public async Task<PartialUser> GetCurrentUser() {
|
||||
var resp = await ApiHttpClient.GetAsync("users/@me");
|
||||
// TODO: abstract out
|
||||
if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync<JsonObject>())!);
|
||||
return (await resp.Content.ReadFromJsonAsync<PartialUser>())!;
|
||||
}
|
||||
|
||||
~AuthenticatedSpacebarClient() {
|
||||
ApiHttpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGatewayClient> logger, SpacebarClientWellKnown wellKnown, string token) {
|
||||
public ClientWebSocket RawClientWebSocket = new();
|
||||
public int Sequence;
|
||||
public bool TraceGatewayMessages = false;
|
||||
|
||||
public IdentifyRequest IdentifyData { get; } = new() {
|
||||
Intents = (GatewayIntentFlags?)0xFFFFFFFF, // too lazy to do math, just gimme everything
|
||||
Capabilities = 0
|
||||
};
|
||||
|
||||
public List<Func<GatewayPayload, Task>> OnGatewayMessage { get; } = [];
|
||||
public List<Func<GatewayPayload, Task<bool>>> OnceGatewayMessage { get; } = [];
|
||||
|
||||
public async Task Connect() {
|
||||
if (RawClientWebSocket.State is WebSocketState.Connecting or WebSocketState.Open) return;
|
||||
Sequence = 0;
|
||||
await RawClientWebSocket.ConnectAsync(new Uri(wellKnown.Gateway.BaseUrl).AddQuery("encoding", "json"), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task Start() {
|
||||
await foreach (var msg in _runReceiveLoop()) {
|
||||
// logger.LogInformation("Got gateway message: {msg}", msg);
|
||||
if (msg.Opcode == GatewayOpcode.S2CHello) {
|
||||
_ = _runHeartbeatLoop(msg.GetData<HelloResponse>()!.HeartbeatInterval).ContinueWith(ct => {
|
||||
logger.LogWarning("Heartbeat loop exited!");
|
||||
if (ct.IsFaulted) throw ct.Exception;
|
||||
});
|
||||
IdentifyData.Token = token;
|
||||
await RawClientWebSocket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new GatewayPayload() {
|
||||
Opcode = GatewayOpcode.C2SIdentify, // Identify
|
||||
EventData = IdentifyData.ToJsonNode().AsObject()
|
||||
}), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
|
||||
}
|
||||
else if (msg.Opcode == GatewayOpcode.S2CHeartbeatAck) {
|
||||
logger.LogInformation("Got heartbeat ACK from server!");
|
||||
}
|
||||
|
||||
await Task.WhenAll(OnGatewayMessage.Select(x => x(msg)).ToArray());
|
||||
foreach (var t in OnceGatewayMessage.Select((Func<GatewayPayload, Task<bool>> Callback, Task<bool> WasHandled) (cb) => (cb, cb(msg))).ToList()) {
|
||||
var handled = await t.WasHandled;
|
||||
if (handled) {
|
||||
OnceGatewayMessage.Remove(t.Callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _runHeartbeatLoop(int interval) {
|
||||
while (RawClientWebSocket.State < WebSocketState.Closed) {
|
||||
await RawClientWebSocket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new GatewayPayload() {
|
||||
Opcode = GatewayOpcode.C2SQoSHeartbeat, // QoS Heartbeat
|
||||
EventData = new QoSHeartbeatRequest() {
|
||||
Sequence = Sequence,
|
||||
QoSPayload = new() {
|
||||
Active = true,
|
||||
Reasons = ["foregrounded"],
|
||||
Version = 27
|
||||
}
|
||||
}.ToJsonNode().AsObject()
|
||||
}), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
|
||||
await Task.Delay(interval);
|
||||
}
|
||||
}
|
||||
|
||||
private const int ReceiveBufferSize = 2 * 1024 * 1024;
|
||||
|
||||
private async IAsyncEnumerable<GatewayPayload> _runReceiveLoop() {
|
||||
List<byte> messageParts = [];
|
||||
List<(string Name, TimeSpan Elapsed)> trace = [];
|
||||
var buffer = new byte[ReceiveBufferSize];
|
||||
int idx = 0;
|
||||
|
||||
while (RawClientWebSocket.State < WebSocketState.Closed) {
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var msg = await RawClientWebSocket.ReceiveAsync(buffer, CancellationToken.None);
|
||||
trace.Add(($"RCV.{idx}", sw.GetElapsedAndRestart()));
|
||||
|
||||
Console.WriteLine($"Websocket message chunk read: {msg.MessageType} {msg.Count} {msg.EndOfMessage}");
|
||||
messageParts.AddRange(msg.Count < ReceiveBufferSize ? buffer[..msg.Count] : buffer);
|
||||
trace.Add(($"STO.{idx}({msg.Count})", sw.GetElapsedAndRestart()));
|
||||
idx++;
|
||||
|
||||
if (msg.EndOfMessage) {
|
||||
Console.WriteLine("Got message, deserialising...");
|
||||
var fullMsg = messageParts.ToArray();
|
||||
trace.Add(($"LD({messageParts.Count})", sw.GetElapsedAndRestart()));
|
||||
|
||||
var d = JsonSerializer.Deserialize<GatewayPayload>(fullMsg);
|
||||
trace.Add(($"LJS({fullMsg.Length})", sw.GetElapsedAndRestart()));
|
||||
|
||||
yield return d ?? throw new InvalidDataException("Gateway message deserialisation returned null?");
|
||||
trace.Add(($"YLD", sw.GetElapsedAndRestart()));
|
||||
|
||||
if (TraceGatewayMessages) {
|
||||
Console.WriteLine("Yielded message!");
|
||||
Console.WriteLine("Trace:");
|
||||
foreach (var t in trace)
|
||||
Console.WriteLine($" - {t.Name}: {t.Elapsed}");
|
||||
}
|
||||
|
||||
messageParts.Clear();
|
||||
trace.Clear();
|
||||
trace.Add(($"RST", sw.GetElapsedAndRestart()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~AuthenticatedSpacebarGatewayClient() {
|
||||
RawClientWebSocket.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using ArcaneLibs.Collections;
|
||||
|
||||
namespace Spacebar.Client.Core;
|
||||
|
||||
public class SpacebarClientProviderService(ILogger<SpacebarClientProviderService> logger, IServiceProvider serviceProvider, SpacebarClientWellKnownResolverService clientWellKnownResolver) {
|
||||
private static readonly SemaphoreCache<UnauthenticatedSpacebarClient> UnauthenticatedClientCache = new();
|
||||
private static readonly SemaphoreCache<AuthenticatedSpacebarClient> AuthenticatedClientCache = new();
|
||||
|
||||
public async Task<UnauthenticatedSpacebarClient> GetUnauthenticatedClientAsync(string serverName) {
|
||||
return await UnauthenticatedClientCache.GetOrAdd(serverName, async () => {
|
||||
logger.LogInformation("Creating a new unauthenticated client for {serverName}!", serverName);
|
||||
var clientLogger = serviceProvider.GetRequiredService<ILogger<UnauthenticatedSpacebarClient>>();
|
||||
var wellKnown = await clientWellKnownResolver.ResolveClientWellKnown(serverName);
|
||||
return new UnauthenticatedSpacebarClient(clientLogger, wellKnown);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AuthenticatedSpacebarClient> GetAuthenticatedClientAsync(string serverName, string accessToken) {
|
||||
return await AuthenticatedClientCache.GetOrAdd(serverName, async () => {
|
||||
logger.LogInformation("Creating a new authenticated client for {serverName}!", serverName);
|
||||
var clientLogger = serviceProvider.GetRequiredService<ILogger<AuthenticatedSpacebarClient>>();
|
||||
var wellKnown = await clientWellKnownResolver.ResolveClientWellKnown(serverName);
|
||||
return new AuthenticatedSpacebarClient(clientLogger, serviceProvider, wellKnown, accessToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Spacebar.Client.Core;
|
||||
|
||||
public class SpacebarClientWellKnownResolverService(ILogger<SpacebarClientWellKnownResolverService> logger) {
|
||||
public async Task<SpacebarClientWellKnown> ResolveClientWellKnown(string serverName) {
|
||||
using var hc = new HttpClient();
|
||||
logger.LogInformation("Resolving .well-known for {serverName}", serverName);
|
||||
return await hc.GetFromJsonAsync<SpacebarClientWellKnown>($"https://{serverName}/.well-known/spacebar/client")!;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpacebarClientWellKnown {
|
||||
[JsonPropertyName("api")]
|
||||
public required ApiWellKnownData Api { get; set; }
|
||||
|
||||
[JsonPropertyName("cdn")]
|
||||
public required GenericUrlWellKnownData Cdn { get; set; }
|
||||
|
||||
[JsonPropertyName("admin")]
|
||||
public GenericUrlWellKnownData? Admin { get; set; }
|
||||
|
||||
[JsonPropertyName("gateway")]
|
||||
public required GatewayWellKnownData Gateway { get; set; }
|
||||
|
||||
public class GenericUrlWellKnownData {
|
||||
[JsonPropertyName("baseUrl")]
|
||||
public required string BaseUrl { get; set; }
|
||||
}
|
||||
|
||||
public class ApiWellKnownData : GenericUrlWellKnownData {
|
||||
[JsonPropertyName("apiVersions")]
|
||||
public required ApiVersionsData ApiVersions { get; set; }
|
||||
|
||||
// Utility methods
|
||||
public Uri GetApiBaseUrl(string? version = null) {
|
||||
return new Uri(BaseUrl + "/api/v" + (version ?? ApiVersions.Default));
|
||||
}
|
||||
|
||||
public class ApiVersionsData {
|
||||
[JsonPropertyName("default")]
|
||||
public required string Default { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public required List<string> Active { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class GatewayWellKnownData : GenericUrlWellKnownData {
|
||||
[JsonPropertyName("encoding")]
|
||||
public required List<string> Encoding { get; set; }
|
||||
|
||||
[JsonPropertyName("compression")]
|
||||
public required List<string?> Compression { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">Spacebar.Client</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu() {
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
@page "/login"
|
||||
@using ArcaneLibs.Blazor.Components
|
||||
@using ArcaneLibs.Extensions
|
||||
@using Spacebar.Client.Core
|
||||
@using Spacebar.Client.WebCore
|
||||
@inject SpacebarClientWellKnownResolverService clientWellKnownResolver
|
||||
@inject SessionStore sessionStore
|
||||
@inject SpacebarClientProviderService clientProvider
|
||||
|
||||
<PageTitle>Log in</PageTitle>
|
||||
|
||||
<span>Instance (server name):</span>
|
||||
<FancyTextBox @bind-Value="@ServerName"/>
|
||||
<LinkButton InlineText="true">[🧭 Discover]</LinkButton>
|
||||
<br/>
|
||||
<span>Email:</span>
|
||||
<FancyTextBox @bind-Value="@Username"/>
|
||||
<br/>
|
||||
<span>Password:</span>
|
||||
<FancyTextBox @bind-Value="@Password" IsPassword="true"/>
|
||||
<br/>
|
||||
<LinkButton OnClickAsync="LoginAsync">Log in</LinkButton>
|
||||
|
||||
<pre>@ServerValidationStatus</pre>
|
||||
|
||||
@code{
|
||||
|
||||
private string ServerName {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
CheckServer(value);
|
||||
}
|
||||
} = "spacebar.chat";
|
||||
|
||||
private string Username { get; set; } = "";
|
||||
private string Password { get; set; } = "";
|
||||
|
||||
private string? ServerValidationStatus;
|
||||
|
||||
private async Task SetStatus(string? status) {
|
||||
ServerValidationStatus = status;
|
||||
StateHasChanged();
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
private async Task CheckServer(string serverName) {
|
||||
await SetStatus("Checking server: " + serverName);
|
||||
try {
|
||||
await clientWellKnownResolver.ResolveClientWellKnown(serverName);
|
||||
await SetStatus($"Server {serverName} is valid!");
|
||||
}
|
||||
catch (Exception e) {
|
||||
await SetStatus("Could not validate server: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoginAsync() {
|
||||
try {
|
||||
await SetStatus($"Looking up {ServerName}...");
|
||||
var cwk = await clientWellKnownResolver.ResolveClientWellKnown(ServerName);
|
||||
|
||||
await SetStatus($"Trying to log in to {ServerName}...");
|
||||
var usc = await clientProvider.GetUnauthenticatedClientAsync(ServerName);
|
||||
var lrsp = await usc.LoginAsync(new() {
|
||||
Login = Username,
|
||||
Password = Password
|
||||
});
|
||||
|
||||
await SetStatus($"Logged in as user ID {lrsp.UserId}, fetching profile...");
|
||||
var asc = await clientProvider.GetAuthenticatedClientAsync(ServerName, lrsp.Token!);
|
||||
var cu = await asc.GetCurrentUser();
|
||||
await SetStatus($"Logged in as {cu.Username}#{cu.Discriminator} ({cu.Id})! Saving...");
|
||||
await sessionStore.AddSession(new SessionEntry() {
|
||||
ServerName = ServerName,
|
||||
AccessToken = lrsp.Token!,
|
||||
ProfileCache = new() {
|
||||
Id = cu.Id,
|
||||
Username = cu.Username,
|
||||
Discriminator = cu.Discriminator,
|
||||
AvatarUrl = cu.Avatar ?? "",
|
||||
}
|
||||
}, setCurrent: true);
|
||||
|
||||
// await SetStatus(lrsp.ToJson());
|
||||
}
|
||||
catch (Exception e) {
|
||||
await SetStatus("Failed to log in: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@* Workaround for Rider "Bad compile constant value" bug *@
|
||||
@attribute [Route(PageUri)]
|
||||
@using ArcaneLibs.Blazor.Components.Services
|
||||
@using Spacebar.Client.Core
|
||||
@using Spacebar.Models.Gateway
|
||||
@inject JsConsoleService jsConsole
|
||||
|
||||
<h3>@@me</h3>
|
||||
<p>@Client?.ApiHttpClient.BaseAddress</p>
|
||||
<p>@Client?.Gateway.RawClientWebSocket.State</p>
|
||||
|
||||
@code {
|
||||
private const string PageUri = "/channels/@me";
|
||||
|
||||
[Parameter, CascadingParameter]
|
||||
public required AuthenticatedSpacebarClient? Client {
|
||||
get;
|
||||
set { field = value; Console.WriteLine("Set client: " + value); }
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
if (Client == null) return;
|
||||
Client.Gateway.OnGatewayMessage.Add(async msg => {
|
||||
await jsConsole.Log("Received gateway message: ", msg);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@page "/"
|
||||
@page "/app"
|
||||
@using ArcaneLibs.Blazor.Components
|
||||
@using ArcaneLibs.Extensions
|
||||
@using Spacebar.Client.Core
|
||||
@using Spacebar.Client.WebCore
|
||||
@inject SpacebarClientWellKnownResolverService cswkrs
|
||||
@inject SessionStore sessionStore
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
<LinkButton OnClickAsync="@(()=>App.SessionManager.SetSessionPickerVisible())">Trigger session picker</LinkButton>
|
||||
<pre>@Ss.ToJson()</pre>
|
||||
<pre>@Res.ToJson()</pre>
|
||||
|
||||
@code{
|
||||
private SpacebarClientWellKnown? Res { get; set; }
|
||||
private SessionEntry? Ss { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
Ss = await sessionStore.GetCurrentSessionAsync();
|
||||
Res = await cswkrs.ResolveClientWellKnown(Ss?.ServerName ?? "spacebar.chat");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,19 @@
|
||||
using ArcaneLibs.Blazor.Components.Services;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Spacebar.Client;
|
||||
using Spacebar.Client.Core;
|
||||
using Spacebar.Client.WebCore;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddSingleton<LocalStorageService>();
|
||||
builder.Services.AddSingleton<JsConsoleService>();
|
||||
builder.Services.AddSingleton<SessionStore>();
|
||||
builder.Services.AddSingleton<SpacebarClientProviderService>();
|
||||
builder.Services.AddSingleton<SpacebarClientWellKnownResolverService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5086",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7015;http://localhost:5086",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||
|
||||
<!-- copied from https://cgit.rory.gay/matrix/tools/MatrixUtils.git/tree/MatrixUtils.Web/MatrixUtils.Web.csproj -->
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
<UseBlazorWebAssembly>true</UseBlazorWebAssembly>
|
||||
<BlazorEnableCompression>false</BlazorEnableCompression>
|
||||
<CompressionEnabled>false</CompressionEnabled>
|
||||
<BlazorCacheBootResources>false</BlazorCacheBootResources>
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<WasmEnableHotReload>false</WasmEnableHotReload>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.1-preview.20260505-124817"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Models\Spacebar.Models.AdminApi\Spacebar.Models.AdminApi.csproj"/>
|
||||
<ProjectReference Include="..\..\Models\Spacebar.Models.Api\Spacebar.Models.Api.csproj"/>
|
||||
<ProjectReference Include="..\..\Models\Spacebar.Models.Gateway\Spacebar.Models.Gateway.csproj"/>
|
||||
<ProjectReference Include="..\..\Models\Spacebar.Models.Generic\Spacebar.Models.Generic.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using ArcaneLibs.Blazor.Components.Services;
|
||||
|
||||
namespace Spacebar.Client.WebCore;
|
||||
|
||||
public class SessionStore(ILogger<SessionStore> logger, LocalStorageService localStorage) {
|
||||
public const string CurrentSessionKey = "chat.spacebar.client.current_session";
|
||||
public const string AllSessionsKey = "chat.spacebar.client.sessions";
|
||||
public async Task<SessionEntry?> GetCurrentSessionAsync() {
|
||||
if (!await localStorage.ContainsKeyAsync(CurrentSessionKey)) return null;
|
||||
var entryId = await localStorage.GetItemFromJsonAsync<Guid>(CurrentSessionKey);
|
||||
var entries = await GetAllSessionsAsync();
|
||||
return entries[entryId];
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, SessionEntry>> GetAllSessionsAsync() {
|
||||
if (!await localStorage.ContainsKeyAsync(AllSessionsKey)) return [];
|
||||
var data = await localStorage.GetItemFromJsonAsync<Dictionary<Guid, SessionEntry>>(AllSessionsKey);
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
public async Task AddSession(SessionEntry sessionEntry, bool setCurrent = false) {
|
||||
var sessions = await GetAllSessionsAsync();
|
||||
var newId = Guid.NewGuid();
|
||||
sessions.Add(newId, sessionEntry);
|
||||
await localStorage.SetItemAsJsonAsync(AllSessionsKey, sessions);
|
||||
|
||||
if (setCurrent) {
|
||||
await localStorage.SetItemAsJsonAsync(CurrentSessionKey, newId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetCurrentSessionAsync(Guid sessionId) {
|
||||
await localStorage.SetItemAsJsonAsync(CurrentSessionKey, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public class SessionEntry {
|
||||
[JsonPropertyName("server_name")]
|
||||
public required string ServerName { get; set; }
|
||||
|
||||
[JsonPropertyName("access_token")]
|
||||
public required string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("profile_cache")]
|
||||
public required ProfileCacheData? ProfileCache { get; set; }
|
||||
|
||||
public class ProfileCacheData {
|
||||
[JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public required long Id { get; set; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public required string Username { get; set; }
|
||||
|
||||
[JsonPropertyName("discriminator")]
|
||||
public string? Discriminator { get; set; }
|
||||
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public required string AvatarUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Spacebar.Client
|
||||
@using Spacebar.Client.Layout
|
||||
@@ -0,0 +1,171 @@
|
||||
@import url('jetbrains-mono/jetbrains-mono.css');
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background-color: #222;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
pre, .code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.dbgBanner {
|
||||
transition: 0.25s;
|
||||
display: block;
|
||||
width: 100%;
|
||||
transform: scale(1, 1);
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.dbgBanner.hidden {
|
||||
transition: 0.5s;
|
||||
transform: scale(1, 0);
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#app > div > main > div {
|
||||
background-color: #333;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table, .table-striped > tbody > tr:nth-of-type(odd), .table-hover > tbody > tr:hover {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
inset: 20vh 0 auto 0;
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:nth-child(2) {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress foreignObject {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-progress foreignObject>img {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
position: absolute;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
left: calc(25%);
|
||||
top: calc(25%);
|
||||
}
|
||||
|
||||
.loading-progress foreignObject>img:nth-of-type(1) {
|
||||
opacity: calc(100% - var(--blazor-load-percentage, 0%));
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.loading-progress foreignObject>img:nth-of-type(2) {
|
||||
opacity: var(--blazor-load-percentage, 0%);
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(30vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* source: https://gist.github.com/aasmpro/95776294ecf48bd7d0562504bad848ea */
|
||||
|
||||
/* normal fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url("./ttf/JetBrainsMono-Thin.ttf") format("truetype");
|
||||
src: url("./webfonts/JetBrainsMono-Thin.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-Light.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* italic fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url("./webfonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url("./webfonts/JetBrainsMono-LightItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./webfonts/JetBrainsMono-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url("./webfonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url("./webfonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./webfonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: JetBrainsMono;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url("./webfonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Spacebar.Client</title>
|
||||
<base href="/"/>
|
||||
<link rel="preload" id="webassembly"/>
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link href="Spacebar.Client.styles.css" rel="stylesheet"/>
|
||||
<link href="manifest.webmanifest" rel="manifest"/>
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png"/>
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png"/>
|
||||
<script type="importmap"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
<circle r="40%" cx="50%" cy="50%"/>
|
||||
<foreignObject>
|
||||
<img id="loadingLogo" src="img/icon_white.png" alt=""/>
|
||||
<img id="loadingLogoColored" src="img/icon.png" alt=""/>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
<br/>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
<script>navigator.serviceWorker.register('service-worker.js', {updateViaCache: 'none'});</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Spacebar.Client",
|
||||
"short_name": "Spacebar.Client",
|
||||
"id": "./",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#03173d",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// In development, always fetch from the network and do not enable offline support.
|
||||
// This is because caching would make development more difficult (changes would not
|
||||
// be reflected on the first load after each change).
|
||||
self.addEventListener('fetch', () => { });
|
||||
@@ -0,0 +1,55 @@
|
||||
// Caution! Be sure you understand the caveats before publishing an application with
|
||||
// offline support. See https://aka.ms/blazor-offline-considerations
|
||||
|
||||
self.importScripts('./service-worker-assets.js');
|
||||
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
|
||||
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
|
||||
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
|
||||
|
||||
const cacheNamePrefix = 'offline-cache-';
|
||||
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
|
||||
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/ ];
|
||||
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
|
||||
|
||||
// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'.
|
||||
const base = "/";
|
||||
const baseUrl = new URL(base, self.origin);
|
||||
const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href);
|
||||
|
||||
async function onInstall(event) {
|
||||
console.info('Service worker: Install');
|
||||
|
||||
// Fetch and cache all matching items from the assets manifest
|
||||
const assetsRequests = self.assetsManifest.assets
|
||||
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
|
||||
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
|
||||
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
|
||||
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
|
||||
}
|
||||
|
||||
async function onActivate(event) {
|
||||
console.info('Service worker: Activate');
|
||||
|
||||
// Delete unused caches
|
||||
const cacheKeys = await caches.keys();
|
||||
await Promise.all(cacheKeys
|
||||
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
|
||||
.map(key => caches.delete(key)));
|
||||
}
|
||||
|
||||
async function onFetch(event) {
|
||||
let cachedResponse = null;
|
||||
if (event.request.method === 'GET') {
|
||||
// For all navigation requests, try to serve index.html from cache,
|
||||
// unless that request is for an offline resource.
|
||||
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
|
||||
const shouldServeIndexHtml = event.request.mode === 'navigate'
|
||||
&& !manifestUrlList.some(url => url === event.request.url);
|
||||
|
||||
const request = shouldServeIndexHtml ? 'index.html' : event.request;
|
||||
const cache = await caches.open(cacheName);
|
||||
cachedResponse = await cache.match(request);
|
||||
}
|
||||
|
||||
return cachedResponse || fetch(event.request);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ in
|
||||
};
|
||||
|
||||
offload = {
|
||||
enable = true;
|
||||
enable = false;
|
||||
gateway = {
|
||||
enableIdentify = true;
|
||||
enableGuildMembers = true;
|
||||
@@ -77,12 +77,12 @@ in
|
||||
};
|
||||
|
||||
cdnCs = {
|
||||
enable = false;
|
||||
enable = true;
|
||||
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
|
||||
};
|
||||
|
||||
uApi = {
|
||||
enable = true;
|
||||
enable = false;
|
||||
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user