[frontend] Code cleanup

This commit is contained in:
Laura Hausmann 2024-06-29 00:46:47 +02:00
parent 791252ccd4
commit 03fc824d34
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
16 changed files with 341 additions and 328 deletions

View file

@ -1,4 +1,5 @@
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<div class="note"> <div class="note">

View file

@ -1,4 +1,3 @@
@using AngleSharp.Dom
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@ -9,15 +8,17 @@
@inject IJSRuntime Js @inject IJSRuntime Js
@inject ApiService ApiService @inject ApiService ApiService
@inject ComposeService ComposeService @inject ComposeService ComposeService
@inject SessionService SessionService
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject SessionService SessionService
<dialog class="compose" @ref="Dialog"> <dialog class="compose" @ref="Dialog">
<div class="header"> <div class="header">
<button @onclick="CloseDialog"> <button @onclick="CloseDialog">
<Icon Name="Icons.X"/> <Icon Name="Icons.X"/>
</button> </button>
<Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/> <Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/>
<button @onclick="SendNote" class="post-btn">@Loc["ComposePost"]<Icon Name="Icons.PaperPlaneRight"/></button> <button @onclick="SendNote" class="post-btn">
@Loc["ComposePost"]<Icon Name="Icons.PaperPlaneRight"/>
</button>
</div> </div>
@if (ReplyOrQuote != null) @if (ReplyOrQuote != null)
{ {
@ -57,18 +58,19 @@
private string? TextPlaceholder { get; set; } private string? TextPlaceholder { get; set; }
private EmojiPicker EmojiPicker { get; set; } = null!; private EmojiPicker EmojiPicker { get; set; } = null!;
private ElementReference Textarea { get; set; } private ElementReference Textarea { get; set; }
private NoteCreateRequest NoteDraft { get; set; } = new NoteCreateRequest
private NoteCreateRequest NoteDraft { get; set; } = new NoteCreateRequest
{ {
Text = "", Text = "",
Visibility = NoteVisibility.Followers, // FIXME: Default to visibilty in settings Visibility = NoteVisibility.Followers, // FIXME: Default to visibilty in settings
Cw = null Cw = null
}; };
private Dictionary<string, string> AvailablePlaceholders { get; set; } = new() private Dictionary<string, string> AvailablePlaceholders { get; set; } = new()
{ {
{ "default", "What's on your mind?" }, { "default", "What's on your mind?" },
{ "reply", "Reply goes here!" }, { "reply", "Reply goes here!" },
{ "quote", "Quote this post!" } { "quote", "Quote this post!" }
}; };
RenderFragment DropdownIcon(NoteVisibility vis) RenderFragment DropdownIcon(NoteVisibility vis)
@ -99,7 +101,12 @@
{ {
return Enum.GetValues<NoteVisibility>() return Enum.GetValues<NoteVisibility>()
.Select(vis => .Select(vis =>
new DropdownElement<NoteVisibility> { Icon = DropdownIcon(vis), Content = DropdownContent(vis), Selection = vis }) new DropdownElement<NoteVisibility>
{
Icon = DropdownIcon(vis),
Content = DropdownContent(vis),
Selection = vis
})
.ToList(); .ToList();
} }
@ -124,6 +131,7 @@
{ {
NoteDraft.Text += $"@{el} "; NoteDraft.Text += $"@{el} ";
} }
StateHasChanged(); StateHasChanged();
} }
else if (quote != null) else if (quote != null)
@ -149,14 +157,15 @@
List<string> mentions = []; List<string> mentions = [];
if (noteBase.User.Id != SessionService.Current!.Id) if (noteBase.User.Id != SessionService.Current!.Id)
{ {
var userMention = noteBase.User.Username; var userMention = noteBase.User.Username;
if (noteBase.User.Host != null) if (noteBase.User.Host != null)
{ {
userMention += $"@{noteBase.User.Host}"; userMention += $"@{noteBase.User.Host}";
} }
mentions.Add(userMention); mentions.Add(userMention);
} }
var current = $"@{SessionService.Current.Username}@{SessionService.Current.Host}"; var current = $"@{SessionService.Current.Username}@{SessionService.Current.Host}";
var mfmNodes = Mfm.parse(noteBase.Text); var mfmNodes = Mfm.parse(noteBase.Text);
foreach (var node in mfmNodes) foreach (var node in mfmNodes)
@ -166,6 +175,7 @@
mentions.Add(mentionNode.Acct); mentions.Add(mentionNode.Acct);
} }
} }
mentions = mentions.Distinct().ToList(); mentions = mentions.Distinct().ToList();
mentions.Remove(current); mentions.Remove(current);
return mentions; return mentions;
@ -173,9 +183,14 @@
private void ResetState() private void ResetState()
{ {
ReplyOrQuote = null; ReplyOrQuote = null;
Attachments = new List<DriveFileResponse>(); Attachments = new List<DriveFileResponse>();
NoteDraft = new NoteCreateRequest { Text = "", Visibility = NoteVisibility.Followers, Cw = null }; NoteDraft = new NoteCreateRequest
{
Text = "",
Visibility = NoteVisibility.Followers,
Cw = null
};
TextPlaceholder = AvailablePlaceholders["default"]; TextPlaceholder = AvailablePlaceholders["default"];
} }
@ -220,15 +235,13 @@
private void ToggleEmojiPicker() private void ToggleEmojiPicker()
{ {
EmojiPicker.Toggle(); EmojiPicker.Toggle();
Console.WriteLine("showing picker");
StateHasChanged(); StateHasChanged();
} }
private async Task AddEmoji(EmojiResponse emoji) private async Task AddEmoji(EmojiResponse emoji)
{ {
var pos = await _module.InvokeAsync<int>("getSelectionStart"); var pos = await _module.InvokeAsync<int>("getSelectionStart");
var text = NoteDraft.Text; var text = NoteDraft.Text;
var emojiString = $":{emoji.Name}"; var emojiString = $":{emoji.Name}";
NoteDraft.Text = text.Insert(pos, emojiString); NoteDraft.Text = text.Insert(pos, emojiString);
StateHasChanged(); StateHasChanged();

View file

@ -3,38 +3,41 @@
@using Ljbc1994.Blazor.IntersectionObserver.API @using Ljbc1994.Blazor.IntersectionObserver.API
@using Ljbc1994.Blazor.IntersectionObserver.Components @using Ljbc1994.Blazor.IntersectionObserver.Components
@inject IJSRuntime Js @inject IJSRuntime Js
@if(_init) // FIXME: We need to wait for the Component to render once before initializing the Intersection Observer. @if (_init) // FIXME: We need to wait for the Component to render once before initializing the Intersection Observer.
// With the <IntersectionObserver> Component this is AFAIK only possible by not rendering it until then. // With the <IntersectionObserver> Component this is AFAIK only possible by not rendering it until then.
// The proper fix for this is to change to the Service Pattern. // The proper fix for this is to change to the Service Pattern.
// But that requires the IntersectionObserver Library to be modified to return what element an observation update is for. // But that requires the IntersectionObserver Library to be modified to return what element an observation update is for.
{ {
<IntersectionObserve Options="new IntersectionObserverOptions(){ Root = Scroller, RootMargin = margin}" OnChange="@(entry => Change(entry))"> <IntersectionObserve Options="new IntersectionObserverOptions() { Root = Scroller, RootMargin = Margin }" OnChange="@(entry => Change(entry))">
<div class="tgt" @ref="context.Ref.Current"> <div class="tgt" @ref="context.Ref.Current">
@if (_isIntersecting) @if (_isIntersecting)
{ {
<TimelineNote Note="Note" /> <TimelineNote Note="Note"/>
}else { }
<div class="placeholder" style="height: @(Height ?? 150)px"></div> else
} {
</div> <div class="placeholder" style="height: @(Height ?? 150)px"></div>
</IntersectionObserve> }
</div>
</IntersectionObserve>
} }
@code { @code {
[Parameter][EditorRequired] public required NoteResponse Note { get; set; } [Parameter] [EditorRequired] public required NoteResponse Note { get; set; }
[Parameter][EditorRequired] public ElementReference Scroller { get; set; } [Parameter] [EditorRequired] public ElementReference Scroller { get; set; }
private IJSObjectReference? _module;
private int? Height { get; set; } = null; private const string Margin = "200%";
private bool _isIntersecting = true; private IJSObjectReference? _module;
private string margin = "200%"; private int? Height { get; set; }
private bool _init = false; private bool _isIntersecting = true;
private bool _init;
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
_module = await Js.InvokeAsync<IJSObjectReference>("import", _module = await Js.InvokeAsync<IJSObjectReference>("import",
"./Components/LazyNote.razor.js"); "./Components/LazyNote.razor.js");
} }
} }
@ -44,10 +47,9 @@
{ {
_init = true; _init = true;
} }
} }
private async Task Change (IntersectionObserverEntry entry) private async Task Change(IntersectionObserverEntry entry)
{ {
if (entry.IsIntersecting == false) if (entry.IsIntersecting == false)
{ {

View file

@ -1,4 +1,3 @@
@using AngleSharp.Dom
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@inject ApiService ApiService; @inject ApiService ApiService;
@ -79,7 +78,6 @@
Note.Reactions[index].Count++; Note.Reactions[index].Count++;
} }
break; break;
} }
@ -121,7 +119,7 @@
public void Reply() public void Reply()
{ {
ComposeService.ComposeDialog?.OpenDialog(Note, null); ComposeService.ComposeDialog?.OpenDialog(Note);
} }
public void Renote() public void Renote()

