Simplify and standardise a lot of the loading

I was overcomplicating everything trying to reduce the render lag.
Just simplify it a bit. It works.
This commit is contained in:
Gordon Pedersen 2024-06-13 14:46:24 +10:00
parent 08788db7fa
commit 0c3836b0c8
16 changed files with 205 additions and 261 deletions

View file

@ -1,11 +1,9 @@
@inject IJSRuntime JS
@inject State State
@if(Html != null) {
<iframe id="@id" frameborder="0" scrolling="no" srcdoc="@Html" onload="() => iframeResize({ license: 'GPLv3' })"></iframe>
}
@code {
[Parameter]
public string Url { get; set; }
@ -13,6 +11,11 @@
public string id { get; set; }
public MarkupString? Html { get; set; }
protected override void OnInitialized() {
base.OnInitialized();
State.CurrentPage = Page.Other;
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if(firstRender){
await Reload();

View file

@ -0,0 +1,16 @@
<article id="@id" class="@(string.IsNullOrEmpty(@class)?"":@class) middle-align center-align">
<div>
<i class="extra square @icon"></i>
<div class="space"></div>
<progress class="circle"></progress>
</div>
</article>
@code {
[Parameter]
public string id { get; set; }
[Parameter]
public string? @class { get; set; }
[Parameter]
public string icon { get; set; }
}

View file

@ -0,0 +1,18 @@
<div class="row center-align">
<h3>
<i class="@icon extra"></i> @title
</h3>
</div>
<div class="row center-align">
<p>@Description</p>
</div>
<div class="space"></div>
@code {
[Parameter]
public string icon { get; set; }
[Parameter]
public string title { get; set; }
[Parameter]
public RenderFragment Description { get; set; }
}

View file

@ -1,27 +1,30 @@
@page "/ephemeral"
@inject IJSRuntime JS
@inject State State
<PageHeading title="Eph.emer.al" icon="fa-light fa-comment-dots">
<Description><a href="https://eph.emer.al">Eph.emer.al</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</Description>
</PageHeading>
<div class="row center-align">
<h3>
<i class="fa-light fa-comment-dots"></i> Ephemeral
</h3>
</div>
<div class="row center-align">
<p><a href="https://eph.emer.al">Ephemeral</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</p>
</div>
<div id="ephemeral" class="responsive">
<Virtualize Items="messages" Context="message">
@if (messages != null) {
foreach (MarkupString message in messages) {
<article class="ephemeral center">
@message
</article>
</Virtualize>
}
}
<LoadingCard id="ephemeral-loading" icon="fa-light fa-comment-dots"></LoadingCard>
</div>
@code {
private List<MarkupString> messages = new List<MarkupString>();
private List<MarkupString>? messages;
protected override async Task OnInitializedAsync() {
//await Shell.Current.GoToAsync(nameof(EphemeralWebPage));
RestService api = new RestService();
messages = await api.Ephemeral();
await base.OnInitializedAsync();
if (messages == null || messages.Count == 0) messages = await State.GetEphemeralMessages();
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "ephemeral-loading");
}
}

View file

@ -1,17 +1,14 @@
@page "/now"
@inject IJSRuntime JS
@inject State State
<div class="row center-align">
<h3>
<i class="fa-duotone fa-seedling"></i> Now.garden
</h3>
</div>
<div class="row center-align">
<p>Feel free to stroll through the <a href="now.garden">now.garden</a> and take a look at what people are up to.</p>
</div>
<PageHeading title="Now.garden" icon="fa-duotone fa-seedling">
<Description>Feel free to stroll through the <a href="now.garden">now.garden</a> and take a look at what people are up to.</Description>
</PageHeading>
<div id="now-garden" class="responsive card-grid">
<Virtualize ItemsProvider="State.VirtualNowGarden" Context="now" ItemSize="180">
<ItemContent>
@if (garden != null) {
foreach (NowData now in garden) {
<article class="now">
<nav>
<a class="author" href="/person/@now.Address#now">
@ -22,13 +19,19 @@
<small>@now.UpdatedRelative</small>
</nav>
</article>
</ItemContent>
<Placeholder>
<StatusCardSkeleton></StatusCardSkeleton>
</Placeholder>
</Virtualize>
}
}
<LoadingCard id="now-loading" icon="fa-duotone fa-seedling" class="now"></LoadingCard>
</div>
@code {
private List<NowData>? garden;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (garden == null || garden.Count == 0) garden = await State.GetNowGarden();
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "now-loading");
}
}

