Compare commits

...
Sign in to create a new pull request.

10 commits

20 changed files with 136 additions and 60 deletions

View file

@ -1,7 +1,6 @@
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Shared.Schemas.Web; using Iceshrimp.Shared.Schemas.Web;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -10,16 +9,17 @@ namespace Iceshrimp.Backend.Controllers.Web.Renderers;
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db)
{ {
public async Task<UserResponse> RenderOne(User user, UserRendererDto? data = null) private UserResponse Render(User user, UserRendererDto data)
{ {
var instance = user.IsLocalUser var instance = user.IsRemoteUser ? data.InstanceData.FirstOrDefault(p => p.Host == user.Host) : null;
? null
: (data?.InstanceData ?? await GetInstanceData([user])).FirstOrDefault(p => p.Host == user.Host);
//TODO: populate the below two lines for local users //TODO: populate the below two lines for local users
var instanceName = user.IsLocalUser ? config.Value.AccountDomain : instance?.Name; var instanceName = user.IsLocalUser ? config.Value.AccountDomain : instance?.Name;
var instanceIcon = user.IsLocalUser ? null : instance?.FaviconUrl; var instanceIcon = user.IsLocalUser ? null : instance?.FaviconUrl;
if (!data.Emojis.TryGetValue(user.Id, out var emoji))
throw new Exception("DTO didn't contain emoji for user");
return new UserResponse return new UserResponse
{ {
Id = user.Id, Id = user.Id,
@ -30,10 +30,20 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
BannerUrl = user.BannerUrl, BannerUrl = user.BannerUrl,
InstanceName = instanceName, InstanceName = instanceName,
InstanceIconUrl = instanceIcon, InstanceIconUrl = instanceIcon,
Emojis = emoji,
MovedTo = user.MovedToUri MovedTo = user.MovedToUri
}; };
} }
public async Task<UserResponse> RenderOne(User user)
{
var instanceData = await GetInstanceData([user]);
var emojis = await GetEmojis([user]);
var data = new UserRendererDto { Emojis = emojis, InstanceData = instanceData };
return Render(user, data);
}
private async Task<List<Instance>> GetInstanceData(IEnumerable<User> users) private async Task<List<Instance>> GetInstanceData(IEnumerable<User> users)
{ {
var hosts = users.Select(p => p.Host).Where(p => p != null).Distinct().Cast<string>(); var hosts = users.Select(p => p.Host).Where(p => p != null).Distinct().Cast<string>();
@ -43,12 +53,40 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
public async Task<IEnumerable<UserResponse>> RenderMany(IEnumerable<User> users) public async Task<IEnumerable<UserResponse>> RenderMany(IEnumerable<User> users)
{ {
var userList = users.ToList(); var userList = users.ToList();
var data = new UserRendererDto { InstanceData = await GetInstanceData(userList) }; var data = new UserRendererDto
return await userList.Select(p => RenderOne(p, data)).AwaitAllAsync(); {
InstanceData = await GetInstanceData(userList), Emojis = await GetEmojis(userList)
};
return userList.Select(p => Render(p, data));
} }
public class UserRendererDto private async Task<Dictionary<string, List<EmojiResponse>>> GetEmojis(ICollection<User> users)
{ {
public List<Instance>? InstanceData; var ids = users.SelectMany(p => p.Emojis).ToList();
if (ids.Count == 0) return users.ToDictionary<User, string, List<EmojiResponse>>(p => p.Id, _ => []);
var emoji = await db.Emojis
.Where(p => ids.Contains(p.Id))
.Select(p => new EmojiResponse
{
Id = p.Id,
Name = p.Name,
Uri = p.Uri,
Aliases = p.Aliases,
Category = p.Category,
PublicUrl = p.PublicUrl,
License = p.License,
Sensitive = p.Sensitive
})
.ToListAsync();
return users.ToDictionary(p => p.Id, p => emoji.Where(e => p.Emojis.Contains(e.Id)).ToList());
}
private class UserRendererDto
{
public required List<Instance> InstanceData;
public required Dictionary<string, List<EmojiResponse>> Emojis;
} }
} }

View file

@ -5,13 +5,14 @@
@code { @code {
[Parameter] [EditorRequired] public required string? Text { get; set; } [Parameter] [EditorRequired] public required string? Text { get; set; }
[Parameter] public List<EmojiResponse> Emoji { get; set; } = []; [Parameter] public List<EmojiResponse> Emoji { get; set; } = [];
[Parameter] public bool Simple { get; set; } = false;
private MarkupString TextBody { get; set; } private MarkupString TextBody { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (Text != null) if (Text != null)
{ {
TextBody = await MfmRenderer.RenderString(Text, Emoji); TextBody = await MfmRenderer.RenderString(Text, Emoji, Simple);
} }
} }
@ -19,7 +20,7 @@
{ {
if (Text != null) if (Text != null)
{ {
TextBody = await MfmRenderer.RenderString(Text, Emoji); TextBody = await MfmRenderer.RenderString(Text, Emoji, Simple);
} }
} }
} }

View file

@ -46,6 +46,23 @@
} }
} }
::deep {
.emoji.simple {
> img {
height: 1.25em;
vertical-align: -0.25em;
}
}
}
::deep {
.emoji.simple {
> img:hover {
transform: none;
}
}
}
::deep { ::deep {
.quote-node { .quote-node {
/*Copying the appearance of -js*/ /*Copying the appearance of -js*/

View file

@ -7,14 +7,7 @@
<div class="renote-info"> <div class="renote-info">
<span class="user"> <span class="user">
<Icon Name="Icons.Repeat"/> Renoted by <Icon Name="Icons.Repeat"/> Renoted by
@if (NoteResponse.User.DisplayName != null) <UserDisplayName User="@NoteResponse.User"/>
{
@NoteResponse.User.DisplayName
}
else
{
@NoteResponse.User.Username
}
</span> </span>
<span class="metadata"> <span class="metadata">
<NoteMetadata Visibility="NoteResponse.Visibility" InstanceName="@null" CreatedAt="DateTime.Parse(NoteResponse.CreatedAt)"/> <NoteMetadata Visibility="NoteResponse.Visibility" InstanceName="@null" CreatedAt="DateTime.Parse(NoteResponse.CreatedAt)"/>

View file

@ -7,7 +7,7 @@
@if (NoteBase.Cw != null) @if (NoteBase.Cw != null)
{ {
<div class="cw"> <div class="cw">
<span class="cw-field">@NoteBase.Cw</span><button class="cw-button" @onclick="ToggleCw" @onclick:stopPropagation="true">Toggle CW</button> <span class="cw-field"><MfmText Text="@NoteBase.Cw" Emoji="@NoteBase.Emoji" Simple="@true"/></span><button class="cw-button" @onclick="ToggleCw" @onclick:stopPropagation="true">Toggle CW</button>
</div> </div>
<div hidden="@_cwToggle" class="note-body @(_cwToggle ? "hidden" : "") @(Indented ? "indent" : "")" @ref="Body"> <div hidden="@_cwToggle" class="note-body @(_cwToggle ? "hidden" : "") @(Indented ? "indent" : "")" @ref="Body">
<span> <span>

View file

@ -5,12 +5,7 @@
@inject ComposeService ComposeService @inject ComposeService ComposeService
@inject SessionService Session; @inject SessionService Session;
<div class="note-header"> <div class="note-header">
<NoteUserInfo <NoteUserInfo User="@Note.User" Indented="Indented"/>
AvatarUrl="@Note.User.AvatarUrl"
DisplayName="@Note.User.DisplayName"
Username="@Note.User.Username"
Host="@Note.User.Host"
Indented="Indented"/>
<NoteMetadata <NoteMetadata
Visibility="@Note.Visibility" Visibility="@Note.Visibility"
InstanceName="@Note.User.InstanceName" InstanceName="@Note.User.InstanceName"

View file

@ -1,27 +1,25 @@
@using Iceshrimp.Shared.Schemas.Web
@inject NavigationManager Nav @inject NavigationManager Nav
@if (Indented == false) @if (Indented == false)
{ {
<img @onclick="OpenProfile" class="user-avatar" src="@AvatarUrl" alt="@(DisplayName ?? Username)" role="link"/> <img @onclick="OpenProfile" class="user-avatar" src="@(User.AvatarUrl ?? $"/identicon/{User.Id}")" alt="@(User.DisplayName ?? User.Username)" role="link"/>
} }
<div class="name-section"> <div class="name-section">
<span class="display-name">@(DisplayName ?? Username)</span> <span class="display-name"><UserDisplayName User="@User"/></span>
<span class="identifier">@@@Username@(Host != null ? $"@{Host}" : "")</span> <span class="identifier">@@@User.Username@(User.Host != null ? $"@{User.Host}" : "")</span>
</div> </div>
@code { @code {
[Parameter] [EditorRequired] public required string AvatarUrl { get; set; } [Parameter] [EditorRequired] public required UserResponse User { get; set; }
[Parameter] [EditorRequired] public required string? DisplayName { get; set; } [Parameter] [EditorRequired] public required bool Indented { get; set; }
[Parameter] [EditorRequired] public required string Username { get; set; }
[Parameter] [EditorRequired] public required bool Indented { get; set; }
[Parameter] [EditorRequired] public required string? Host { get; set; }
private void OpenProfile() private void OpenProfile()
{ {
var path = $"@{Username}"; var path = $"@{User.Username}";
if (Host?.Length > 0) if (User.Host?.Length > 0)
{ {
path += $"@{Host}"; path += $"@{User.Host}";
} }
Nav.NavigateTo($"/{path}"); Nav.NavigateTo($"/{path}");

View file

@ -13,7 +13,7 @@
<div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry"> <div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry">
<img class="icon" src="@el.AvatarUrl"/> <img class="icon" src="@el.AvatarUrl"/>
<div class="name-section"> <div class="name-section">
<div class="displayname">@(el.DisplayName ?? el.Username)</div> <div class="displayname"><UserDisplayName User="@el"/></div>
<div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div> <div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div>
</div> </div>
</div> </div>

View file

@ -13,7 +13,7 @@
<div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry"> <div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry">
<img class="icon" src="@el.AvatarUrl"/> <img class="icon" src="@el.AvatarUrl"/>
<div class="name-section"> <div class="name-section">
<div class="displayname">@(el.DisplayName ?? el.Username)</div> <div class="displayname"><UserDisplayName User="@el"/></div>
<div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div> <div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div>
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@
<div class="notification-body"> <div class="notification-body">
@if (NotificationResponse is { User: not null }) @if (NotificationResponse is { User: not null })
{ {
<span @onclick="OpenProfile" class="display-name">@(NotificationResponse.User.DisplayName ?? NotificationResponse.User.Username)</span> <span @onclick="OpenProfile" class="display-name"><UserDisplayName User="@NotificationResponse.User"/></span>
} }
@if (NotificationResponse is { Note: not null, Type: "like", User: not null }) @if (NotificationResponse is { Note: not null, Type: "like", User: not null })

View file

@ -7,7 +7,7 @@
@if (UserProfile.Bio != null) @if (UserProfile.Bio != null)
{ {
<div class="bio"> <div class="bio">
<MfmText Text="@UserProfile.Bio"/> <MfmText Text="@UserProfile.Bio" Emoji="@Emojis"/>
</div> </div>
} }
<div class="data"> <div class="data">
@ -37,7 +37,7 @@
<div class="fields"> <div class="fields">
@foreach (var field in UserProfile.Fields) @foreach (var field in UserProfile.Fields)
{ {
<ProfileInfoField Field="field"/> <ProfileInfoField Field="field" Emojis="@Emojis"/>
} }
</div> </div>
} }
@ -63,4 +63,5 @@
@code { @code {
[Parameter] [EditorRequired] public required UserProfileResponse UserProfile { get; set; } [Parameter] [EditorRequired] public required UserProfileResponse UserProfile { get; set; }
[Parameter] [EditorRequired] public required List<EmojiResponse> Emojis { get; set; }
} }

View file

@ -6,13 +6,14 @@
{ {
<Icon class="verified" Name="Icons.SealCheck" Size="1.3em"/> <Icon class="verified" Name="Icons.SealCheck" Size="1.3em"/>
} }
@Field.Name <MfmText Text="@Field.Name.Split('\n')[0]" Emoji="@Emojis" Simple="@true"></MfmText>
</span> </span>
<span class="field-value"> <span class="field-value">
<MfmText Text="@Field.Value"></MfmText> <MfmText Text="@Field.Value.Split('\n')[0]" Emoji="@Emojis" Simple="@true"></MfmText>
</span> </span>
</div> </div>
@code { @code {
[Parameter] [EditorRequired] public required UserProfileField Field { get; set; } [Parameter] [EditorRequired] public required UserProfileField Field { get; set; }
[Parameter] [EditorRequired] public required List<EmojiResponse> Emojis { get; set; }
} }

View file

@ -13,7 +13,7 @@
<div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry"> <div @onclick="() => OpenProfile(el.Username, el.Host)" class="detail-entry">
<img class="icon" src="@el.AvatarUrl"/> <img class="icon" src="@el.AvatarUrl"/>
<div class="name-section"> <div class="name-section">
<div class="displayname">@(el.DisplayName ?? el.Username)</div> <div class="displayname"><UserDisplayName User="@el"/></div>
<div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div> <div class="username">@@@el.Username@(el.Host != null ? $"@{el.Host}" : "")</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,13 @@
@using Iceshrimp.Shared.Schemas.Web
@if (User.DisplayName != null && User.Emojis.Count != 0)
{
<MfmText Text="@User.DisplayName.Split('\n')[0]" Emoji="@User.Emojis" Simple="@true"/>
}
else
{
@(User.DisplayName ?? User.Username)
}
@code {
[Parameter] [EditorRequired] public required UserResponse User { get; set; }
}

View file

@ -14,7 +14,7 @@
</div> </div>
<div class="name-section"> <div class="name-section">
<div class="name"> <div class="name">
@(User.DisplayName ?? User.Username) <UserDisplayName User="@User"/>
</div> </div>
<div class="identifier"> <div class="identifier">
@@@User.Username @@@User.Username
@ -49,7 +49,7 @@
@if (UserProfile.Bio != null) @if (UserProfile.Bio != null)
{ {
<div class="bio"> <div class="bio">
<MfmText Text="@UserProfile.Bio"/> <MfmText Text="@UserProfile.Bio" Emoji="@User.Emojis"/>
</div> </div>
} }
} }

View file

@ -8,18 +8,18 @@ namespace Iceshrimp.Frontend.Core.Miscellaneous;
public static 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, bool simple = false)
{ {
var res = Mfm.parse(text); var res = simple ? Mfm.parseSimple(text) : Mfm.parse(text);
var context = BrowsingContext.New(); var context = BrowsingContext.New();
var document = await context.OpenNewAsync(); var document = await context.OpenNewAsync();
var renderedMfm = RenderMultipleNodes(res, document, emoji); var renderedMfm = RenderMultipleNodes(res, document, emoji, simple);
var html = renderedMfm.ToHtml(); var html = renderedMfm.ToHtml();
return new MarkupString(html); return new MarkupString(html);
} }
private static INode RenderMultipleNodes( private static INode RenderMultipleNodes(
IEnumerable<MfmNodeTypes.MfmNode> nodes, IDocument document, List<EmojiResponse> emoji IEnumerable<MfmNodeTypes.MfmNode> nodes, IDocument document, List<EmojiResponse> emoji, bool simple
) )
{ {
var el = document.CreateElement("span"); var el = document.CreateElement("span");
@ -29,7 +29,7 @@ public static class MfmRenderer
{ {
try try
{ {
el.AppendNodes(RenderNode(node, document, emoji)); el.AppendNodes(RenderNode(node, document, emoji, simple));
} }
catch (NotImplementedException e) catch (NotImplementedException e)
{ {
@ -42,7 +42,7 @@ public static class MfmRenderer
return el; return el;
} }
private static INode RenderNode(MfmNodeTypes.MfmNode node, IDocument document, List<EmojiResponse> emoji) private static INode RenderNode(MfmNodeTypes.MfmNode node, IDocument document, List<EmojiResponse> emoji, bool simple)
{ {
// Hard wrap makes this impossible to read // Hard wrap makes this impossible to read
// @formatter:off // @formatter:off
@ -55,7 +55,7 @@ public static class MfmRenderer
MfmNodeTypes.MfmSearchNode mfmSearchNode => throw new NotImplementedException($"{mfmSearchNode.GetType()}"), MfmNodeTypes.MfmSearchNode mfmSearchNode => throw new NotImplementedException($"{mfmSearchNode.GetType()}"),
MfmNodeTypes.MfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"), MfmNodeTypes.MfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"),
MfmNodeTypes.MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document), MfmNodeTypes.MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document),
MfmNodeTypes.MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji), MfmNodeTypes.MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji, simple),
MfmNodeTypes.MfmFnNode mfmFnNode => throw new NotImplementedException($"{mfmFnNode.GetType()}"), MfmNodeTypes.MfmFnNode mfmFnNode => throw new NotImplementedException($"{mfmFnNode.GetType()}"),
MfmNodeTypes.MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode, document), MfmNodeTypes.MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode, document),
MfmNodeTypes.MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document), MfmNodeTypes.MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document),
@ -79,7 +79,7 @@ public static class MfmRenderer
{ {
try try
{ {
rendered.AppendNodes(RenderNode(childNode, document, emoji)); rendered.AppendNodes(RenderNode(childNode, document, emoji, simple));
} }
catch (NotImplementedException e) catch (NotImplementedException e)
{ {
@ -157,11 +157,11 @@ public static class MfmRenderer
} }
private static INode MfmEmojiCodeNode( private static INode MfmEmojiCodeNode(
MfmNodeTypes.MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList MfmNodeTypes.MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList, bool simple
) )
{ {
var el = document.CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "emoji"; el.ClassName = simple ? "emoji simple" : "emoji";
var emoji = emojiList.Find(p => p.Name == node.Name); var emoji = emojiList.Find(p => p.Name == node.Name);
if (emoji is null) if (emoji is null)
@ -173,6 +173,7 @@ public static class MfmRenderer
var image = document.CreateElement("img"); var image = document.CreateElement("img");
image.SetAttribute("src", emoji.PublicUrl); image.SetAttribute("src", emoji.PublicUrl);
image.SetAttribute("alt", node.Name); image.SetAttribute("alt", node.Name);
image.SetAttribute("title", $":{emoji.Name}:");
el.AppendChild(image); el.AppendChild(image);
} }

View file

@ -72,6 +72,7 @@
Token = res.Token!, Token = res.Token!,
Host = res.User.Host, Host = res.User.Host,
IsAdmin = res.IsAdmin ?? false, IsAdmin = res.IsAdmin ?? false,
Emojis = res.User.Emojis,
MovedTo = res.User.MovedTo MovedTo = res.User.MovedTo
}); });
SessionService.SetSession(res.User.Id); SessionService.SetSession(res.User.Id);