View file

@ -1,5 +1,4 @@
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
<div class="note-footer"> <div class="note-footer">
@if (Reactions.Count > 0) @if (Reactions.Count > 0)
@ -44,7 +43,7 @@
</button> </button>
<button class="btn positioned" @onclick="ToggleEmojiPicker" @onclick:stopPropagation="true"> <button class="btn positioned" @onclick="ToggleEmojiPicker" @onclick:stopPropagation="true">
<Icon Name="Icons.Smiley" Size="1.3em"/> <Icon Name="Icons.Smiley" Size="1.3em"/>
<EmojiPicker @ref="EmojiPicker" OnEmojiSelect="React" /> <EmojiPicker @ref="EmojiPicker" OnEmojiSelect="React"/>
</button> </button>
<button class="btn" @onclick="Quote" @onclick:stopPropagation="true"> <button class="btn" @onclick="Quote" @onclick:stopPropagation="true">
<Icon Name="Icons.Quotes" Size="1.3em"/> <Icon Name="Icons.Quotes" Size="1.3em"/>
@ -60,7 +59,7 @@
[Parameter] [EditorRequired] public required bool IsLiked { get; set; } [Parameter] [EditorRequired] public required bool IsLiked { get; set; }
[Parameter] [EditorRequired] public required int Renotes { get; set; } [Parameter] [EditorRequired] public required int Renotes { get; set; }
[Parameter] public bool RenotePossible { get; set; } [Parameter] public bool RenotePossible { get; set; }
private EmojiPicker EmojiPicker { get; set; } private EmojiPicker? EmojiPicker { get; set; }
[CascadingParameter] NoteComponent NoteComponent { get; set; } = null!; [CascadingParameter] NoteComponent NoteComponent { get; set; } = null!;
@ -86,12 +85,11 @@
private void ToggleEmojiPicker() private void ToggleEmojiPicker()
{ {
EmojiPicker.Toggle(); EmojiPicker?.Toggle();
} }
private void React(EmojiResponse emoji) private void React(EmojiResponse emoji)
{ {
NoteComponent.React(emoji.Name, true, url: emoji.PublicUrl); NoteComponent.React(emoji.Name, true, url: emoji.PublicUrl);
} }
} }

