Iceshrimp.NET/Iceshrimp.Frontend/Pages/SingleNote.razor

270 lines
No EOL
9.3 KiB
Text

@page "/notes/{NoteId}"
@attribute [Authorize]
@inject ApiService ApiService
@inject IJSRuntime Js
@inject MessageService MessageService
@inject StateService StateSvc
@inject NavigationManager Navigation
@inject IStringLocalizer<Localization> Loc
@inject ILogger<SingleNote> Logger;
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization
@implements IDisposable
@if (_componentState == State.Loaded)
{
<SectionContent SectionName="top-bar">
<Icon Name="Icons.Signpost"></Icon>
@Loc["Note by {0}", (RootNote?.User.DisplayName ?? RootNote?.User.Username) ?? string.Empty]
</SectionContent>
<div @ref="Scroller" class="scroller">
<div class="wrapper">
<div class="container">
@if (Ascendants != null)
{
<div class="ascendants">
@foreach (var note in Ascendants)
{
<AscendedNote Note="note"/>
}
</div>
}
<div @ref="RootNoteRef" class="root-note">
<Note NoteResponse="RootNote"></Note>
</div>
<div class="details">
<TabbedMenu>
<TabPage>
<Title>
@Loc["Replies ({0})", RootNote!.Replies]
</Title>
<TabContent>
@if (Descendants != null)
{
<div class="descendants">
@foreach (var element in Descendants)
{
<RecursiveNote Note="element" Depth="0" MaxDepth="_depth"/>
}
</div>
}
</TabContent>
</TabPage>
@if (RootNote?.Likes > 0)
{
<TabPage>
<Title>@Loc["Likes ({0})", RootNote!.Likes]</Title>
<TabContent>
<NoteLikeDetails NoteId="@RootNote?.Id"/>
</TabContent>
</TabPage>
}
@if (RootNote?.Renotes > 0)
{
<TabPage>
<Title>@Loc["Renotes ({0})", RootNote!.Renotes]</Title>
<TabContent>
<NoteRenoteDetails NoteId="@RootNote?.Id"/>
</TabContent>
</TabPage>
}
@if (RootNote?.Reactions.Count > 0)
{
<TabPage>
<Title>@Loc["Reactions ({0})", RootNote!.Reactions.Count]</Title>
<TabContent>
<NoteReactionDetails Reactions="RootNote?.Reactions" NoteId="@RootNote?.Id"/>
</TabContent>
</TabPage>
}
<TabPage>
<Title>@Loc["Quotes"]</Title>
<TabContent>
<NoteQuoteDetails NoteId="@RootNote?.Id"/>
</TabContent>
</TabPage>
</TabbedMenu>
</div>
</div>
</div>
</div>
}
@if (_componentState == State.Loading)
{
<div>Loading</div>
}
@if (_componentState == State.NotFound)
{
<div>This note does not exist!</div>
}
@if (_componentState == State.Empty)
{
<div>@Loc["This post has been deleted"]</div>
}
@if (_componentState == State.Error)
{
<div>An error occured loading the notes. Please inspect logs.</div>
}
@code {
[Parameter] public string? NoteId { get; set; }
public NoteResponse? RootNote { get; set; }
private IList<NoteResponse>? Descendants { get; set; }
private IList<NoteResponse>? Ascendants { get; set; }
private IJSInProcessObjectReference? Module { get; set; }
private ElementReference RootNoteRef { get; set; }
private int _depth = 20;
private ElementReference Scroller { get; set; }
private IDisposable? _locationChangingHandlerDisposable;
private IDisposable? _noteChangedHandler;
private State _componentState;
private bool _firstLoad = true;
private async Task Load()
{
Logger.LogTrace($"Opening NoteID: {NoteId}");
_componentState = State.Loading;
if (NoteId == null)
{
_componentState = State.NotFound;
return;
}
try
{
var rootNoteTask = ApiService.Notes.GetNote(NoteId);
var descendantsTask = ApiService.Notes.GetNoteDescendants(NoteId, _depth);
var ascendantsTask = ApiService.Notes.GetNoteAscendants(NoteId, default);
RootNote = await rootNoteTask;
Descendants = await descendantsTask;
Ascendants = await ascendantsTask;
}
catch (ApiException e)
{
Logger.LogWarning($"Failed to load ID '{NoteId}' due to API Exception: {e.Message}");
_componentState = State.Error;
return;
}
if (RootNote == null)
{
_componentState = State.NotFound;
return;
}
_componentState = State.Loaded;
}
protected override async Task OnInitializedAsync()
{
await Load();
}
protected override async Task OnParametersSetAsync()
{
if (_firstLoad)
{
_firstLoad = false;
return;
}
await Load();
StateHasChanged();
}
protected override void OnInitialized()
{
if (NoteId != null) _noteChangedHandler = MessageService.Register(NoteId, OnNoteChanged, MessageService.Type.Updated);
_locationChangingHandlerDisposable = Navigation.RegisterLocationChangingHandler(LocationChangeHandler);
MessageService.AnyNoteDeleted += OnNoteDeleted;
}
private void OnNoteChanged(object? _, NoteResponse note)
{
var __ = Refresh();
}
private void OnNoteDeleted(object? _, NoteResponse note)
{
if (NoteId == note.Id)
{
_componentState = State.Empty;
}
else
{
Ascendants?.Remove(note);
Descendants?.Remove(note);
}
StateHasChanged();
}
private async Task Refresh()
{
if (NoteId == null) throw new InvalidOperationException("RefreshNote called under impossible circumstances");
Descendants = await ApiService.Notes.GetNoteDescendants(NoteId, default);
Ascendants = await ApiService.Notes.GetNoteAscendants(NoteId, default);
StateHasChanged();
}
private ValueTask LocationChangeHandler(LocationChangingContext arg)
{
SaveState();
return ValueTask.CompletedTask;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "/Pages/SingleNote.razor.js");
}
if (_componentState == State.Loaded)
{
var state = StateSvc.SingleNote.GetState(NoteId!);
if (Module is null)
{
Logger.LogError("JS Interop used before initialization");
return;
}
if (state != null)
{
Module.InvokeVoid("SetScrollY", state.ScrollTop);
}
else
{
Module.InvokeVoid("ScrollIntoView", RootNoteRef);
}
}
}
private void SaveState()
{
if (NoteId == null || _componentState != State.Loaded) return;
var scrollTop = (Module ?? throw new Exception("JS Interop used before init"))
.Invoke<float>("GetScrollY");
var state = new SingleNoteState { ScrollTop = scrollTop };
StateSvc.SingleNote.SetState(NoteId, state);
}
public void Dispose()
{
_noteChangedHandler?.Dispose();
SaveState();
_locationChangingHandlerDisposable?.Dispose();
MessageService.AnyNoteDeleted -= OnNoteDeleted;
}
}