View file

@ -25,7 +25,7 @@
</div> </div>
<div class="name-section"> <div class="name-section">
<div class="name"> <div class="name">
@(UserResponse.DisplayName ?? UserResponse.Username) <UserDisplayName User="@UserResponse"/>
</div> </div>
<div class="identifier"> <div class="identifier">
@@@UserResponse.Username @@@UserResponse.Username
@ -39,7 +39,7 @@
</div> </div>
<FollowButton User="UserResponse" UserProfile="Profile"/> <FollowButton User="UserResponse" UserProfile="Profile"/>
</div> </div>
<ProfileInfo UserProfile="Profile"/> <ProfileInfo Emojis="@UserResponse.Emojis" UserProfile="Profile"/>
</div> </div>
@if (UserNotes.Count > 0) @if (UserNotes.Count > 0)
{ {

View file

@ -188,6 +188,7 @@ module private MfmParser =
// References // References
let node, nodeRef = createParserForwardedToRef () let node, nodeRef = createParserForwardedToRef ()
let inlineNode, inlineNodeRef = createParserForwardedToRef () let inlineNode, inlineNodeRef = createParserForwardedToRef ()
let simple, simpleRef = createParserForwardedToRef ()
let seqFlatten items = let seqFlatten items =
seq { seq {
@ -375,6 +376,11 @@ module private MfmParser =
fnNode fnNode
charNode ] charNode ]
let simpleNodeSeq =
[ plainNode
emojiCodeNode
charNode ]
//TODO: still missing: FnNode //TODO: still missing: FnNode
let blockNodeSeq = let blockNodeSeq =
@ -386,9 +392,13 @@ module private MfmParser =
do nodeRef.Value <- choice <| seqAttempt (seqFlatten <| nodeSeq) do nodeRef.Value <- choice <| seqAttempt (seqFlatten <| nodeSeq)
do inlineNodeRef.Value <- choice <| (seqAttempt inlineNodeSeq) |>> fun v -> v :?> MfmInlineNode do inlineNodeRef.Value <- choice <| (seqAttempt inlineNodeSeq) |>> fun v -> v :?> MfmInlineNode
do simpleRef.Value <- choice <| seqAttempt simpleNodeSeq
// Final parse command // Final parse command
let parse = spaces >>. manyTill node eof .>> spaces let parse = spaces >>. manyTill node eof .>> spaces
let parseSimple = spaces >>. manyTill simple eof .>> spaces
open MfmParser open MfmParser
@ -397,3 +407,8 @@ module Mfm =
match runParserOnString parse 0 "" str with match runParserOnString parse 0 "" str with
| Success(result, _, _) -> aggregateText result | Success(result, _, _) -> aggregateText result
| Failure(s, _, _) -> failwith $"Failed to parse MFM: {s}" | Failure(s, _, _) -> failwith $"Failed to parse MFM: {s}"
let parseSimple str =
match runParserOnString parseSimple 0 "" str with
| Success(result, _, _) -> aggregateText result
| Failure(s, _, _) -> failwith $"Failed to parse MFM: {s}"

View file

@ -14,5 +14,7 @@ public class UserResponse
public required string? InstanceName { get; set; } public required string? InstanceName { get; set; }
public required string? InstanceIconUrl { get; set; } public required string? InstanceIconUrl { get; set; }
public List<EmojiResponse> Emojis { get; set; } = [];
[JI(Condition = WhenWritingNull)] public string? MovedTo { get; set; } [JI(Condition = WhenWritingNull)] public string? MovedTo { get; set; }
} }