View file

@ -1,4 +1,5 @@
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization

View file

@ -1,7 +1,3 @@
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas
@if (_state == State.Init) @if (_state == State.Init)
{ {
<div class="scroller"> <div class="scroller">
@ -26,4 +22,3 @@
@code { @code {
} }

View file

@ -7,73 +7,74 @@ namespace Iceshrimp.Frontend.Components;
public partial class NotificationList : IAsyncDisposable public partial class NotificationList : IAsyncDisposable
{ {
[Inject] private StreamingService StreamingService { get; set; } = null!; [Inject] private StreamingService StreamingService { get; set; } = null!;
[Inject] private ApiService Api { get; set; } = null!; [Inject] private ApiService Api { get; set; } = null!;
private List<NotificationResponse> Notifications { get; set; } = []; private List<NotificationResponse> Notifications { get; set; } = [];
private State _state = State.Loading; private State _state = State.Loading;
private string? _minId; private string? _minId;
private enum State private enum State
{ {
Loading, Loading,
Error, Error,
Init Init
} }
private async Task GetNotifications() private async Task GetNotifications()
{ {
try try
{ {
var res = await Api.Notifications.GetNotifications(new PaginationQuery()); var res = await Api.Notifications.GetNotifications(new PaginationQuery());
if (res.Count > 0) if (res.Count > 0)
{ {
Notifications = res; Notifications = res;
_minId = res.Last().Id; _minId = res.Last().Id;
foreach (var el in res) foreach (var el in res)
{ {
Console.WriteLine(el.Type); Console.WriteLine(el.Type);
} }
} }
_state = State.Init; _state = State.Init;
} }
catch (ApiException) catch (ApiException)
{ {
_state = State.Error; _state = State.Error;
} }
} }
private async Task LoadMore() private async Task LoadMore()
{ {
var pq = new PaginationQuery { MaxId = _minId, Limit = 20 }; var pq = new PaginationQuery { MaxId = _minId, Limit = 20 };
var res = await Api.Notifications.GetNotifications(pq); var res = await Api.Notifications.GetNotifications(pq);
if (res.Count > 0) if (res.Count > 0)
{ {
Notifications.AddRange(res); Notifications.AddRange(res);
_minId = res.Last().Id; _minId = res.Last().Id;
StateHasChanged(); StateHasChanged();
} }
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
StreamingService.Notification += OnNotification; StreamingService.Notification += OnNotification;
await StreamingService.Connect(); await StreamingService.Connect();
await GetNotifications(); await GetNotifications();
StateHasChanged(); StateHasChanged();
} }
private async void OnNotification(object? _, NotificationResponse notificationResponse) private void OnNotification(object? _, NotificationResponse notificationResponse)
{ {
Notifications.Insert(0, notificationResponse); Notifications.Insert(0, notificationResponse);
StateHasChanged(); StateHasChanged();
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
StreamingService.Notification -= OnNotification; StreamingService.Notification -= OnNotification;
await StreamingService.DisposeAsync(); await StreamingService.DisposeAsync();
} GC.SuppressFinalize(this);
}
} }

View file

@ -1,4 +1,5 @@
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager

View file

@ -1,4 +1,5 @@
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@inject NavigationManager Navigation @inject NavigationManager Navigation
<div class="note-container" @onclick="OpenNote" id="@Note.Id"> <div class="note-container" @onclick="OpenNote" id="@Note.Id">

View file

@ -1,6 +1,3 @@
@using System.Collections.Specialized
@using System.Runtime.InteropServices.JavaScript
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns @using Iceshrimp.Frontend.Core.Services.StateServicePatterns
@using Iceshrimp.Shared.Schemas @using Iceshrimp.Shared.Schemas
@ -8,30 +5,33 @@
@using Ljbc1994.Blazor.IntersectionObserver.API @using Ljbc1994.Blazor.IntersectionObserver.API
@inject IIntersectionObserverService ObserverService @inject IIntersectionObserverService ObserverService
@inject IJSRuntime Js @inject IJSRuntime Js
@inject StateService StateService @inject StateService StateService
<div @ref="@Scroller" class="scroller"> <div @ref="@_scroller" class="scroller">
<div @ref="@_padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div> <div @ref="@_padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div>
@if(_loadingTop){ @if (_loadingTop)
<div class="target"> {
Loading! <div class="target">
</div> Loading!
</div>
} }
@foreach (var el in State.RenderedList) @foreach (var el in State.RenderedList)
{ {
<div class="target" @ref="@Ref"> <div class="target" @ref="@Ref">
<TimelineNote @key="el.Id" Note="el"></TimelineNote> <TimelineNote @key="el.Id" Note="el"></TimelineNote>
</div> </div>
} }
@if(loadingBottom){ @if (_loadingBottom)
<div class="target"> {
Loading! <div class="target">
</div> Loading!
</div>
} }
else else
{ {
<div class="target"><span>The end!<button @onclick="Down">Load more!</button></span></div> <div class="target">
<span>The end!<button @onclick="Down">Load more!</button></span>
</div>
} }
<div @ref="@_padBotRef" class="padding bottom" style="height: @(State.PadBottom + "px")"> <div @ref="@_padBotRef" class="padding bottom" style="height: @(State.PadBottom + "px")">
</div> </div>
@ -53,10 +53,10 @@
private bool _overscrollBottom = false; private bool _overscrollBottom = false;
private ElementReference _padTopRef; private ElementReference _padTopRef;
private ElementReference _padBotRef; private ElementReference _padBotRef;
private ElementReference Scroller; private ElementReference _scroller;
private bool _loadingTop = false; private bool _loadingTop = false;
private bool loadingBottom = false; private bool _loadingBottom = false;
public bool setScroll = false; private bool _setScroll = false;
private ElementReference Ref private ElementReference Ref
{ {
@ -68,18 +68,15 @@
private void InitialRender(string? id) private void InitialRender(string? id)
{ {
var a = new List<NoteResponse>(); State.RenderedList = NoteResponseList.Count < _count ? NoteResponseList : NoteResponseList.GetRange(0, _count);
a = NoteResponseList.Count < _count ? NoteResponseList : NoteResponseList.GetRange(0, _count);
State.RenderedList = a;
} }
private async Task LoadOlder() private async Task LoadOlder()
{ {
loadingBottom = true; _loadingBottom = true;
StateHasChanged(); StateHasChanged();
await ReachedEnd.InvokeAsync(); await ReachedEnd.InvokeAsync();
loadingBottom = false; _loadingBottom = false;
StateHasChanged(); StateHasChanged();
} }
@ -98,7 +95,6 @@
StateService.VirtualScroller.SetState("home", State); StateService.VirtualScroller.SetState("home", State);
} }
private async Task RemoveAbove(int amount) private async Task RemoveAbove(int amount)
{ {
for (int i = 0; i < amount; i++) for (int i = 0; i < amount; i++)
@ -109,7 +105,6 @@
} }
State.RenderedList.RemoveRange(0, amount); State.RenderedList.RemoveRange(0, amount);
} }
private async Task Down() private async Task Down()
@ -128,10 +123,8 @@
var heightChange = 0; var heightChange = 0;
foreach (var el in a) foreach (var el in a)
{ {
if (State.Height.ContainsKey(el.Id)) if (State.Height.TryGetValue(el.Id, out var value))
{ heightChange += value;
heightChange += State.Height[el.Id];
}
} }
if (State.PadBottom > 0) State.PadBottom -= heightChange; if (State.PadBottom > 0) State.PadBottom -= heightChange;
@ -155,6 +148,7 @@
State.PadBottom += height; State.PadBottom += height;
State.Height[State.RenderedList[i].Id] = height; State.Height[State.RenderedList[i].Id] = height;
} }
var index = NoteResponseList.IndexOf(State.RenderedList.First()); var index = NoteResponseList.IndexOf(State.RenderedList.First());
var a = NoteResponseList.GetRange(index - updateCount, updateCount); var a = NoteResponseList.GetRange(index - updateCount, updateCount);
var heightChange = 0; var heightChange = 0;
@ -174,8 +168,6 @@
private async Task SetupObservers() private async Task SetupObservers()
{ {
// Enabling root margin causes erratic virtual scroller behavior, i've not figured out why
var options = new IntersectionObserverOptions { RootMargin = "100%" };
OvrscrlObsvTop = await ObserverService.Create(OverscrollCallbackTop); OvrscrlObsvTop = await ObserverService.Create(OverscrollCallbackTop);
OvrscrlObsvBottom = await ObserverService.Create(OverscrollCallbackBottom); OvrscrlObsvBottom = await ObserverService.Create(OverscrollCallbackBottom);
@ -192,7 +184,6 @@
} }
} }
private async void OverscrollCallbackTop(IList<IntersectionObserverEntry> list) private async void OverscrollCallbackTop(IList<IntersectionObserverEntry> list)
{ {
var entry = list.First(); var entry = list.First();
@ -242,13 +233,13 @@
private async Task GetScrollTop() private async Task GetScrollTop()
{ {
var scrollTop = await Module.InvokeAsync<float>("GetScrollTop", Scroller); var scrollTop = await Module.InvokeAsync<float>("GetScrollTop", _scroller);
State.ScrollTop = scrollTop; State.ScrollTop = scrollTop;
} }
private async Task SetScrollTop() private async Task SetScrollTop()
{ {
await Module.InvokeVoidAsync("SetScrollTop", State.ScrollTop, Scroller); await Module.InvokeVoidAsync("SetScrollTop", State.ScrollTop, _scroller);
} }
protected override void OnInitialized() protected override void OnInitialized()
@ -256,9 +247,8 @@
try try
{ {
var virtualScrollerState = StateService.VirtualScroller.GetState("home"); var virtualScrollerState = StateService.VirtualScroller.GetState("home");
State = virtualScrollerState; State = virtualScrollerState;
setScroll = true; _setScroll = true;
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -279,13 +269,12 @@
await SetupObservers(); await SetupObservers();
} }
if (setScroll) if (_setScroll)
{ {
await SetScrollTop(); await SetScrollTop();
setScroll = false; _setScroll = false;
} }
await SaveState(); await SaveState();
} }
} }