View file

@ -1,6 +1,7 @@
@page "/person/{Address}"
@inject State State
@inject IJSRuntime JS
@inject NavigationManager Nav
<div class="row center-align">
@ -10,16 +11,6 @@
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div>
<div id="bio" class="center-align max">
@if (bio == null)
{
<p><em>Getting Bio...</em></p>
}
else {
@bio
}
</div>
<div class="responsive">
<div class="tabs">
<a data-ui="#profile" @onclick="ReloadProfile">
@ -42,6 +33,7 @@
}
</div>
</div>
<div class="responsive page-container">
<div id="profile" class="page no-padding">
<a href="@ProfileUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
@ -49,14 +41,21 @@
</div>
<div id="statuses" class="page padding active">
<StatusList StatusFunc="@State.VirtualStatusesFunc(Address)"></StatusList>
<div id="info" class="box basis transparent">
<article id="bio" class="container shadowed blue-2-bg gray-9-fg">
@if (bio == null) {
<p><progress class="circle small"></progress></p>
}
else {
@bio
}
</article>
</div>
<StatusList StatusFunc="@(async() => await State.GetStatuses(Address))"></StatusList>
</div>
<div id="pics" class="page padding">
@if(Editable){
<EditPicDialog @ref="editPicDialog" id="EditPicModal"></EditPicDialog>
}
<PicList PicsFunc="@(async() => await State.GetPics(Address))" Editable="@Editable" Dialog="@editPicDialog"></PicList>
<PicList PicsFunc="@(async() => await State.GetPics(Address))" Editable="@Editable"></PicList>
</div>
@if(now != null){
<div id="now" class="page no-padding">
@ -73,7 +72,6 @@
get => $"https://{Address}.omg.lol/";
}
private EditPicDialog? editPicDialog { get; set; }
public ExternalPageComponent? NowPage { get; set; }
public ExternalPageComponent? ProfilePage { get; set; }
@ -86,9 +84,14 @@
private NowData? now;
protected override async Task OnInitializedAsync() {
List<NowData>? garden = await State.GetNowGarden();
now = garden?.FirstOrDefault(n => n.Address == Address);
await InvokeAsync(StateHasChanged);
string fragment = new Uri(Nav.Uri).Fragment;
await JS.InvokeVoidAsync("ui", fragment);
if (fragment.EndsWith("now")) await ReloadNow();
else if (fragment.EndsWith("profile")) await ReloadProfile();
bio = await State.GetBio(Address);
List<NowData> garden = await State.GetNowGarden();
now = garden.FirstOrDefault(n => n.Address == Address);
}
private async Task ReloadNow() {

View file

@ -1,16 +1,9 @@
@page "/pics"
@inject State State
@inject IJSRuntime JS
<div class="row center-align">
<h3>
<i class="fa-solid fa-images"></i> Some.pics
</h3>
</div>
<div class="row center-align">
<p>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></p>
</div>
<PageHeading title="Some.pics" icon="fa-solid fa-images">
<Description>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></Description>
</PageHeading>
<AuthorizeView>
<Authorized>

View file

@ -1,14 +1,9 @@
@page "/statuslog/latest"
@inject State State
<div class="row center-align">
<h3>
<i class="fa-solid fa-message-smile"></i> Statuslog
</h3>
</div>
<div class="row center-align">
<p>The latest posts from everyone at <a href="https://status.lol">status.lol</a></p>
</div>
<PageHeading title="Status.lol" icon="fa-solid fa-message-smile">
<Description>The latest posts from everyone at <a href="https://status.lol">status.lol</a></Description>
</PageHeading>
<AuthorizeView>
<Authorized>
@ -20,10 +15,9 @@
</AuthorizeView>
<div id="statuses" class="responsive">
<StatusList StatusFunc="@State.VirtualStatusesFunc()"></StatusList>
<StatusList StatusFunc="@(async() => await State.GetStatuses())"></StatusList>
</div>
@code {
}

View file

@ -12,7 +12,7 @@
<i class="fa fa-clock"></i> @Pic.RelativeTime
</a>
</nav>
<p>@Pic.Description</p>
<p>@((MarkupString)Pic.DescriptionHtml)</p>
<nav>
<div class="max"></div>
@if(Editable) {

View file

@ -2,112 +2,30 @@
@inject State State
@if (Editable) {
<PicCardEditableTemplate></PicCardEditableTemplate>
}
else {
<PicCardTemplate></PicCardTemplate>
<EditPicDialog @ref="Dialog" id="EditPicModal"></EditPicDialog>
}
<article id="pics-loading" class="middle-align center-align">
<div>
<i class="extra fa-solid fa-images"></i>
<div class="space"></div>
<progress class="circle"></progress>
</div>
</article>
<script>
async function renderPics(somePics) {
const template = document.getElementById("PicCard")
if(template) for (const pic of somePics) {
const clone = template.content.cloneNode(true)
let html = clone.children[0].innerHTML
html = html.replaceAll("{{Pic.Id}}", pic.id)
html = html.replaceAll("{{Pic.Url}}", pic.url ?? `https://cdn.some.pics/${pic.address}/${pic.id}.${JSON.parse(pic.exif)["File Type Extension"]}`) //TODO: temporary fix
html = html.replaceAll("{{Pic.Address}}", pic.address)
html = html.replaceAll("{{Pic.RelativeTime}}", pic.relativeTime)
html = html.replaceAll("{{Pic.Description}}", pic.description)
html = html.replaceAll("{{Pic.DescriptionHtml}}", pic.descriptionHtml)
clone.children[0].innerHTML = html
clone.querySelector('.share-button').addEventListener('click', shareClick)
clone.querySelector('.edit-button')?.addEventListener('click', editClick)
document.getElementById("pics").insertBefore(clone, document.getElementById("pics-loading"))
}
}
async function clearLoading() {
document.getElementById("pics-loading").remove()
@if (pics != null) foreach (Pic pic in pics) {
<PicCard Pic="pic" Editable="Editable" Dialog="Dialog"></PicCard>
}
async function shareClick(e) {
const url = this.querySelector('input[name="url"]')?.value
const desc = this.querySelector('input[name="description"]')?.value
if (CSHARP) CSHARP.invokeMethodAsync('ShareClick', url, desc)
}
async function editClick(e) {
const id = this.querySelector('input[name="id"]')?.value
if (CSHARP) CSHARP.invokeMethodAsync('EditClick', id)
}
</script>
<LoadingCard id="pics-loading" icon="fa-solid fa-images"></LoadingCard>
@code {
[Parameter]
public Func<Task<List<Pic>?>> PicsFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPicDialog? Dialog { get; set; }
private List<Pic> pics;
private DotNetObjectReference<PicList>? objRef;
private List<Pic>? pics;
protected override void OnInitialized() {
base.OnInitialized();
objRef = DotNetObjectReference.Create(this);
State.CurrentPage = Page.Pics;
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
await base.OnAfterRenderAsync(firstRender);
if (firstRender) {
await JS.InvokeVoidAsync("injectCSharp", objRef);
if (State.CurrentPage != Page.Pics) return;
// TODO: There is a noticable rendering delay between the pics loading and the page rendering
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (pics == null || pics.Count == 0) pics = await PicsFunc();
if (State.CurrentPage != Page.Pics) return;
int page_size = 1;
for (int i = 0; i < pics.Count; i += page_size) {
if (State.CurrentPage != Page.Pics) return;
await JS.InvokeVoidAsync("renderPics", pics.Skip(i * page_size).Take(page_size));
if (State.CurrentPage != Page.Pics) return;
}
if (State.CurrentPage != Page.Pics) return;
await JS.InvokeVoidAsync("clearLoading");
}
}
[JSInvokable]
public async Task EditClick(string? id) {
if (Editable && Dialog != null)
{
Pic? pic = pics.FirstOrDefault(p => p.Id == id);
Dialog.Pic = pic;
// await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
}
[JSInvokable]
public async Task ShareClick(string? url, string? description) {
await Share.Default.RequestAsync(new ShareTextRequest {
Uri = url,
Text = description,
Title = "I saw this on some.pics",
Subject = "I saw this on some.pics"
});
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "pics-loading");
}
}

View file

@ -1,14 +1,12 @@
<article class="status">
<div class="row">
<div class="large emoji" data-emoji="@status.EmojiOrDefault">@status.EmojiOrDefault</div>
<div class="emoji" data-emoji="@status.EmojiOrDefault">@status.EmojiOrDefault</div>
<div class="max">
<a class="author" href="/person/@status.Address">
<i class="fa-solid fa-fw fa-at"></i>@status.Address
</a>
@status.HtmlContent
</div>
</div>
<nav>
<nav class="no-margin">
<a class="chip transparent-border">
<i class="fa fa-clock"></i> @status.RelativeTime
</a>
@ -20,6 +18,8 @@
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
</div>
</div>
</article>
@code {

View file

@ -1,23 +1,24 @@
@inject State State
<Virtualize ItemsProvider="GetStatuses" Context="status" ItemSize="180">
<ItemContent>
@inject IJSRuntime JS
@inject State State
@if(statuses != null) foreach(Status status in statuses) {
<StatusCard status="@status"></StatusCard>
</ItemContent>
<Placeholder>
<StatusCardSkeleton></StatusCardSkeleton>
</Placeholder>
</Virtualize>
}
<LoadingCard id="statusLoading" icon="fa-solid fa-message-smile"></LoadingCard>
@code {
[Parameter]
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Status>>> StatusFunc { get; set; }
public Func<Task<List<Status>?>> StatusFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
private async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request) {
return await this.StatusFunc(request);
}
private List<Status>? statuses;
protected override void OnInitialized() {
base.OnInitialized();
State.CurrentPage = Page.Status;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (statuses == null || statuses.Count == 0) statuses = await StatusFunc();
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "statusLoading");
}
}

View file

@ -17,7 +17,7 @@ namespace Neighbourhood.omg.lol.Models {
public long Size { get; set; }
public string Mime { get; set; }
public string Description { get; set; }
public string DescriptionHtml { get => Markdown.ToHtml(Description); }
public string DescriptionHtml { get => Description == null ? string.Empty : Markdown.ToHtml(Description); }
[JsonPropertyName("exif")]
public JsonElement ExifJson { get; set; }

View file

@ -25,6 +25,7 @@ namespace Neighbourhood.omg.lol.Models {
public List<Status>? Statuses { get; set; }
public List<Pic>? Pics { get; set; }
public List<NowData>? NowGarden { get; set; }
public List<MarkupString>? EphemeralMessages { get; set; }
public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; }
@ -88,6 +89,14 @@ namespace Neighbourhood.omg.lol.Models {
return CachedAddressBio;
}
public async Task<List<MarkupString>?> GetEphemeralMessages(bool forceRefresh = false) {
RestService api = new RestService();
if(forceRefresh || this.EphemeralMessages == null || this.EphemeralMessages.Count == 0) {
this.EphemeralMessages = await api.Ephemeral();
}
return this.EphemeralMessages;
}
public async Task<List<Status>?> GetStatuses(bool forceRefresh = false) {
RestService api = new RestService();
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
@ -96,27 +105,13 @@ namespace Neighbourhood.omg.lol.Models {
return this.Statuses;
}
public async ValueTask<ItemsProviderResult<Status>> VirtualStatuses(ItemsProviderRequest request) {
// TODO: request.cancellationToken
var statuses = (await this.GetStatuses()) ?? new List<Status>();
var numStatuses = Math.Min(request.Count, statuses.Count - request.StartIndex);
return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Count);
}
public async ValueTask<ItemsProviderResult<Status>> VirtualStatuses(ItemsProviderRequest request, string address) {
// TODO: request.cancellationToken
public async Task<List<Status>?> GetStatuses(string address, bool forceRefresh = false) {
this.CachedAddress = address;
RestService api = new RestService();
CachedAddressStatuses = (await api.Statuslog(address)) ?? new List<Status>();
var numStatuses = Math.Min(request.Count, CachedAddressStatuses.Count - request.StartIndex);
return new ItemsProviderResult<Status>(CachedAddressStatuses.Skip(request.StartIndex).Take(numStatuses), CachedAddressStatuses.Count);
}
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Status>>> VirtualStatusesFunc(string? address = null) {
if (address == null) return VirtualStatuses;
else {
CachedAddress = address;
return async (ItemsProviderRequest request) => await VirtualStatuses(request, CachedAddress);
if (forceRefresh || this.CachedAddressStatuses == null || this.CachedAddressStatuses.Count == 0) {
this.CachedAddressStatuses = await api.Statuslog(address);
}
return this.CachedAddressStatuses;
}
public async Task<List<NowData>?> GetNowGarden(bool forceRefresh = false) {
@ -126,16 +121,6 @@ namespace Neighbourhood.omg.lol.Models {
}
return this.NowGarden;
}
public async ValueTask<ItemsProviderResult<NowData>> VirtualNowGarden(ItemsProviderRequest request) {
// TODO: request.cancellationToken
var garden = (await this.GetNowGarden()) ?? new List<NowData>();
var numSeedlings = Math.Min(request.Count, garden.Count - request.StartIndex);
return new ItemsProviderResult<NowData>(garden.Skip(request.StartIndex).Take(numSeedlings), garden.Count);
}
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<NowData>>> VirtualNowGardenFunc() {
return VirtualNowGarden;
}
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
if(forceRefresh || this.Pics == null || this.Pics.Count == 0) {
@ -147,36 +132,13 @@ namespace Neighbourhood.omg.lol.Models {
public async Task<List<Pic>?> GetPics(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || this.Pics == null || this.Pics.Count == 0) {
if (forceRefresh || this.CachedAddressPics == null || this.CachedAddressPics.Count == 0) {
RestService api = new RestService();
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
}
return CachedAddressPics;
}
public async ValueTask<ItemsProviderResult<Pic>> VirtualPics(ItemsProviderRequest request) {
// TODO: request.cancellationToken
var pics = (await this.GetPics()) ?? new List<Pic>();
var numPics = Math.Min(request.Count, pics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(pics.Skip(request.StartIndex).Take(numPics), pics.Count);
}
public async ValueTask<ItemsProviderResult<Pic>> VirtualPics(ItemsProviderRequest request, string address) {
// TODO: request.cancellationToken
RestService api = new RestService();
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
var numPics = Math.Min(request.Count, CachedAddressPics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(CachedAddressPics.Skip(request.StartIndex).Take(numPics), CachedAddressPics.Count);
}
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Pic>>> VirtualPicsFunc(string? address = null) {
if (address == null) return VirtualPics;
else {
CachedAddress = address;
return async (ItemsProviderRequest request) => await VirtualPics(request, CachedAddress);
}
}
public async Task<long> FileSize(FileResult file) {
using var fileStream = await file.OpenReadAsync();
return fileStream.Length;
@ -213,7 +175,7 @@ namespace Neighbourhood.omg.lol.Models {
Status,
Pics,
Ephemeral,
Person,
NowGarden,
Other
}
}

View file

@ -10,6 +10,13 @@
--emoji-font: SegoeUIEmoji, 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Symbol';
--font: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif, var(--emoji-font);
--prami-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="%23FF6BAE" stroke="none" d="M250 450C211.612 450 173.225 435.354 143.934 406.066L43.9346 306.066C-14.6446 247.487 -14.6446 152.513 43.9346 93.9341C100.533 37.3361 191.104 35.421 250 88.1907C308.898 35.4229 399.47 37.3379 456.066 93.9341C514.645 152.513 514.645 247.487 456.066 306.066L356.066 406.066C326.778 435.354 288.389 450 250 450" /><path fill="none" stroke="%23471036" stroke-width="19" stroke-linecap="round" d="M208.749 223.817C227.625 254.619 272.376 254.619 291.251 223.817" /><circle fill="%23471036" cx="291.3" cy="176.6" r="17.75" /><circle fill="%23471036" cx="208.6" cy="176.6" r="17.75" /><circle fill="%23E24097" cx="120.3" cy="212" r="59.2" /><circle fill="%23E24097" cx="379.7" cy="212" r="59.2" /></svg>');
--spacing: 1.5rem;
--radius: .75em;
--small-radius: .13em;
/* --color: var(--white);
--background: var(--gray-8);
--shadow: var(--black);
--button-shadow: var(--gray-7);*/
}
html, body {
font-family: var(--font);
@ -53,13 +60,17 @@ img {
.status .emoji, #status-emoji {
margin-bottom: auto;
inline-size: 3.5rem;
block-size: 3.5rem;
font-size: 3.1rem;
line-height: 3.5rem;
inline-size: 5.5rem;
block-size: 5.5rem;
font-size: 5rem;
line-height: 5.5rem;
text-indent: -6px;
}
.status nav .chip, .status nav {
color: var(--gray-7);
}
.profile.avatar {
border-radius: .75rem;
max-block-size: 10rem;
@ -269,3 +280,11 @@ main {
.hover {
z-index: 1;
}
#info :is(ul, ol) {
margin-left: var(--spacing);
}
#info :is(p, ul, ol) {
margin-bottom: var(--spacing);
}

View file

@ -1,3 +1,14 @@
window.injectCSharp = async function (helper) {
window.CSHARP = helper
}
async function delay(t) {
return new Promise((resolve) => {
setTimeout(resolve, t);
});
}
async function removeElementById(id) {
document.getElementById(id)?.remove()
}