362 lines
12 KiB
Text
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;
|
|
}
|
|
}
|