View file

@ -3,176 +3,188 @@ using AngleSharp.Dom;
using Iceshrimp.Parsing; using Iceshrimp.Parsing;
using Iceshrimp.Shared.Schemas; using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.FSharp.Core;
namespace Iceshrimp.Frontend.Core.Miscellaneous; namespace Iceshrimp.Frontend.Core.Miscellaneous;
public class MfmRenderer public static class MfmRenderer
{ {
public static async Task<MarkupString> RenderString(string text, List<EmojiResponse> emoji) public static async Task<MarkupString> RenderString(string text, List<EmojiResponse> emoji)
{ {
var res = Mfm.parse(text); var res = Mfm.parse(text);
var context = BrowsingContext.New(); var context = BrowsingContext.New();
var document = await context.OpenNewAsync(); var document = await context.OpenNewAsync();
var renderedMfm = MfmRenderer.RenderMultipleNodes(res, document, emoji); var renderedMfm = RenderMultipleNodes(res, document, emoji);
var html = renderedMfm.ToHtml(); var html = renderedMfm.ToHtml();
return new MarkupString(html); return new MarkupString(html);
} }
public static INode RenderMultipleNodes(IEnumerable<MfmNodeTypes.MfmNode> nodes, IDocument document, List<EmojiResponse> emoji)
{
var el = document.CreateElement("span");
el.SetAttribute("mfm", "mfm");
el.ClassName = "mfm";
foreach (var node in nodes)
{
try
{
el.AppendNodes(RenderNode(node, document, emoji));
}
catch (NotImplementedException e)
{
var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
el.AppendNodes(fallback);
}
}
return el; private static INode RenderMultipleNodes(
} IEnumerable<MfmNodeTypes.MfmNode> nodes, IDocument document, List<EmojiResponse> emoji
private static INode RenderNode(MfmNodeTypes.MfmNode node, IDocument document, List<EmojiResponse> emoji) )
{ {
var rendered = node switch var el = document.CreateElement("span");
{ el.SetAttribute("mfm", "mfm");
MfmNodeTypes.MfmCenterNode mfmCenterNode => throw new NotImplementedException($"{mfmCenterNode.GetType()}"), el.ClassName = "mfm";
MfmNodeTypes.MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode, document), foreach (var node in nodes)
MfmNodeTypes.MfmMathBlockNode mfmMathBlockNode => throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"), {
MfmNodeTypes.MfmQuoteNode mfmQuoteNode => throw new NotImplementedException($"{mfmQuoteNode.GetType()}"), try
MfmNodeTypes.MfmSearchNode mfmSearchNode => throw new NotImplementedException($"{mfmSearchNode.GetType()}"), {
MfmNodeTypes.MfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"), el.AppendNodes(RenderNode(node, document, emoji));
MfmNodeTypes.MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document), }
MfmNodeTypes.MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji), catch (NotImplementedException e)
MfmNodeTypes.MfmFnNode mfmFnNode => throw new NotImplementedException($"{mfmFnNode.GetType()}"), {
MfmNodeTypes.MfmHashtagNode mfmHashtagNode => throw new NotImplementedException($"{mfmHashtagNode.GetType()}"), var fallback = document.CreateElement("span");
MfmNodeTypes.MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document), fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
MfmNodeTypes.MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode, document), el.AppendNodes(fallback);
MfmNodeTypes.MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode, document), }
MfmNodeTypes.MfmMathInlineNode mfmMathInlineNode => throw new NotImplementedException($"{mfmMathInlineNode.GetType()}"), }
MfmNodeTypes.MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, document),
MfmNodeTypes.MfmPlainNode mfmPlainNode => throw new NotImplementedException($"{mfmPlainNode.GetType()}"),
MfmNodeTypes.MfmSmallNode mfmSmallNode => throw new NotImplementedException($"{mfmSmallNode.GetType()}"),
MfmNodeTypes.MfmStrikeNode mfmStrikeNode => throw new NotImplementedException($"{mfmStrikeNode.GetType()}"),
MfmNodeTypes.MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode, document),
MfmNodeTypes.MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode, document),
MfmNodeTypes.MfmInlineNode mfmInlineNode => throw new NotImplementedException($"{mfmInlineNode.GetType()}"),
_ => throw new ArgumentOutOfRangeException(nameof(node))
};
if (node.Children.Length > 0)
{
foreach (var childNode in node.Children)
{
try
{
rendered.AppendNodes(RenderNode(childNode, document, emoji));
}
catch (NotImplementedException e)
{
var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
rendered.AppendNodes(fallback);
}
}
}
return rendered;
}
private static INode MfmCodeBlockNode(MfmNodeTypes.MfmCodeBlockNode node, IDocument document) return el;
{ }
var el = document.CreateElement("pre");
var childEl = document.CreateElement("code");
childEl.TextContent = node.Code;
el.AppendChild(childEl);
return el;
}
private static INode MfmInlineCodeNode(MfmNodeTypes.MfmInlineCodeNode node, IDocument document) private static INode RenderNode(MfmNodeTypes.MfmNode node, IDocument document, List<EmojiResponse> emoji)
{ {
var el = document.CreateElement("code"); var rendered = node switch
el.TextContent = node.Code; {
return el; MfmNodeTypes.MfmCenterNode mfmCenterNode => throw new NotImplementedException($"{mfmCenterNode.GetType()}"),
} MfmNodeTypes.MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode, document),
MfmNodeTypes.MfmMathBlockNode mfmMathBlockNode =>
throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"),
MfmNodeTypes.MfmQuoteNode mfmQuoteNode => throw new NotImplementedException($"{mfmQuoteNode.GetType()}"),
MfmNodeTypes.MfmSearchNode mfmSearchNode => throw new NotImplementedException($"{mfmSearchNode.GetType()}"),
MfmNodeTypes.MfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"),
MfmNodeTypes.MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document),
MfmNodeTypes.MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji),
MfmNodeTypes.MfmFnNode mfmFnNode => throw new NotImplementedException($"{mfmFnNode.GetType()}"),
MfmNodeTypes.MfmHashtagNode mfmHashtagNode =>
throw new NotImplementedException($"{mfmHashtagNode.GetType()}"),
MfmNodeTypes.MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document),
MfmNodeTypes.MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode, document),
MfmNodeTypes.MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode, document),
MfmNodeTypes.MfmMathInlineNode mfmMathInlineNode =>
throw new NotImplementedException($"{mfmMathInlineNode.GetType()}"),
MfmNodeTypes.MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, document),
MfmNodeTypes.MfmPlainNode mfmPlainNode => throw new NotImplementedException($"{mfmPlainNode.GetType()}"),
MfmNodeTypes.MfmSmallNode mfmSmallNode => throw new NotImplementedException($"{mfmSmallNode.GetType()}"),
MfmNodeTypes.MfmStrikeNode mfmStrikeNode => throw new NotImplementedException($"{mfmStrikeNode.GetType()}"),
MfmNodeTypes.MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode, document),
MfmNodeTypes.MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode, document),
MfmNodeTypes.MfmInlineNode mfmInlineNode => throw new NotImplementedException($"{mfmInlineNode.GetType()}"),
_ => throw new ArgumentOutOfRangeException(nameof(node))
};
if (node.Children.Length > 0)
{
foreach (var childNode in node.Children)
{
try
{
rendered.AppendNodes(RenderNode(childNode, document, emoji));
}
catch (NotImplementedException e)
{
var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
rendered.AppendNodes(fallback);
}
}
}
private static INode MfmLinkNode(MfmNodeTypes.MfmLinkNode node, IDocument document) return rendered;
{ }
var el = document.CreateElement("a");
el.SetAttribute("href", node.Url);
el.ClassName = "link-node";
return el;
}
private static INode MfmItalicNode(MfmNodeTypes.MfmItalicNode node, IDocument document) private static INode MfmCodeBlockNode(MfmNodeTypes.MfmCodeBlockNode node, IDocument document)
{ {
var el = document.CreateElement("span"); var el = document.CreateElement("pre");
el.SetAttribute("style", "font-style: italic"); var childEl = document.CreateElement("code");
return el; childEl.TextContent = node.Code;
} el.AppendChild(childEl);
return el;
}
private static INode MfmEmojiCodeNode(MfmNodeTypes.MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList) private static INode MfmInlineCodeNode(MfmNodeTypes.MfmInlineCodeNode node, IDocument document)
{ {
var el = document.CreateElement("span"); var el = document.CreateElement("code");
el.ClassName = "emoji"; el.TextContent = node.Code;
return el;
}
var emoji = emojiList.Find(p => p.Name == node.Name); private static INode MfmLinkNode(MfmNodeTypes.MfmLinkNode node, IDocument document)
if (emoji is null) {
{ var el = document.CreateElement("a");
el.TextContent = node.Name; el.SetAttribute("href", node.Url);
} el.ClassName = "link-node";
else return el;
{ }
var image = document.CreateElement("img");
image.SetAttribute("src", emoji.PublicUrl);
image.SetAttribute("alt", node.Name);
el.AppendChild(image);
}
return el; private static INode MfmItalicNode(MfmNodeTypes.MfmItalicNode _, IDocument document)
} {
private static INode MfmUrlNode(MfmNodeTypes.MfmUrlNode node, IDocument document) var el = document.CreateElement("span");
{ el.SetAttribute("style", "font-style: italic");
var el = document.CreateElement("a"); return el;
el.SetAttribute("href", node.Url); }
el.ClassName = "url-node";
el.TextContent = node.Url;
return el;
}
private static INode MfmBoldNode(MfmNodeTypes.MfmBoldNode node, IDocument document) private static INode MfmEmojiCodeNode(
{ MfmNodeTypes.MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList
var el = document.CreateElement("strong"); )
return el; {
} var el = document.CreateElement("span");
private static INode MfmTextNode(MfmNodeTypes.MfmTextNode node, IDocument document) el.ClassName = "emoji";
{
var el = document.CreateElement("span");
el.TextContent = node.Text;
return el;
}
private static INode MfmMentionNode(MfmNodeTypes.MfmMentionNode node, IDocument document)
{
var link = document.CreateElement("a");
link.SetAttribute("href", $"/@{node.Acct}");
link.ClassName = "mention";
var userPart = document.CreateElement("span");
userPart.ClassName = "user";
userPart.TextContent = $"@{node.Username}";
link.AppendChild(userPart);
if (node.Host != null)
{
var hostPart = document.CreateElement("span");
hostPart.ClassName = "host";
hostPart.TextContent = $"@{node.Host.Value}";
link.AppendChild(hostPart);
}
return link; var emoji = emojiList.Find(p => p.Name == node.Name);
} if (emoji is null)
{
el.TextContent = node.Name;
}
else
{
var image = document.CreateElement("img");
image.SetAttribute("src", emoji.PublicUrl);
image.SetAttribute("alt", node.Name);
el.AppendChild(image);
}
return el;
}
private static INode MfmUrlNode(MfmNodeTypes.MfmUrlNode node, IDocument document)
{
var el = document.CreateElement("a");
el.SetAttribute("href", node.Url);
el.ClassName = "url-node";
el.TextContent = node.Url;
return el;
}
private static INode MfmBoldNode(MfmNodeTypes.MfmBoldNode _, IDocument document)
{
var el = document.CreateElement("strong");
return el;
}
private static INode MfmTextNode(MfmNodeTypes.MfmTextNode node, IDocument document)
{
var el = document.CreateElement("span");
el.TextContent = node.Text;
return el;
}
private static INode MfmMentionNode(MfmNodeTypes.MfmMentionNode node, IDocument document)
{
var link = document.CreateElement("a");
link.SetAttribute("href", $"/@{node.Acct}");
link.ClassName = "mention";
var userPart = document.CreateElement("span");
userPart.ClassName = "user";
userPart.TextContent = $"@{node.Username}";
link.AppendChild(userPart);
if (node.Host != null)
{
var hostPart = document.CreateElement("span");
hostPart.ClassName = "host";
hostPart.TextContent = $"@{node.Host.Value}";
link.AppendChild(hostPart);
}
return link;
}
} }

