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

362 lines
12 KiB
Text

@page "/notes/{NoteId}"
@attribute [Authorize]
@inject NoteStore NoteStore;
@inject RelatedStore RelatedStore;
@inject IJSRuntime Js
@inject StateService StateSvc
@inject NavigationManager Navigation
@inject IStringLocalizer<Localization> Loc
@inject ILogger<SingleNote> Logger;
@inject StateSynchronizer StateSynchronizer;
@inherits ChildNoteController
@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.NoteStore
@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 IAsyncDisposable
@if (_componentState == State.Loaded)
{
<HeadTitle Text="@Loc["Note by {0}", RootNote?.User.DisplayName ?? RootNote?.User.Username ?? ""]"/>
<SectionContent SectionName="top-bar">
<Icon Name="Icons.Signpost"></Icon>
@if (RootNote is { User.DisplayName: not null } && RootNote.User.Emojis.Count != 0)
{
<MfmText Text="@Loc["Note by {0}", RootNote.User.DisplayName]" Emoji="@RootNote.User.Emojis"
Simple="@true"/>
}
else
{
@Loc["Note by {0}", (RootNote?.User.DisplayName ?? RootNote?.User.Username) ?? string.Empty]
}
<button class="cw-button button" @onclick="ToggleCw">@if (_cwState)
{
<Icon Name="Icons.EyeClosed"/>
}
else
{
<Icon Name="Icons.Eye"/>
}
</button>
</SectionContent>
@if (RootNote?.User.Host != null)
{
<RemoteUserBanner OriginalPageUrl="@RootNote?.Url"/>
}
<div class="scroller">
<div class="wrapper">
<div class="container">
@if (Ascendants != null)
{
<div class="ascendants">
@foreach (var note in Ascendants)
{
<CascadingValue Value="this" Name="Controller">
<CascadingValue Value="RelatedStore" TValue="NoteMessageProvider" Name="Provider">
<AscendedNote Note="note.Value"/>
</CascadingValue>
</CascadingValue>
}
</div>
}
<div @ref="RootNoteRef" class="root-note">
<CascadingValue Value="this" Name="Controller">
<CascadingValue Value="NoteStore" TValue="NoteMessageProvider" Name="Provider">
<Note NoteResponse="RootNote" OpenNote="false" RootNote="true"></Note>
</CascadingValue>
</CascadingValue>
</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)
{
<CascadingValue Value="this" Name="Controller">
<CascadingValue Value="RelatedStore" TValue="NoteMessageProvider"
Name="Provider">
<RecursiveNote Note="element.Value" Depth="0" MaxDepth="_depth"/>
</CascadingValue>
</CascadingValue>
}
</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)
{
<HeadTitle Text="@Loc["Loading"]"/>
<div class="loading">
<LoadingSpinner Scale="2"/>
</div>
}
@if (_componentState == State.NotFound)
{
<HeadTitle Text="@Loc["Note not found"]"/>
<div>This note does not exist!</div>
}
@if (_componentState == State.Empty)
{
<HeadTitle Text="@Loc["Deleted"]"/>
<div>@Loc["This post has been deleted"]</div>
}
@if (_componentState == State.Error)
{
<HeadTitle Text="@Loc["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 SortedList<string, NoteResponse>? Descendants { get; set; }
private SortedList<string, NoteResponse>? Ascendants { get; set; }
private IJSInProcessObjectReference? Module { get; set; }
private readonly Lazy<Task<IJSObjectReference>> _moduleTask;
private ElementReference RootNoteRef { get; set; }
private int _depth = 20;
private IDisposable? _locationChangingHandlerDisposable;
private State _componentState;
private bool _firstLoad = true;
private bool _cwState;
private async Task Load()
{
Logger.LogTrace($"Opening NoteID: {NoteId}");
Module = (IJSInProcessObjectReference?)await _moduleTask.Value;
Logger.LogInformation("Loaded Module");
_componentState = State.Loading;
if (NoteId == null)
{
_componentState = State.NotFound;
return;
}
try
{
var rootNoteTask = NoteStore.GetNoteAsync(NoteId);
var descendantsTask = RelatedStore.GetDescendantsAsync(NoteId, _depth);
var ascendantsTask = RelatedStore.GetAscendantsAsync(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()
{
_locationChangingHandlerDisposable = Navigation.RegisterLocationChangingHandler(LocationChangeHandler);
StateSynchronizer.NoteDeleted += OnNoteDeleted;
NoteStore.AnyNoteChanged += OnNoteChanged;
}
private void OnNoteDeleted(object? _, NoteBase note)
{
if (NoteId == note.Id)
{
_componentState = State.Empty;
}
else
{
Ascendants?.Remove(note.Id);
Descendants?.Remove(note.Id);
}
StateHasChanged();
}
private void OnNoteChanged(object? _, NoteResponse note)
{
if (note.Id == NoteId)
{
var __ = Refresh();
}
}
private async Task Refresh()
{
if (NoteId != null) Descendants = await RelatedStore.GetDescendantsAsync(NoteId, _depth, true);
StateHasChanged();
}
// ReSharper disable once InconsistentNaming
private async ValueTask LocationChangeHandler(LocationChangingContext arg)
{
await SaveState();
}
public SingleNote()
{
_moduleTask = new Lazy<Task<IJSObjectReference>>(() =>
Js.InvokeAsync<IJSObjectReference>(
"import",
"./Pages/SingleNote.razor.js")
.AsTask());
}
protected override void OnAfterRender(bool firstRender)
{
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);
}
}
if (_cwState)
{
foreach (var note in NoteChildren)
{
note.OpenCw();
}
}
else
{
foreach (var note in NoteChildren)
{
note.CloseCw();
}
}
}
private void ToggleCw()
{
if (_cwState)
{
foreach (var note in NoteChildren)
{
note.CloseCw();
}
_cwState = false;
}
else
{
foreach (var note in NoteChildren)
{
note.OpenCw();
}
_cwState = true;
}
}
private async Task SaveState()
{
if (NoteId == null || _componentState != State.Loaded) return;
var module = await _moduleTask.Value;
var scrollTop = await module.InvokeAsync<float>("GetScrollY");
var state = new SingleNoteState { ScrollTop = scrollTop };
StateSvc.SingleNote.SetState(NoteId, state);
}
public async ValueTask DisposeAsync()
{
await SaveState();
_locationChangingHandlerDisposable?.Dispose();
StateSynchronizer.NoteDeleted -= OnNoteDeleted;
NoteStore.AnyNoteChanged -= OnNoteChanged;
}
}