View file

@ -1,5 +1,4 @@
using Iceshrimp.Frontend.Components; using Iceshrimp.Frontend.Components;
using Microsoft.AspNetCore.Components;
namespace Iceshrimp.Frontend.Core.Services; namespace Iceshrimp.Frontend.Core.Services;

View file

@ -5,7 +5,7 @@
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@inject ApiService Api @inject ApiService Api
@if (_init) @if (_init)
{ {
@ -64,11 +64,12 @@
private UserProfileResponse? Profile { get; set; } private UserProfileResponse? Profile { get; set; }
private string? MinId { get; set; } private string? MinId { get; set; }
private List<NoteResponse> UserNotes { get; set; } = []; private List<NoteResponse> UserNotes { get; set; } = [];
private bool _loading = true;
private bool _init = false; private bool _loading = true;
private bool _notFound = false; private bool _init;
private bool _error = false; private bool _notFound;
private bool _fetchLock = false; private bool _error;
private bool _fetchLock;
private async Task GetNotes(string? minId) private async Task GetNotes(string? minId)
{ {

View file

@ -52,8 +52,8 @@ else
private IList<NoteResponse>? Ascendants { get; set; } private IList<NoteResponse>? Ascendants { get; set; }
private IJSObjectReference? Module { get; set; } private IJSObjectReference? Module { get; set; }
private ElementReference RootNoteRef { get; set; } private ElementReference RootNoteRef { get; set; }
private bool _init = false; private bool _init;
private bool _error = false; private bool _error;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {

View file

@ -101,6 +101,7 @@
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ENUM_MEMBERS_ON_LINE/@EntryValue">1</s:Int64> <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ENUM_MEMBERS_ON_LINE/@EntryValue">1</s:Int64>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/NESTED_TERNARY_STYLE/@EntryValue">EXPANDED</s:String> <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/NESTED_TERNARY_STYLE/@EntryValue">EXPANDED</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">NEXT_LINE</s:String> <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">NEXT_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/RazorCSharpFormat/RemoveBlankLinesNearBraces/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String> <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_IF_LONG</s:String> <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_IF_LONG</s:String>