Compare commits

..

No commits in common. "dev" and "blazor" have entirely different histories.
dev ... blazor

36 changed files with 394 additions and 682 deletions

View file

@ -2,11 +2,6 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

View file

@ -14,7 +14,7 @@ public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMed
[UsedImplicitly] [UsedImplicitly]
public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingletonService public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingletonService
{ {
public MfmRenderData? Render( public async Task<MfmRenderData?> RenderAsync(
string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement, string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement,
List<PreviewAttachment>? media = null List<PreviewAttachment>? media = null
) )
@ -27,14 +27,14 @@ public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingleton
flags.SupportsInlineMedia.Value = true; flags.SupportsInlineMedia.Value = true;
var mfmInlineMedia = media?.Select(m => new MfmInlineMedia(MfmInlineMedia.GetType(m.MimeType), m.Url, m.Alt)).ToList(); var mfmInlineMedia = media?.Select(m => new MfmInlineMedia(MfmInlineMedia.GetType(m.MimeType), m.Url, m.Alt)).ToList();
var serialized = converter.ToHtml(parsed, mentions, host, emoji: emoji, rootElement: rootElement, media: mfmInlineMedia); var serialized = await converter.ToHtmlAsync(parsed, mentions, host, emoji: emoji, rootElement: rootElement, media: mfmInlineMedia);
return new MfmRenderData(new MarkupString(serialized.Html), serialized.InlineMedia); return new MfmRenderData(new MarkupString(serialized.Html), serialized.InlineMedia);
} }
public MarkupString? RenderSimple(string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement) public async Task<MarkupString?> RenderSimpleAsync(string? text, string? host, List<Note.MentionedUser> mentions, List<Emoji> emoji, string rootElement)
{ {
var rendered = Render(text, host, mentions, emoji, rootElement); var rendered = await RenderAsync(text, host, mentions, emoji, rootElement);
return rendered?.Html; return rendered?.Html;
} }
} }

View file

@ -30,31 +30,31 @@ public class NoteRenderer(
var attachments = await GetAttachmentsAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes);
var polls = await GetPollsAsync(allNotes); var polls = await GetPollsAsync(allNotes);
return Render(note, users, mentions, emoji, attachments, polls); return await RenderAsync(note, users, mentions, emoji, attachments, polls);
} }
private PreviewNote Render( private async Task<PreviewNote> RenderAsync(
Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions, Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions,
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments, Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
Dictionary<string, PreviewPoll> polls Dictionary<string, PreviewPoll> polls
) )
{ {
var renderedText = mfm.Render(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]); var renderedText = await mfm.RenderAsync(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]);
var inlineMediaUrls = renderedText?.InlineMedia.Select(m => m.Src).ToArray() ?? []; var inlineMediaUrls = renderedText?.InlineMedia.Select(m => m.Src).ToArray() ?? [];
var res = new PreviewNote var res = new PreviewNote
{ {
User = users.First(p => p.Id == note.User.Id), User = users.First(p => p.Id == note.User.Id),
Text = renderedText?.Html, Text = renderedText?.Html,
Cw = note.Cw, Cw = note.Cw,
RawText = note.Text, RawText = note.Text,
Uri = note.Uri ?? note.GetPublicUri(instance.Value), Uri = note.Uri ?? note.GetPublicUri(instance.Value),
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value), QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false, QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false,
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(), Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
Poll = polls.GetValueOrDefault(note.Id), Poll = polls.GetValueOrDefault(note.Id),
CreatedAt = note.CreatedAt.ToDisplayStringTz(), CreatedAt = note.CreatedAt.ToDisplayStringTz(),
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz() UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
}; };
return res; return res;
@ -143,6 +143,8 @@ public class NoteRenderer(
var emoji = await GetEmojiAsync(allNotes); var emoji = await GetEmojiAsync(allNotes);
var attachments = await GetAttachmentsAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes);
var polls = await GetPollsAsync(allNotes); var polls = await GetPollsAsync(allNotes);
return notes.Select(p => Render(p, users, mentions, emoji, attachments, polls)).ToList(); return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments, polls))
.AwaitAllAsync()
.ToListAsync();
} }
} }

View file

@ -19,10 +19,10 @@ public class UserRenderer(
{ {
if (user == null) return null; if (user == null) return null;
var emoji = await GetEmojiAsync([user]); var emoji = await GetEmojiAsync([user]);
return Render(user, emoji); return await RenderAsync(user, emoji);
} }
private PreviewUser Render(User user, Dictionary<string, List<Emoji>> emoji) private async Task<PreviewUser> RenderAsync(User user, Dictionary<string, List<Emoji>> emoji)
{ {
var mentions = user.UserProfile?.Mentions ?? []; var mentions = user.UserProfile?.Mentions ?? [];
@ -37,8 +37,8 @@ public class UserRenderer(
AvatarUrl = user.GetAvatarUrl(instance.Value), AvatarUrl = user.GetAvatarUrl(instance.Value),
BannerUrl = user.GetBannerUrl(instance.Value), BannerUrl = user.GetBannerUrl(instance.Value),
RawDisplayName = user.DisplayName, RawDisplayName = user.DisplayName,
DisplayName = mfm.RenderSimple(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"), DisplayName = await mfm.RenderSimpleAsync(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
Bio = mfm.RenderSimple(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"), Bio = await mfm.RenderSimpleAsync(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
MovedToUri = user.MovedToUri MovedToUri = user.MovedToUri
}; };
// @formatter:on // @formatter:on
@ -64,6 +64,6 @@ public class UserRenderer(
public async Task<List<PreviewUser>> RenderManyAsync(List<User> users) public async Task<List<PreviewUser>> RenderManyAsync(List<User> users)
{ {
var emoji = await GetEmojiAsync(users); var emoji = await GetEmojiAsync(users);
return users.Select(p => Render(p, emoji)).ToList(); return await users.Select(p => RenderAsync(p, emoji)).AwaitAllAsync().ToListAsync();
} }
} }

View file

@ -55,7 +55,7 @@ public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverte
}) })
.ToListAsync(); .ToListAsync();
res.ForEach(p => p.Content = mfmConverter.ToHtml(p.Content, [], null).Html); await res.Select(async p => p.Content = (await mfmConverter.ToHtmlAsync(p.Content, [], null)).Html).AwaitAllAsync();
return res; return res;
} }

View file

@ -1,7 +1,6 @@
using System.Net; using System.Net;
using System.Net.Mime; using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes; using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
@ -9,7 +8,6 @@ using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
@ -159,44 +157,4 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
return new object(); return new object();
} }
[Authenticate]
[HttpGet("/api/oauth_tokens.json")]
[ProducesResults(HttpStatusCode.OK)]
public async Task<List<PleromaOauthTokenEntity>> GetOauthTokens()
{
var user = HttpContext.GetUserOrFail();
var oauthTokens = await db.OauthTokens
.Where(p => p.User == user)
.Include(oauthToken => oauthToken.App)
.ToListAsync();
List<PleromaOauthTokenEntity> result = [];
foreach (var token in oauthTokens)
{
result.Add(new PleromaOauthTokenEntity()
{
Id = token.Id,
AppName = token.App.Name,
ValidUntil = token.CreatedAt + TimeSpan.FromDays(365 * 100)
});
}
return result;
}
[Authenticate]
[HttpDelete("/api/oauth_tokens/{id}")]
[ProducesResults(HttpStatusCode.Created)]
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
public async Task RevokeOauthTokenPleroma(string id)
{
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.Forbidden("You are not authorized to revoke this token");
db.Remove(token);
await db.SaveChangesAsync();
Response.StatusCode = 201;
}
} }

View file

@ -153,8 +153,8 @@ public class NoteRenderer(
{ {
if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible) if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible)
{ {
(content, inlineMedia) = mfmConverter.ToHtml(text ?? "", mentionedUsers, note.UserHost, quoteUri, (content, inlineMedia) = await mfmConverter.ToHtmlAsync(text ?? "", mentionedUsers, note.UserHost, quoteUri,
quoteInaccessible, replyInaccessible, media: inlineMedia); quoteInaccessible, replyInaccessible, media: inlineMedia);
attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url))); attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
} }
@ -258,7 +258,7 @@ public class NoteRenderer(
_ => MfmInlineMedia.MediaType.Other _ => MfmInlineMedia.MediaType.Other
}, p.RemoteUrl ?? p.Url, p.Description)).ToList(); }, p.RemoteUrl ?? p.Url, p.Description)).ToList();
(var content, inlineMedia) = mfmConverter.ToHtml(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia); (var content, inlineMedia) = await mfmConverter.ToHtmlAsync(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia);
files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url))); files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
var entry = new StatusEdit var entry = new StatusEdit

View file

@ -1,11 +1,9 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
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.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -15,8 +13,7 @@ public class UserRenderer(
IOptions<Config.InstanceSection> config, IOptions<Config.InstanceSection> config,
IOptionsSnapshot<Config.SecuritySection> security, IOptionsSnapshot<Config.SecuritySection> security,
MfmConverter mfmConverter, MfmConverter mfmConverter,
DatabaseContext db, DatabaseContext db
FlagService flags
) : IScopedService ) : IScopedService
{ {
private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png"; private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png";
@ -34,15 +31,18 @@ public class UserRenderer(
var profileEmoji = data?.Emoji.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([user]); var profileEmoji = data?.Emoji.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmojiAsync([user]);
var mentions = profile?.Mentions ?? []; var mentions = profile?.Mentions ?? [];
var fields = profile?.Fields var fields = profile != null
.Select(p => new Field ? await profile.Fields
{ .Select(async p => new Field
Name = p.Name, {
Value = (mfmConverter.ToHtml(p.Value, mentions, user.Host)).Html, Name = p.Name,
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value Value = (await mfmConverter.ToHtmlAsync(p.Value, mentions, user.Host)).Html,
? DateTime.Now.ToStringIso8601Like() VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
: null ? DateTime.Now.ToStringIso8601Like()
}); : null
})
.AwaitAllAsync()
: null;
var fieldsSource = source var fieldsSource = source
? profile?.Fields.Select(p => new Field { Name = p.Name, Value = p.Value }).ToList() ?? [] ? profile?.Fields.Select(p => new Field { Name = p.Name, Value = p.Value }).ToList() ?? []
@ -51,25 +51,6 @@ public class UserRenderer(
var avatarAlt = data?.AvatarAlt.GetValueOrDefault(user.Id); var avatarAlt = data?.AvatarAlt.GetValueOrDefault(user.Id);
var bannerAlt = data?.BannerAlt.GetValueOrDefault(user.Id); var bannerAlt = data?.BannerAlt.GetValueOrDefault(user.Id);
string? favicon;
string? softwareName;
string? softwareVersion;
if (user.IsRemoteUser)
{
var instInfo = await db.Instances
.Where(p => p.Host == user.Host)
.FirstOrDefaultAsync();
favicon = instInfo?.FaviconUrl;
softwareName = instInfo?.SoftwareName;
softwareVersion = instInfo?.SoftwareVersion;
}
else
{
favicon = config.Value.WebDomain + "/_content/Iceshrimp.Assets.Branding/favicon.png";
softwareName = "iceshrimp";
softwareVersion = config.Value.Version;
}
var res = new AccountEntity var res = new AccountEntity
{ {
Id = user.Id, Id = user.Id,
@ -84,7 +65,7 @@ public class UserRenderer(
FollowersCount = user.FollowersCount, FollowersCount = user.FollowersCount,
FollowingCount = user.FollowingCount, FollowingCount = user.FollowingCount,
StatusesCount = user.NotesCount, StatusesCount = user.NotesCount,
Note = mfmConverter.ToHtml(profile?.Description ?? "", mentions, user.Host).Html, Note = (await mfmConverter.ToHtmlAsync(profile?.Description ?? "", mentions, user.Host)).Html,
Url = profile?.Url ?? user.Uri ?? user.GetPublicUrl(config.Value), Url = profile?.Url ?? user.Uri ?? user.GetPublicUrl(config.Value),
Uri = user.Uri ?? user.GetPublicUri(config.Value), Uri = user.Uri ?? user.GetPublicUri(config.Value),
AvatarStaticUrl = user.GetAvatarUrl(config.Value), //TODO AvatarStaticUrl = user.GetAvatarUrl(config.Value), //TODO
@ -96,30 +77,7 @@ public class UserRenderer(
IsBot = user.IsBot, IsBot = user.IsBot,
IsDiscoverable = user.IsExplorable, IsDiscoverable = user.IsExplorable,
Fields = fields?.ToList() ?? [], Fields = fields?.ToList() ?? [],
Emoji = profileEmoji, Emoji = profileEmoji
Pleroma = flags?.IsPleroma.Value == true
? new PleromaUserExtensions
{
IsAdmin = user.IsAdmin,
IsModerator = user.IsModerator,
Favicon = favicon!
} : null,
Akkoma = flags?.IsPleroma.Value == true
? new AkkomaUserExtensions
{
Instance = new AkkomaInstanceEntity
{
Name = user.Host ?? config.Value.AccountDomain,
NodeInfo = new AkkomaNodeInfoEntity
{
Software = new AkkomaNodeInfoSoftwareEntity
{
Name = softwareName,
Version = softwareVersion
}
}
}
} : null
}; };
if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO

View file

@ -1,4 +1,3 @@
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Shared.Helpers; using Iceshrimp.Shared.Helpers;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
@ -6,32 +5,30 @@ namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
public class AccountEntity : IIdentifiable public class AccountEntity : IIdentifiable
{ {
[J("username")] public required string Username { get; set; } [J("username")] public required string Username { get; set; }
[J("acct")] public required string Acct { get; set; } [J("acct")] public required string Acct { get; set; }
[J("fqn")] public required string FullyQualifiedName { get; set; } [J("fqn")] public required string FullyQualifiedName { get; set; }
[J("display_name")] public required string DisplayName { get; set; } [J("display_name")] public required string DisplayName { get; set; }
[J("locked")] public required bool IsLocked { get; set; } [J("locked")] public required bool IsLocked { get; set; }
[J("created_at")] public required string CreatedAt { get; set; } [J("created_at")] public required string CreatedAt { get; set; }
[J("followers_count")] public required long FollowersCount { get; set; } [J("followers_count")] public required long FollowersCount { get; set; }
[J("following_count")] public required long FollowingCount { get; set; } [J("following_count")] public required long FollowingCount { get; set; }
[J("statuses_count")] public required long StatusesCount { get; set; } [J("statuses_count")] public required long StatusesCount { get; set; }
[J("note")] public required string Note { get; set; } [J("note")] public required string Note { get; set; }
[J("url")] public required string Url { get; set; } [J("url")] public required string Url { get; set; }
[J("uri")] public required string Uri { get; set; } [J("uri")] public required string Uri { get; set; }
[J("avatar")] public required string AvatarUrl { get; set; } [J("avatar")] public required string AvatarUrl { get; set; }
[J("avatar_static")] public required string AvatarStaticUrl { get; set; } [J("avatar_static")] public required string AvatarStaticUrl { get; set; }
[J("header")] public required string HeaderUrl { get; set; } [J("header")] public required string HeaderUrl { get; set; }
[J("header_static")] public required string HeaderStaticUrl { get; set; } [J("header_static")] public required string HeaderStaticUrl { get; set; }
[J("moved")] public required AccountEntity? MovedToAccount { get; set; } [J("moved")] public required AccountEntity? MovedToAccount { get; set; }
[J("bot")] public required bool IsBot { get; set; } [J("bot")] public required bool IsBot { get; set; }
[J("discoverable")] public required bool IsDiscoverable { get; set; } [J("discoverable")] public required bool IsDiscoverable { get; set; }
[J("fields")] public required List<Field> Fields { get; set; } [J("fields")] public required List<Field> Fields { get; set; }
[J("source")] public AccountSource? Source { get; set; } [J("source")] public AccountSource? Source { get; set; }
[J("emojis")] public required List<EmojiEntity> Emoji { get; set; } [J("emojis")] public required List<EmojiEntity> Emoji { get; set; }
[J("id")] public required string Id { get; set; } [J("id")] public required string Id { get; set; }
[J("last_status_at")] public string? LastStatusAt { get; set; } [J("last_status_at")] public string? LastStatusAt { get; set; }
[J("pleroma")] public required PleromaUserExtensions? Pleroma { get; set; }
[J("akkoma")] public required AkkomaUserExtensions? Akkoma { get; set; }
[J("avatar_description")] public required string AvatarDescription { get; set; } [J("avatar_description")] public required string AvatarDescription { get; set; }
[J("header_description")] public required string HeaderDescription { get; set; } [J("header_description")] public required string HeaderDescription { get; set; }

View file

@ -1,106 +0,0 @@
using System.Net;
using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas;
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Controllers.Web.Renderers;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using NoteRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.NoteRenderer;
using UserRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.UserRenderer;
namespace Iceshrimp.Backend.Controllers.Pleroma;
[MastodonApiController]
[Authenticate]
[EnableCors("mastodon")]
[EnableRateLimiting("sliding")]
[Produces(MediaTypeNames.Application.Json)]
public class AdminController(
DatabaseContext db,
ReportRenderer reportRenderer,
NoteRenderer noteRenderer,
UserRenderer userRenderer
) : ControllerBase
{
[HttpGet("/api/v1/pleroma/admin/reports")]
[Authenticate("admin:read:reports")]
[ProducesResults(HttpStatusCode.OK)]
public async Task<ReportsQuery> GetReports()
{
var user = HttpContext.GetUserOrFail();
var reports = await db.Reports
.IncludeCommonProperties()
.ToListAsync();
var rendered = await reportRenderer.RenderManyAsync(reports);
var reportsList = new List<Reports>();
foreach (var r in rendered)
{
var reActor = await db.Users
.IncludeCommonProperties()
.Where(p => p.Id == r.Reporter.Id)
.RenderAllForMastodonAsync(userRenderer, user);
var reTarget = await db.Users
.IncludeCommonProperties()
.Where(p => p.Id == r.TargetUser.Id)
.RenderAllForMastodonAsync(userRenderer, user);
foreach (var n in r.Notes)
{
var note = await db.Notes
.IncludeCommonProperties()
.Where(p => p.Id == n.Id)
.RenderAllForMastodonAsync(noteRenderer, user);
reportsList.Add(new Reports()
{
Account = reTarget.FirstOrDefault()!,
Actor = reActor.FirstOrDefault()!,
Id = r.Id,
CreatedAt = r.CreatedAt,
State = r.Resolved ? "resolved" : "open",
Content = r.Comment,
Statuses = note,
Notes = [] // unsupported
});
}
}
var resps = new ReportsQuery()
{
Total = reportsList.Count,
Reports = reportsList
};
return resps;
}
[HttpPatch("/api/v1/pleroma/admin/reports")]
[Authenticate("admin:read:reports")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
// ReSharper disable once AsyncVoidMethod
public async Task<ReportsQuery>? SetReportState(ReportsQuery query)
{
foreach (var list in query.Reports)
{
var report = await db.Reports.Where(p => p.Id == list.Id).FirstOrDefaultAsync()
?? throw GracefulException.NotFound("Report not found");
report.Resolved = list.State is "resolved" or "closed";
await db.SaveChangesAsync();
}
return query;
}
}

View file

@ -1,9 +0,0 @@
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
public class AkkomaInstanceEntity
{
[J("name")] public required string Name { get; set; }
[J("nodeinfo")] public required AkkomaNodeInfoEntity NodeInfo { get; set; }
}

View file

@ -1,8 +0,0 @@
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
public class AkkomaNodeInfoEntity
{
[J("software")] public required AkkomaNodeInfoSoftwareEntity Software { get; set; }
}

View file

@ -1,9 +0,0 @@
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
public class AkkomaNodeInfoSoftwareEntity
{
[J("name")] public required string? Name { get; set; }
[J("version")] public required string? Version { get; set; }
}

View file

@ -1,10 +0,0 @@
using Microsoft.EntityFrameworkCore;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
[Keyless]
public class AkkomaUserExtensions
{
[J("instance")] public required AkkomaInstanceEntity Instance { get; set; }
}

View file

@ -1,11 +0,0 @@
using System.Runtime.InteropServices.JavaScript;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
public class PleromaOauthTokenEntity
{
[J("id")] public required string Id { get; set; }
[J("valid_until")] public required DateTime ValidUntil { get; set; }
[J("app_name")] public required string? AppName { get; set; }
}

View file

@ -1,12 +0,0 @@
using Microsoft.EntityFrameworkCore;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
[Keyless]
public class PleromaUserExtensions
{
[J("is_admin")] public required bool IsAdmin { get; set; }
[J("is_moderator")] public required bool IsModerator { get; set; }
[J("favicon")] public required string Favicon { get; set; }
}

View file

@ -1,22 +0,0 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas;
public class ReportsQuery
{
[J("total")] public int Total { get; set; }
[J("reports")] public required List<Reports> Reports { get; set; }
}
public class Reports
{
[J("account")] public AccountEntity? Account { get; set; }
[J("actor")] public AccountEntity? Actor { get; set; }
[J("id")] public required string Id { get; set; }
[J("created_at")] public DateTime? CreatedAt { get; set; }
[J("state")] public required string State { get; set; }
[J("content")] public string? Content { get; set; }
[J("statuses")] public IEnumerable<StatusEntity>? Statuses { get; set; }
[J("notes")] public string[]? Notes { get; set; }
}

View file

@ -182,7 +182,7 @@ public class NoteRenderer(
To = to, To = to,
Tags = tags, Tags = tags,
Attachments = attachments, Attachments = attachments,
Content = text != null ? mfmConverter.ToHtml(text, mentions, note.UserHost, media: inlineMedia).Html : null, Content = text != null ? (await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost, media: inlineMedia)).Html : null,
Summary = note.Cw, Summary = note.Cw,
Source = rawText != null Source = rawText != null
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" } ? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
@ -214,7 +214,7 @@ public class NoteRenderer(
To = to, To = to,
Tags = tags, Tags = tags,
Attachments = attachments, Attachments = attachments,
Content = text != null ? mfmConverter.ToHtml(text, mentions, note.UserHost, media: inlineMedia).Html : null, Content = text != null ? (await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost, media: inlineMedia)).Html : null,
Summary = note.Cw, Summary = note.Cw,
Source = rawText != null Source = rawText != null
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" } ? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }

View file

@ -5,7 +5,6 @@ using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.MfmSharp;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -85,7 +84,7 @@ public class UserRenderer(
.ToList(); .ToList();
var summary = profile?.Description != null var summary = profile?.Description != null
? mfmConverter.ToHtml(profile.Description, profile.Mentions, user.Host).Html ? (await mfmConverter.ToHtmlAsync(profile.Description, profile.Mentions, user.Host)).Html
: null; : null;
var pronouns = profile?.Pronouns != null ? new LDLocalizedString { Values = profile.Pronouns! } : null; var pronouns = profile?.Pronouns != null ? new LDLocalizedString { Values = profile.Pronouns! } : null;

View file

@ -2,14 +2,13 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using AngleSharp.Html.Dom; using AngleSharp.Html.Parser;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using Iceshrimp.MfmSharp; using Iceshrimp.MfmSharp;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.MfmSharp.Helpers;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser; using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser;
using HtmlParser = AngleSharp.Html.Parser.HtmlParser; using HtmlParser = AngleSharp.Html.Parser.HtmlParser;
@ -48,15 +47,7 @@ public class MfmConverter(
FlagService flags FlagService flags
) : ISingletonService ) : ISingletonService
{ {
private static readonly HtmlParser Parser = new(); public static async Task<HtmlMfmData> FromHtmlAsync(
private static readonly Lazy<IHtmlDocument> OwnerDocument =
new(() => Parser.ParseDocument(ReadOnlyMemory<char>.Empty));
private static IElement CreateElement(string name) => OwnerDocument.Value.CreateElement(name);
private static IText CreateTextNode(string data) => OwnerDocument.Value.CreateTextNode(data);
public static HtmlMfmData FromHtml(
string? html, List<Note.MentionedUser>? mentions = null, List<string>? hashtags = null string? html, List<Note.MentionedUser>? mentions = null, List<string>? hashtags = null
) )
{ {
@ -73,7 +64,7 @@ public class MfmConverter(
// Ensure compatibility with AP servers that send CRLF or CR instead of LF-style newlines // Ensure compatibility with AP servers that send CRLF or CR instead of LF-style newlines
html = html.ReplaceLineEndings("\n"); html = html.ReplaceLineEndings("\n");
var dom = Parser.ParseDocument(html); var dom = await new HtmlParser().ParseDocumentAsync(html);
if (dom.Body == null) return new HtmlMfmData("", media); if (dom.Body == null) return new HtmlMfmData("", media);
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -82,7 +73,7 @@ public class MfmConverter(
return new HtmlMfmData(sb.ToString().Trim(), media); return new HtmlMfmData(sb.ToString().Trim(), media);
} }
public static List<string> ExtractMentionsFromHtml(string? html) public static async Task<List<string>> ExtractMentionsFromHtmlAsync(string? html)
{ {
if (html == null) return []; if (html == null) return [];
@ -90,7 +81,7 @@ public class MfmConverter(
var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase); var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase);
html = regex.Replace(html, "\n"); html = regex.Replace(html, "\n");
var dom = Parser.ParseDocument(html); var dom = await new HtmlParser().ParseDocumentAsync(html);
if (dom.Body == null) return []; if (dom.Body == null) return [];
var parser = new HtmlMentionsExtractor(); var parser = new HtmlMentionsExtractor();
@ -100,26 +91,28 @@ public class MfmConverter(
return parser.Mentions; return parser.Mentions;
} }
public MfmHtmlData ToHtml( public async Task<MfmHtmlData> ToHtmlAsync(
IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null, IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
) )
{ {
var element = CreateElement(rootElement); var context = BrowsingContext.New();
var document = await context.OpenNewAsync();
var element = document.CreateElement(rootElement);
var hasContent = nodes.Length > 0; var hasContent = nodes.Length > 0;
if (replyInaccessible) if (replyInaccessible)
{ {
var wrapper = CreateElement("span"); var wrapper = document.CreateElement("span");
var re = CreateElement("span"); var re = document.CreateElement("span");
re.TextContent = "RE: \ud83d\udd12"; // lock emoji re.TextContent = "RE: \ud83d\udd12"; // lock emoji
wrapper.AppendChild(re); wrapper.AppendChild(re);
if (hasContent) if (hasContent)
{ {
wrapper.AppendChild(CreateElement("br")); wrapper.AppendChild(document.CreateElement("br"));
wrapper.AppendChild(CreateElement("br")); wrapper.AppendChild(document.CreateElement("br"));
} }
element.AppendChild(wrapper); element.AppendChild(wrapper);
@ -127,23 +120,23 @@ public class MfmConverter(
var usedMedia = new List<MfmInlineMedia>(); var usedMedia = new List<MfmInlineMedia>();
foreach (var node in nodes) foreach (var node in nodes)
element.AppendNodes(FromMfmNode(node, mentions, host, usedMedia, emoji, media)); element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media));
if (quoteUri != null) if (quoteUri != null)
{ {
var a = CreateElement("a"); var a = document.CreateElement("a");
a.SetAttribute("href", quoteUri); a.SetAttribute("href", quoteUri);
a.TextContent = quoteUri.StartsWith("https://") ? quoteUri[8..] : quoteUri[7..]; a.TextContent = quoteUri.StartsWith("https://") ? quoteUri[8..] : quoteUri[7..];
var quote = CreateElement("span"); var quote = document.CreateElement("span");
quote.ClassList.Add("quote-inline"); quote.ClassList.Add("quote-inline");
if (hasContent) if (hasContent)
{ {
quote.AppendChild(CreateElement("br")); quote.AppendChild(document.CreateElement("br"));
quote.AppendChild(CreateElement("br")); quote.AppendChild(document.CreateElement("br"));
} }
var re = CreateElement("span"); var re = document.CreateElement("span");
re.TextContent = "RE: "; re.TextContent = "RE: ";
quote.AppendChild(re); quote.AppendChild(re);
quote.AppendChild(a); quote.AppendChild(a);
@ -151,47 +144,39 @@ public class MfmConverter(
} }
else if (quoteInaccessible) else if (quoteInaccessible)
{ {
var wrapper = CreateElement("span"); var wrapper = document.CreateElement("span");
var re = CreateElement("span"); var re = document.CreateElement("span");
re.TextContent = "RE: \ud83d\udd12"; // lock emoji re.TextContent = "RE: \ud83d\udd12"; // lock emoji
if (hasContent) if (hasContent)
{ {
wrapper.AppendChild(CreateElement("br")); wrapper.AppendChild(document.CreateElement("br"));
wrapper.AppendChild(CreateElement("br")); wrapper.AppendChild(document.CreateElement("br"));
} }
wrapper.AppendChild(re); wrapper.AppendChild(re);
element.AppendChild(wrapper); element.AppendChild(wrapper);
} }
return new MfmHtmlData(element.ToHtml(), usedMedia); await using var sw = new StringWriter();
await element.ToHtmlAsync(sw);
return new MfmHtmlData(sw.ToString(), usedMedia);
} }
public MfmHtmlData ToHtml( public async Task<MfmHtmlData> ToHtmlAsync(
string mfm, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null, string mfm, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
) )
{ {
var nodes = MfmParser.Parse(mfm); var nodes = MfmParser.Parse(mfm);
return ToHtml(nodes, mentions, host, quoteUri, quoteInaccessible, replyInaccessible, rootElement, emoji, media); return await ToHtmlAsync(nodes, mentions, host, quoteUri, quoteInaccessible,
} replyInaccessible, rootElement, emoji, media);
public string ProfileFieldToHtml(MfmUrlNode node)
{
var parsed = FromMfmNode(node, [], null, []);
if (parsed is not IHtmlAnchorElement el)
return parsed.ToHtml();
el.SetAttribute("rel", "me nofollow noopener");
el.SetAttribute("target", "_blank");
return el.ToHtml();
} }
private INode FromMfmNode( private INode FromMfmNode(
IMfmNode node, List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia, IDocument document, IMfmNode node, List<Note.MentionedUser> mentions, string? host,
List<MfmInlineMedia> usedMedia,
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
) )
{ {
@ -209,7 +194,7 @@ public class MfmConverter(
if (!flags.SupportsInlineMedia.Value || current.Type == MfmInlineMedia.MediaType.Other) if (!flags.SupportsInlineMedia.Value || current.Type == MfmInlineMedia.MediaType.Other)
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", current.Src); el.SetAttribute("href", current.Src);
if (current.Type == MfmInlineMedia.MediaType.Other) if (current.Type == MfmInlineMedia.MediaType.Other)
@ -236,7 +221,7 @@ public class MfmConverter(
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
}; };
var el = CreateElement(nodeName); var el = document.CreateElement(nodeName);
el.SetAttribute("src", current.Src); el.SetAttribute("src", current.Src);
el.SetAttribute("alt", current.Alt); el.SetAttribute("alt", current.Alt);
return el; return el;
@ -245,16 +230,16 @@ public class MfmConverter(
} }
{ {
var el = CreateInlineFormattingElement("i"); var el = CreateInlineFormattingElement(document, "i");
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
return el; return el;
} }
} }
case MfmFnNode { Name: "unixtime" } fn: case MfmFnNode { Name: "unixtime" } fn:
{ {
var el = CreateInlineFormattingElement("i"); var el = CreateInlineFormattingElement(document, "i");
if (fn.Children.Length != 1 || fn.Children.FirstOrDefault() is not MfmTextNode textNode) if (fn.Children.Length != 1 || fn.Children.FirstOrDefault() is not MfmTextNode textNode)
return Fallback(); return Fallback();
@ -269,55 +254,55 @@ public class MfmConverter(
IElement Fallback() IElement Fallback()
{ {
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
return el; return el;
} }
} }
case MfmBoldNode: case MfmBoldNode:
{ {
var el = CreateInlineFormattingElement("b"); var el = CreateInlineFormattingElement(document, "b");
AddHtmlMarkup(el, "**"); AddHtmlMarkup(document, el, "**");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "**"); AddHtmlMarkup(document, el, "**");
return el; return el;
} }
case MfmSmallNode: case MfmSmallNode:
{ {
var el = CreateElement("small"); var el = document.CreateElement("small");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
return el; return el;
} }
case MfmStrikeNode: case MfmStrikeNode:
{ {
var el = CreateInlineFormattingElement("del"); var el = CreateInlineFormattingElement(document, "del");
AddHtmlMarkup(el, "~~"); AddHtmlMarkup(document, el, "~~");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "~~"); AddHtmlMarkup(document, el, "~~");
return el; return el;
} }
case MfmItalicNode: case MfmItalicNode:
case MfmFnNode: case MfmFnNode:
{ {
var el = CreateInlineFormattingElement("i"); var el = CreateInlineFormattingElement(document, "i");
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*"); AddHtmlMarkup(document, el, "*");
return el; return el;
} }
case MfmCodeBlockNode codeBlockNode: case MfmCodeBlockNode codeBlockNode:
{ {
var el = CreateInlineFormattingElement("pre"); var el = CreateInlineFormattingElement(document, "pre");
var inner = CreateInlineFormattingElement("code"); var inner = CreateInlineFormattingElement(document, "code");
inner.TextContent = codeBlockNode.Code; inner.TextContent = codeBlockNode.Code;
el.AppendNodes(inner); el.AppendNodes(inner);
return el; return el;
} }
case MfmCenterNode: case MfmCenterNode:
{ {
var el = CreateElement("div"); var el = document.CreateElement("div");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
return el; return el;
} }
case MfmEmojiCodeNode emojiCodeNode: case MfmEmojiCodeNode emojiCodeNode:
@ -325,8 +310,8 @@ public class MfmConverter(
var punyHost = host?.ToPunycodeLower(); var punyHost = host?.ToPunycodeLower();
if (emoji?.FirstOrDefault(p => p.Name == emojiCodeNode.Name && p.Host == punyHost) is { } hit) if (emoji?.FirstOrDefault(p => p.Name == emojiCodeNode.Name && p.Host == punyHost) is { } hit)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var inner = CreateElement("img"); var inner = document.CreateElement("img");
inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit)); inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit));
inner.SetAttribute("alt", hit.Name); inner.SetAttribute("alt", hit.Name);
el.AppendChild(inner); el.AppendChild(inner);
@ -334,11 +319,11 @@ public class MfmConverter(
return el; return el;
} }
return CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B"); return document.CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B");
} }
case MfmHashtagNode hashtagNode: case MfmHashtagNode hashtagNode:
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}"); el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}");
el.TextContent = $"#{hashtagNode.Hashtag}"; el.TextContent = $"#{hashtagNode.Hashtag}";
el.SetAttribute("rel", "tag"); el.SetAttribute("rel", "tag");
@ -347,32 +332,32 @@ public class MfmConverter(
} }
case MfmInlineCodeNode inlineCodeNode: case MfmInlineCodeNode inlineCodeNode:
{ {
var el = CreateInlineFormattingElement("code"); var el = CreateInlineFormattingElement(document, "code");
el.TextContent = inlineCodeNode.Code; el.TextContent = inlineCodeNode.Code;
return el; return el;
} }
case MfmInlineMathNode inlineMathNode: case MfmInlineMathNode inlineMathNode:
{ {
var el = CreateInlineFormattingElement("code"); var el = CreateInlineFormattingElement(document, "code");
el.TextContent = inlineMathNode.Formula; el.TextContent = inlineMathNode.Formula;
return el; return el;
} }
case MfmMathBlockNode mathBlockNode: case MfmMathBlockNode mathBlockNode:
{ {
var el = CreateInlineFormattingElement("code"); var el = CreateInlineFormattingElement(document, "code");
el.TextContent = mathBlockNode.Formula; el.TextContent = mathBlockNode.Formula;
return el; return el;
} }
case MfmLinkNode linkNode: case MfmLinkNode linkNode:
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", linkNode.Url); el.SetAttribute("href", linkNode.Url);
el.TextContent = linkNode.Text; el.TextContent = linkNode.Text;
return el; return el;
} }
case MfmMentionNode mentionNode: case MfmMentionNode mentionNode:
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
// Fall back to object host, as localpart-only mentions are relative to the instance the note originated from // Fall back to object host, as localpart-only mentions are relative to the instance the note originated from
var finalHost = mentionNode.Host ?? host ?? config.Value.AccountDomain; var finalHost = mentionNode.Host ?? host ?? config.Value.AccountDomain;
@ -393,10 +378,10 @@ public class MfmConverter(
{ {
el.ClassList.Add("h-card"); el.ClassList.Add("h-card");
el.SetAttribute("translate", "no"); el.SetAttribute("translate", "no");
var a = CreateElement("a"); var a = document.CreateElement("a");
a.ClassList.Add("u-url", "mention"); a.ClassList.Add("u-url", "mention");
a.SetAttribute("href", mention.Url ?? mention.Uri); a.SetAttribute("href", mention.Url ?? mention.Uri);
var span = CreateElement("span"); var span = document.CreateElement("span");
span.TextContent = $"@{mention.Username}"; span.TextContent = $"@{mention.Username}";
a.AppendChild(span); a.AppendChild(span);
el.AppendChild(a); el.AppendChild(a);
@ -406,25 +391,25 @@ public class MfmConverter(
} }
case MfmQuoteNode: case MfmQuoteNode:
{ {
var el = CreateInlineFormattingElement("blockquote"); var el = CreateInlineFormattingElement(document, "blockquote");
AddHtmlMarkup(el, "> "); AddHtmlMarkup(document, el, "> ");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkupTag(el, "br"); AddHtmlMarkupTag(document, el, "br");
AddHtmlMarkupTag(el, "br"); AddHtmlMarkupTag(document, el, "br");
return el; return el;
} }
case MfmTextNode textNode: case MfmTextNode textNode:
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var nodes = textNode.Text.Split("\r\n") var nodes = textNode.Text.Split("\r\n")
.SelectMany(p => p.Split('\r')) .SelectMany(p => p.Split('\r'))
.SelectMany(p => p.Split('\n')) .SelectMany(p => p.Split('\n'))
.Select(CreateTextNode); .Select(document.CreateTextNode);
foreach (var htmlNode in nodes) foreach (var htmlNode in nodes)
{ {
el.AppendNodes(htmlNode); el.AppendNodes(htmlNode);
el.AppendNodes(CreateElement("br")); el.AppendNodes(document.CreateElement("br"));
} }
if (el.LastChild != null) if (el.LastChild != null)
@ -433,25 +418,17 @@ public class MfmConverter(
} }
case MfmUrlNode urlNode: case MfmUrlNode urlNode:
{ {
if ( var el = document.CreateElement("a");
!Uri.TryCreate(urlNode.Url, UriKind.Absolute, out var uri)
|| uri is not { Scheme: "http" or "https" }
)
{
var fallbackEl = CreateElement("span");
fallbackEl.TextContent = urlNode.Url;
return fallbackEl;
}
var el = CreateElement("a");
el.SetAttribute("href", urlNode.Url); el.SetAttribute("href", urlNode.Url);
el.TextContent = uri.ToMfmDisplayString(); var prefix = urlNode.Url.StartsWith("https://") ? "https://" : "http://";
var length = prefix.Length;
el.TextContent = urlNode.Url[length..];
return el; return el;
} }
case MfmPlainNode: case MfmPlainNode:
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
AppendChildren(el, node, mentions, host, usedMedia); AppendChildren(el, document, node, mentions, host, usedMedia);
return el; return el;
} }
default: default:
@ -462,32 +439,32 @@ public class MfmConverter(
} }
private void AppendChildren( private void AppendChildren(
INode element, IMfmNode parent, INode element, IDocument document, IMfmNode parent,
List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia, List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
) )
{ {
foreach (var node in parent.Children) foreach (var node in parent.Children)
element.AppendNodes(FromMfmNode(node, mentions, host, usedMedia, emoji, media)); element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media));
} }
private IElement CreateInlineFormattingElement(string name) private IElement CreateInlineFormattingElement(IDocument document, string name)
{ {
return CreateElement(flags.SupportsHtmlFormatting.Value ? name : "span"); return document.CreateElement(flags.SupportsHtmlFormatting.Value ? name : "span");
} }
private void AddHtmlMarkup(IElement node, string chars) private void AddHtmlMarkup(IDocument document, IElement node, string chars)
{ {
if (flags.SupportsHtmlFormatting.Value) return; if (flags.SupportsHtmlFormatting.Value) return;
var el = CreateElement("span"); var el = document.CreateElement("span");
el.AppendChild(CreateTextNode(chars)); el.AppendChild(document.CreateTextNode(chars));
node.AppendChild(el); node.AppendChild(el);
} }
private void AddHtmlMarkupTag(IElement node, string tag) private void AddHtmlMarkupTag(IDocument document, IElement node, string tag)
{ {
if (flags.SupportsHtmlFormatting.Value) return; if (flags.SupportsHtmlFormatting.Value) return;
var el = CreateElement(tag); var el = document.CreateElement(tag);
node.AppendChild(el); node.AppendChild(el);
} }
} }

View file

@ -1028,7 +1028,7 @@ public class NoteService(
.ToList() .ToList()
?? []; ?? [];
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags); (text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
} }
var cw = note.Summary; var cw = note.Summary;
@ -1128,7 +1128,7 @@ public class NoteService(
.ToList() .ToList()
?? []; ?? [];
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags); (text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
} }
var cw = note.Summary; var cw = note.Summary;

View file

@ -27,10 +27,11 @@ public class UserProfileMentionsResolver(
?? []; ?? [];
if (fields is not { Count: > 0 } && (actor.MkSummary ?? actor.Summary) == null) return ([], []); if (fields is not { Count: > 0 } && (actor.MkSummary ?? actor.Summary) == null) return ([], []);
var parsedFields = fields.SelectMany<ASField, string?>(p => [p.Name, p.Value]) var parsedFields = await fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
.Select(MfmConverter.ExtractMentionsFromHtml); .Select(async p => await MfmConverter.ExtractMentionsFromHtmlAsync(p))
.AwaitAllAsync();
var parsedBio = actor.MkSummary == null ? MfmConverter.ExtractMentionsFromHtml(actor.Summary) : []; var parsedBio = actor.MkSummary == null ? await MfmConverter.ExtractMentionsFromHtmlAsync(actor.Summary) : [];
var userUris = parsedFields.Prepend(parsedBio).SelectMany(p => p).ToList(); var userUris = parsedFields.Prepend(parsedBio).SelectMany(p => p).ToList();
var mentionNodes = new List<MfmMentionNode>(); var mentionNodes = new List<MfmMentionNode>();

View file

@ -148,12 +148,16 @@ public class UserService(
var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType<ASEmoji>().ToList(), host); var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType<ASEmoji>().ToList(), host);
var fields = actor.Attachments?.OfType<ASField>() var fields = actor.Attachments != null
.Where(p => p is { Name: not null, Value: not null }) ? await actor.Attachments
.Select(p => new UserProfile.Field .OfType<ASField>()
{ .Where(p => p is { Name: not null, Value: not null })
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm .Select(async p => new UserProfile.Field
}); {
Name = p.Name!, Value = (await MfmConverter.FromHtmlAsync(p.Value)).Mfm
})
.AwaitAllAsync()
: null;
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? ""); var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
@ -166,7 +170,7 @@ public class UserService(
.ToList() .ToList()
?? []; ?? [];
bio = MfmConverter.FromHtml(actor.Summary, hashtags: asHashtags).Mfm; bio = (await MfmConverter.FromHtmlAsync(actor.Summary, hashtags: asHashtags)).Mfm;
} }
var tags = ResolveHashtags(MfmParser.Parse(bio), actor); var tags = ResolveHashtags(MfmParser.Parse(bio), actor);
@ -314,12 +318,16 @@ public class UserService(
?? throw new ?? throw new
Exception("User host must not be null at this stage")); Exception("User host must not be null at this stage"));
var fields = actor.Attachments?.OfType<ASField>() var fields = actor.Attachments != null
.Where(p => p is { Name: not null, Value: not null }) ? await actor.Attachments
.Select(p => new UserProfile.Field .OfType<ASField>()
{ .Where(p => p is { Name: not null, Value: not null })
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm .Select(async p => new UserProfile.Field
}); {
Name = p.Name!, Value = (await MfmConverter.FromHtmlAsync(p.Value)).Mfm
})
.AwaitAllAsync()
: null;
var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? ""); var pronouns = actor.Pronouns?.Values.ToDictionary(p => p.Key, p => p.Value ?? "");
@ -340,7 +348,7 @@ public class UserService(
.ToList() .ToList()
?? []; ?? [];
user.UserProfile.Description = MfmConverter.FromHtml(actor.Summary, hashtags: asHashtags).Mfm; user.UserProfile.Description = (await MfmConverter.FromHtmlAsync(actor.Summary, hashtags: asHashtags)).Mfm;
} }
//user.UserProfile.Birthday = TODO; //user.UserProfile.Birthday = TODO;
@ -1124,17 +1132,21 @@ public class UserService(
{ {
var (mentions, splitDomainMapping) = var (mentions, splitDomainMapping) =
await bgMentionsResolver.ResolveMentionsAsync(actor, bgUser.Host); await bgMentionsResolver.ResolveMentionsAsync(actor, bgUser.Host);
var fields = actor.Attachments?.OfType<ASField>() var fields = actor.Attachments != null
.Where(p => p is { Name: not null, Value: not null }) ? await actor.Attachments
.Select(p => new UserProfile.Field .OfType<ASField>()
{ .Where(p => p is { Name: not null, Value: not null })
Name = p.Name!, .Select(async p => new UserProfile.Field
Value = MfmConverter.FromHtml(p.Value, mentions).Mfm {
}); Name = p.Name!,
Value = (await MfmConverter.FromHtmlAsync(p.Value, mentions)).Mfm
})
.AwaitAllAsync()
: null;
var description = actor.MkSummary != null var description = actor.MkSummary != null
? mentionsResolver.ResolveMentions(actor.MkSummary, bgUser.Host, mentions, splitDomainMapping) ? mentionsResolver.ResolveMentions(actor.MkSummary, bgUser.Host, mentions, splitDomainMapping)
: MfmConverter.FromHtml(actor.Summary, mentions).Mfm; : (await MfmConverter.FromHtmlAsync(actor.Summary, mentions)).Mfm;
bgUser.UserProfile.Mentions = mentions; bgUser.UserProfile.Mentions = mentions;
bgUser.UserProfile.Fields = fields?.ToArray() ?? []; bgUser.UserProfile.Fields = fields?.ToArray() ?? [];

View file

@ -56,7 +56,7 @@
<PackageReference Include="Ulid" Version="1.3.4" /> <PackageReference Include="Ulid" Version="1.3.4" />
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" /> <PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" /> <PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.20" /> <PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.18" />
<PackageReference Include="Iceshrimp.Utils.Common" Version="1.2.1" /> <PackageReference Include="Iceshrimp.Utils.Common" Version="1.2.1" />
<PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" /> <PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" />
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" /> <PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />

View file

@ -6,7 +6,6 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"externalUrlConfiguration": true, "externalUrlConfiguration": true,
"commandLineArgs": "--migrate-and-start",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View file

@ -7,8 +7,8 @@ ListenHost = localhost
;;ListenSocketPerms = 660 ;;ListenSocketPerms = 660
;; Caution: changing these settings after initial setup *will* break federation ;; Caution: changing these settings after initial setup *will* break federation
WebDomain = localhost:3000 WebDomain = shrimp.example.org
AccountDomain = localhost:3000 AccountDomain = example.org
;; End of problematic settings block ;; End of problematic settings block
;; Additional domains this instance allows API access from, separated by commas. ;; Additional domains this instance allows API access from, separated by commas.
@ -184,7 +184,7 @@ ProxyRemoteMedia = true
[Storage:Local] [Storage:Local]
;; Path where media is stored at. Must be writable for the service user. ;; Path where media is stored at. Must be writable for the service user.
Path = /home/luke/Documents/shrimp Path = /path/to/media/location
[Storage:ObjectStorage] [Storage:ObjectStorage]
;;Endpoint = endpoint.example.org ;;Endpoint = endpoint.example.org

View file

@ -15,7 +15,7 @@
if (Text != null) if (Text != null)
{ {
var instance = await MetadataService.Instance.Value; var instance = await MetadataService.Instance.Value;
TextBody = MfmRenderer.RenderString(Text, Emoji, instance.AccountDomain, Simple); TextBody = await MfmRenderer.RenderStringAsync(Text, Emoji, instance.AccountDomain, Simple);
} }
} }
@ -24,7 +24,7 @@
if (Text != null) if (Text != null)
{ {
var instance = await MetadataService.Instance.Value; var instance = await MetadataService.Instance.Value;
TextBody = MfmRenderer.RenderString(Text, Emoji, instance.AccountDomain, Simple); TextBody = await MfmRenderer.RenderStringAsync(Text, Emoji, instance.AccountDomain, Simple);
} }
} }
} }

View file

@ -45,7 +45,7 @@
} }
.truncate-btn { .truncate-btn {
z-index: 5; z-index: 1;
width: 100%; width: 100%;
margin-top: 0.5em; margin-top: 0.5em;
background-color: var(--background-color); background-color: var(--background-color);

View file

@ -86,54 +86,54 @@
</button> </button>
<button @ref="MenuButton" class="btn" @onclick="ToggleMenu" @onclick:stopPropagation="true" aria-label="more"> <button @ref="MenuButton" class="btn" @onclick="ToggleMenu" @onclick:stopPropagation="true" aria-label="more">
<Icon Name="Icons.DotsThreeOutline" Size="1.3em"/> <Icon Name="Icons.DotsThreeOutline" Size="1.3em"/>
<Menu @ref="ContextMenu">
@if (Note.User.Id != Session.Current?.Id)
{
<MenuElement Icon="Icons.Tooth" OnSelect="Bite">
<Text>@Loc["Bite"]</Text>
</MenuElement>
}
@if (Note.User.Host != null)
{
<MenuElement Icon="Icons.ArrowsClockwise" OnSelect="RefetchNote">
<Text>@Loc["Refetch"]</Text>
</MenuElement>
}
<MenuElement Icon="Icons.SpeakerX" OnSelect="Mute">
<Text>@Loc["Mute thread"]</Text>
</MenuElement>
<hr class="rule"/>
<MenuElement Icon="Icons.ArrowSquareOut" OnSelect="OpenOriginal">
<Text>@Loc["Open original page"]</Text>
</MenuElement>
<MenuElement Icon="Icons.Share" OnSelect="CopyLink">
<Text>@Loc["Copy link"]</Text>
</MenuElement>
@if (Note.User.Host != null)
{
<MenuElement Icon="Icons.ShareNetwork" OnSelect="CopyLinkRemote">
<Text>@Loc["Copy link (remote)"]</Text>
</MenuElement>
}
@if (!string.IsNullOrWhiteSpace(Note.Text))
{
<MenuElement Icon="Icons.Copy" OnSelect="CopyContents">
<Text>@Loc["Copy contents"]</Text>
</MenuElement>
}
@if (Note.User.Id == Session.Current?.Id)
{
<hr class="rule"/>
<MenuElement Icon="Icons.Eraser" OnSelect="Redraft" Danger>
<Text>@Loc["Delete & redraft"]</Text>
</MenuElement>
<MenuElement Icon="Icons.Trash" OnSelect="Delete" Danger>
<Text>@Loc["Delete"]</Text>
</MenuElement>
}
<ClosingBackdrop OnClose="ContextMenu.Close"></ClosingBackdrop>
</Menu>
</button> </button>
<Menu @ref="ContextMenu">
@if (Note.User.Id != Session.Current?.Id)
{
<MenuElement Icon="Icons.Tooth" OnSelect="Bite">
<Text>@Loc["Bite"]</Text>
</MenuElement>
}
@if (Note.User.Host != null)
{
<MenuElement Icon="Icons.ArrowsClockwise" OnSelect="RefetchNote">
<Text>@Loc["Refetch"]</Text>
</MenuElement>
}
<MenuElement Icon="Icons.SpeakerX" OnSelect="Mute">
<Text>@Loc["Mute thread"]</Text>
</MenuElement>
<hr class="rule"/>
<MenuElement Icon="Icons.ArrowSquareOut" OnSelect="OpenOriginal">
<Text>@Loc["Open original page"]</Text>
</MenuElement>
<MenuElement Icon="Icons.Share" OnSelect="CopyLink">
<Text>@Loc["Copy link"]</Text>
</MenuElement>
@if (Note.User.Host != null)
{
<MenuElement Icon="Icons.ShareNetwork" OnSelect="CopyLinkRemote">
<Text>@Loc["Copy link (remote)"]</Text>
</MenuElement>
}
@if (!string.IsNullOrWhiteSpace(Note.Text))
{
<MenuElement Icon="Icons.Copy" OnSelect="CopyContents">
<Text>@Loc["Copy contents"]</Text>
</MenuElement>
}
@if (Note.User.Id == Session.Current?.Id)
{
<hr class="rule"/>
<MenuElement Icon="Icons.Eraser" OnSelect="Redraft" Danger>
<Text>@Loc["Delete & redraft"]</Text>
</MenuElement>
<MenuElement Icon="Icons.Trash" OnSelect="Delete" Danger>
<Text>@Loc["Delete"]</Text>
</MenuElement>
}
<ClosingBackdrop OnClose="ContextMenu.Close"></ClosingBackdrop>
</Menu>
</div> </div>
@code { @code {

View file

@ -10,11 +10,19 @@
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
::deep {
.reactions .reaction {
position: relative;
z-index: +1;
}
}
.indent { .indent {
padding-left: 0.75em; padding-left: 0.75em;
} }
.btn { .btn {
position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-width: 2.5em; min-width: 2.5em;
@ -23,6 +31,7 @@
background-color: var(--foreground-color); background-color: var(--foreground-color);
border: 0.1rem solid var(--foreground-color); border: 0.1rem solid var(--foreground-color);
width: fit-content; width: fit-content;
z-index: +1;
} }
.btn:hover { .btn:hover {

View file

@ -1,8 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using AngleSharp.Text; using AngleSharp.Text;
using Iceshrimp.MfmSharp; using Iceshrimp.MfmSharp;
using Iceshrimp.Shared.Schemas.Web; using Iceshrimp.Shared.Schemas.Web;
@ -12,37 +10,34 @@ namespace Iceshrimp.Frontend.Core.Miscellaneous;
public static partial class MfmRenderer public static partial class MfmRenderer
{ {
public static MarkupString RenderString( public static async Task<MarkupString> RenderStringAsync(
string text, List<EmojiResponse> emoji, string accountDomain, bool simple = false string text, List<EmojiResponse> emoji, string accountDomain, bool simple = false
) )
{ {
var res = MfmParser.Parse(text, simple); var res = MfmParser.Parse(text, simple);
var renderedMfm = RenderMultipleNodes(res, emoji, accountDomain, simple); var context = BrowsingContext.New();
var document = await context.OpenNewAsync();
var renderedMfm = RenderMultipleNodes(res, document, emoji, accountDomain, simple);
var html = renderedMfm.ToHtml(); var html = renderedMfm.ToHtml();
return new MarkupString(html); return new MarkupString(html);
} }
private static readonly Lazy<IHtmlDocument> OwnerDocument =
new(() => new HtmlParser().ParseDocument(ReadOnlyMemory<char>.Empty));
private static IElement CreateElement(string name) => OwnerDocument.Value.CreateElement(name);
private static INode RenderMultipleNodes( private static INode RenderMultipleNodes(
IEnumerable<IMfmNode> nodes, List<EmojiResponse> emoji, string accountDomain, bool simple IEnumerable<IMfmNode> nodes, IDocument document, List<EmojiResponse> emoji, string accountDomain, bool simple
) )
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.SetAttribute("mfm", "mfm"); el.SetAttribute("mfm", "mfm");
el.ClassName = "mfm"; el.ClassName = "mfm";
foreach (var node in nodes) foreach (var node in nodes)
{ {
try try
{ {
el.AppendNodes(RenderNode(node, emoji, accountDomain, simple)); el.AppendNodes(RenderNode(node, document, emoji, accountDomain, simple));
} }
catch (NotImplementedException e) catch (NotImplementedException e)
{ {
var fallback = CreateElement("span"); var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]"; fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
el.AppendNodes(fallback); el.AppendNodes(fallback);
} }
@ -52,32 +47,32 @@ public static partial class MfmRenderer
} }
private static INode RenderNode( private static INode RenderNode(
IMfmNode node, List<EmojiResponse> emoji, string accountDomain, bool simple IMfmNode node, IDocument document, List<EmojiResponse> emoji, string accountDomain, bool simple
) )
{ {
// Hard wrap makes this impossible to read // Hard wrap makes this impossible to read
// @formatter:off // @formatter:off
var rendered = node switch var rendered = node switch
{ {
MfmCenterNode _ => MfmCenterNode(), MfmCenterNode _ => MfmCenterNode(document),
MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode), MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode, document),
MfmMathBlockNode mfmMathBlockNode => throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"), MfmMathBlockNode mfmMathBlockNode => throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"),
MfmQuoteNode mfmQuoteNode => MfmQuoteNode(mfmQuoteNode), MfmQuoteNode mfmQuoteNode => MfmQuoteNode(mfmQuoteNode, document),
IMfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"), IMfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"),
MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode), MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document),
MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, emoji, simple), MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji, simple),
MfmFnNode mfmFnNode => MfmFnNode(mfmFnNode), MfmFnNode mfmFnNode => MfmFnNode(mfmFnNode, document),
MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode), MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode, document),
MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode), MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document),
MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode), MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode, document),
MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode), MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode, document),
MfmInlineMathNode mfmInlineMathNode => throw new NotImplementedException($"{mfmInlineMathNode.GetType()}"), MfmInlineMathNode mfmInlineMathNode => throw new NotImplementedException($"{mfmInlineMathNode.GetType()}"),
MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, accountDomain), MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, document, accountDomain),
MfmPlainNode mfmPlainNode => MfmPlainNode(mfmPlainNode), MfmPlainNode mfmPlainNode => MfmPlainNode(mfmPlainNode, document),
MfmSmallNode _ => MfmSmallNode(), MfmSmallNode _ => MfmSmallNode(document),
MfmStrikeNode mfmStrikeNode => MfmStrikeNode(mfmStrikeNode), MfmStrikeNode mfmStrikeNode => MfmStrikeNode(mfmStrikeNode, document),
MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode), MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode, document),
MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode), MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode, document),
IMfmInlineNode mfmInlineNode => throw new NotImplementedException($"{mfmInlineNode.GetType()}"), IMfmInlineNode mfmInlineNode => throw new NotImplementedException($"{mfmInlineNode.GetType()}"),
_ => throw new ArgumentOutOfRangeException(nameof(node)) _ => throw new ArgumentOutOfRangeException(nameof(node))
}; };
@ -89,11 +84,11 @@ public static partial class MfmRenderer
{ {
try try
{ {
rendered.AppendNodes(RenderNode(childNode, emoji, accountDomain, simple)); rendered.AppendNodes(RenderNode(childNode, document, emoji, accountDomain, simple));
} }
catch (NotImplementedException e) catch (NotImplementedException e)
{ {
var fallback = CreateElement("span"); var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]"; fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
rendered.AppendNodes(fallback); rendered.AppendNodes(fallback);
} }
@ -103,56 +98,56 @@ public static partial class MfmRenderer
return rendered; return rendered;
} }
private static INode MfmPlainNode(MfmPlainNode _) private static INode MfmPlainNode(MfmPlainNode _, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "plain"; el.ClassName = "plain";
return el; return el;
} }
private static INode MfmCenterNode() private static INode MfmCenterNode(IDocument document)
{ {
var el = CreateElement("div"); var el = document.CreateElement("div");
el.SetAttribute("style", "text-align: center"); el.SetAttribute("style", "text-align: center");
return el; return el;
} }
private static INode MfmCodeBlockNode(MfmCodeBlockNode node) private static INode MfmCodeBlockNode(MfmCodeBlockNode node, IDocument document)
{ {
var el = CreateElement("pre"); var el = document.CreateElement("pre");
el.ClassName = "code-pre"; el.ClassName = "code-pre";
var childEl = CreateElement("code"); var childEl = document.CreateElement("code");
childEl.TextContent = node.Code; childEl.TextContent = node.Code;
el.AppendChild(childEl); el.AppendChild(childEl);
return el; return el;
} }
private static INode MfmQuoteNode(MfmQuoteNode _) private static INode MfmQuoteNode(MfmQuoteNode _, IDocument document)
{ {
var el = CreateElement("blockquote"); var el = document.CreateElement("blockquote");
el.ClassName = "quote-node"; el.ClassName = "quote-node";
return el; return el;
} }
private static INode MfmInlineCodeNode(MfmInlineCodeNode node) private static INode MfmInlineCodeNode(MfmInlineCodeNode node, IDocument document)
{ {
var el = CreateElement("code"); var el = document.CreateElement("code");
el.TextContent = node.Code; el.TextContent = node.Code;
return el; return el;
} }
private static INode MfmHashtagNode(MfmHashtagNode node) private static INode MfmHashtagNode(MfmHashtagNode node, IDocument document)
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", $"/tags/{node.Hashtag}"); el.SetAttribute("href", $"/tags/{node.Hashtag}");
el.ClassName = "hashtag-node"; el.ClassName = "hashtag-node";
el.TextContent = "#" + node.Hashtag; el.TextContent = "#" + node.Hashtag;
return el; return el;
} }
private static INode MfmLinkNode(MfmLinkNode node) private static INode MfmLinkNode(MfmLinkNode node, IDocument document)
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", node.Url); el.SetAttribute("href", node.Url);
el.SetAttribute("target", "_blank"); el.SetAttribute("target", "_blank");
el.ClassName = "link-node"; el.ClassName = "link-node";
@ -160,18 +155,18 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmItalicNode(MfmItalicNode _) private static INode MfmItalicNode(MfmItalicNode _, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.SetAttribute("style", "font-style: italic"); el.SetAttribute("style", "font-style: italic");
return el; return el;
} }
private static INode MfmEmojiCodeNode( private static INode MfmEmojiCodeNode(
MfmEmojiCodeNode node, List<EmojiResponse> emojiList, bool simple MfmEmojiCodeNode node, IDocument document, List<EmojiResponse> emojiList, bool simple
) )
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = simple ? "emoji simple" : "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);
@ -181,7 +176,7 @@ public static partial class MfmRenderer
} }
else else
{ {
var image = 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}:"); image.SetAttribute("title", $":{emoji.Name}:");
@ -191,9 +186,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmUrlNode(MfmUrlNode node) private static INode MfmUrlNode(MfmUrlNode node, IDocument document)
{ {
var el = CreateElement("a"); var el = document.CreateElement("a");
el.SetAttribute("href", node.Url); el.SetAttribute("href", node.Url);
el.SetAttribute("target", "_blank"); el.SetAttribute("target", "_blank");
el.ClassName = "url-node"; el.ClassName = "url-node";
@ -201,47 +196,47 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmBoldNode(MfmBoldNode _) private static INode MfmBoldNode(MfmBoldNode _, IDocument document)
{ {
var el = CreateElement("strong"); var el = document.CreateElement("strong");
return el; return el;
} }
private static INode MfmSmallNode() private static INode MfmSmallNode(IDocument document)
{ {
var el = CreateElement("small"); var el = document.CreateElement("small");
el.SetAttribute("style", "opacity: 0.7;"); el.SetAttribute("style", "opacity: 0.7;");
return el; return el;
} }
private static INode MfmStrikeNode(MfmStrikeNode _) private static INode MfmStrikeNode(MfmStrikeNode _, IDocument document)
{ {
var el = CreateElement("del"); var el = document.CreateElement("del");
return el; return el;
} }
private static INode MfmTextNode(MfmTextNode node) private static INode MfmTextNode(MfmTextNode node, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.TextContent = node.Text; el.TextContent = node.Text;
return el; return el;
} }
private static INode MfmMentionNode(MfmMentionNode node, string accountDomain) private static INode MfmMentionNode(MfmMentionNode node, IDocument document, string accountDomain)
{ {
var link = CreateElement("a"); var link = document.CreateElement("a");
link.SetAttribute("href", link.SetAttribute("href",
node.Host != null && node.Host != accountDomain node.Host != null && node.Host != accountDomain
? $"/@{node.Acct}" ? $"/@{node.Acct}"
: $"/@{node.User}"); : $"/@{node.User}");
link.ClassName = "mention"; link.ClassName = "mention";
var userPart = CreateElement("span"); var userPart = document.CreateElement("span");
userPart.ClassName = "user"; userPart.ClassName = "user";
userPart.TextContent = $"@{node.User}"; userPart.TextContent = $"@{node.User}";
link.AppendChild(userPart); link.AppendChild(userPart);
if (node.Host != null && node.Host != accountDomain) if (node.Host != null && node.Host != accountDomain)
{ {
var hostPart = CreateElement("span"); var hostPart = document.CreateElement("span");
hostPart.ClassName = "host"; hostPart.ClassName = "host";
hostPart.TextContent = $"@{node.Host}"; hostPart.TextContent = $"@{node.Host}";
link.AppendChild(hostPart); link.AppendChild(hostPart);
@ -250,46 +245,46 @@ public static partial class MfmRenderer
return link; return link;
} }
private static INode MfmFnNode(MfmFnNode node) private static INode MfmFnNode(MfmFnNode node, IDocument document)
{ {
// Simplify node.Args structure to make it more readable in below functions // Simplify node.Args structure to make it more readable in below functions
var args = node.Args ?? []; var args = node.Args ?? [];
return node.Name switch { return node.Name switch {
"flip" => MfmFnFlip(args), "flip" => MfmFnFlip(args, document),
"font" => MfmFnFont(args), "font" => MfmFnFont(args, document),
"x2" => MfmFnX(node.Name), "x2" => MfmFnX(node.Name, document),
"x3" => MfmFnX(node.Name), "x3" => MfmFnX(node.Name, document),
"x4" => MfmFnX(node.Name), "x4" => MfmFnX(node.Name, document),
"blur" => MfmFnBlur(), "blur" => MfmFnBlur(document),
"jelly" => MfmFnAnimation(node.Name, args), "jelly" => MfmFnAnimation(node.Name, args, document),
"tada" => MfmFnAnimation(node.Name, args), "tada" => MfmFnAnimation(node.Name, args, document),
"jump" => MfmFnAnimation(node.Name, args, "0.75s"), "jump" => MfmFnAnimation(node.Name, args, document, "0.75s"),
"bounce" => MfmFnAnimation(node.Name, args, "0.75s"), "bounce" => MfmFnAnimation(node.Name, args, document, "0.75s"),
"spin" => MfmFnSpin(args), "spin" => MfmFnSpin(args, document),
"shake" => MfmFnAnimation(node.Name, args, "0.5s"), "shake" => MfmFnAnimation(node.Name, args, document, "0.5s"),
"twitch" => MfmFnAnimation(node.Name, args, "0.5s"), "twitch" => MfmFnAnimation(node.Name, args, document, "0.5s"),
"rainbow" => MfmFnAnimation(node.Name, args), "rainbow" => MfmFnAnimation(node.Name, args, document),
"sparkle" => throw new NotImplementedException($"{node.Name}"), "sparkle" => throw new NotImplementedException($"{node.Name}"),
"rotate" => MfmFnRotate(args), "rotate" => MfmFnRotate(args, document),
"fade" => MfmFnFade(args), "fade" => MfmFnFade(args, document),
"crop" => MfmFnCrop(args), "crop" => MfmFnCrop(args, document),
"position" => MfmFnPosition(args), "position" => MfmFnPosition(args, document),
"scale" => MfmFnScale(args), "scale" => MfmFnScale(args, document),
"fg" => MfmFnFg(args), "fg" => MfmFnFg(args, document),
"bg" => MfmFnBg(args), "bg" => MfmFnBg(args, document),
"border" => MfmFnBorder(args), "border" => MfmFnBorder(args, document),
"ruby" => MfmFnRuby(node), "ruby" => MfmFnRuby(node, document),
"unixtime" => MfmFnUnixtime(node), "unixtime" => MfmFnUnixtime(node, document),
"center" => MfmCenterNode(), "center" => MfmCenterNode(document),
"small" => MfmSmallNode(), "small" => MfmSmallNode(document),
_ => throw new NotImplementedException($"{node.Name}") _ => throw new NotImplementedException($"{node.Name}")
}; };
} }
private static INode MfmFnFlip(Dictionary<string, string?> args) private static INode MfmFnFlip(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
if (args.ContainsKey("h") && args.ContainsKey("v")) if (args.ContainsKey("h") && args.ContainsKey("v"))
el.ClassName = "fn-flip h v"; el.ClassName = "fn-flip h v";
@ -301,9 +296,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnFont(Dictionary<string, string?> args) private static INode MfmFnFont(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
if (args.ContainsKey("serif")) if (args.ContainsKey("serif"))
el.SetAttribute("style", "font-family: serif;"); el.SetAttribute("style", "font-family: serif;");
@ -317,9 +312,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnX(string name) private static INode MfmFnX(string name, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var size = name switch var size = name switch
{ {
@ -333,9 +328,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnBlur() private static INode MfmFnBlur(IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "fn-blur"; el.ClassName = "fn-blur";
@ -343,10 +338,10 @@ public static partial class MfmRenderer
} }
private static INode MfmFnAnimation( private static INode MfmFnAnimation(
string name, Dictionary<string, string?> args, string defaultSpeed = "1s" string name, Dictionary<string, string?> args, IDocument document, string defaultSpeed = "1s"
) )
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "fn-animation"; el.ClassName = "fn-animation";
@ -363,9 +358,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnSpin(Dictionary<string, string?> args) private static INode MfmFnSpin(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "fn-spin"; el.ClassName = "fn-spin";
@ -390,9 +385,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnRotate(Dictionary<string, string?> args) private static INode MfmFnRotate(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var deg = args.GetValueOrDefault("deg") ?? "90"; var deg = args.GetValueOrDefault("deg") ?? "90";
@ -405,9 +400,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnFade(Dictionary<string, string?> args) private static INode MfmFnFade(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
el.ClassName = "fn-fade"; el.ClassName = "fn-fade";
@ -422,9 +417,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnCrop(Dictionary<string, string?> args) private static INode MfmFnCrop(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var inset = $"{args.GetValueOrDefault("top") ?? "0"}% {args.GetValueOrDefault("right") ?? "0"}% {args.GetValueOrDefault("bottom") ?? "0"}% {args.GetValueOrDefault("left") ?? "0"}%"; var inset = $"{args.GetValueOrDefault("top") ?? "0"}% {args.GetValueOrDefault("right") ?? "0"}% {args.GetValueOrDefault("bottom") ?? "0"}% {args.GetValueOrDefault("left") ?? "0"}%";
el.SetAttribute("style", $"display: inline-block; clip-path: inset({inset});"); el.SetAttribute("style", $"display: inline-block; clip-path: inset({inset});");
@ -432,9 +427,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnPosition(Dictionary<string, string?> args) private static INode MfmFnPosition(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var translateX = args.GetValueOrDefault("x") ?? "0"; var translateX = args.GetValueOrDefault("x") ?? "0";
var translateY = args.GetValueOrDefault("y") ?? "0"; var translateY = args.GetValueOrDefault("y") ?? "0";
@ -443,9 +438,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnScale(Dictionary<string, string?> args) private static INode MfmFnScale(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var scaleX = args.GetValueOrDefault("x") ?? "1"; var scaleX = args.GetValueOrDefault("x") ?? "1";
var scaleY = args.GetValueOrDefault("y") ?? "1"; var scaleY = args.GetValueOrDefault("y") ?? "1";
@ -462,9 +457,9 @@ public static partial class MfmRenderer
return color != null && ColorRegex().Match(color).Success; return color != null && ColorRegex().Match(color).Success;
} }
private static INode MfmFnFg(Dictionary<string, string?> args) private static INode MfmFnFg(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
if (args.TryGetValue("color", out var color) && ValidColor(color)) if (args.TryGetValue("color", out var color) && ValidColor(color))
el.SetAttribute("style", $"display: inline-block; color: #{color};"); el.SetAttribute("style", $"display: inline-block; color: #{color};");
@ -472,9 +467,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnBg(Dictionary<string, string?> args) private static INode MfmFnBg(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
if (args.TryGetValue("color", out var color) && ValidColor(color)) if (args.TryGetValue("color", out var color) && ValidColor(color))
el.SetAttribute("style", $"display: inline-block; background-color: #{color};"); el.SetAttribute("style", $"display: inline-block; background-color: #{color};");
@ -482,9 +477,9 @@ public static partial class MfmRenderer
return el; return el;
} }
private static INode MfmFnBorder(Dictionary<string, string?> args) private static INode MfmFnBorder(Dictionary<string, string?> args, IDocument document)
{ {
var el = CreateElement("span"); var el = document.CreateElement("span");
var width = args.GetValueOrDefault("width") ?? "1"; var width = args.GetValueOrDefault("width") ?? "1";
var radius = args.GetValueOrDefault("radius") ?? "0"; var radius = args.GetValueOrDefault("radius") ?? "0";
@ -505,9 +500,9 @@ public static partial class MfmRenderer
}; };
} }
private static INode MfmFnRuby(MfmFnNode node) private static INode MfmFnRuby(MfmFnNode node, IDocument document)
{ {
var el = CreateElement("ruby"); var el = document.CreateElement("ruby");
if (node.Children.Length != 1) return el; if (node.Children.Length != 1) return el;
var childText = GetNodeText(node.Children[0]); var childText = GetNodeText(node.Children[0]);
@ -517,24 +512,24 @@ public static partial class MfmRenderer
el.TextContent = split[0]; el.TextContent = split[0];
var rp1 = CreateElement("rp"); var rp1 = document.CreateElement("rp");
rp1.TextContent = "("; rp1.TextContent = "(";
el.AppendChild(rp1); el.AppendChild(rp1);
var rt = CreateElement("rt"); var rt = document.CreateElement("rt");
rt.TextContent = split[1]; rt.TextContent = split[1];
el.AppendChild(rt); el.AppendChild(rt);
var rp2 = CreateElement("rp"); var rp2 = document.CreateElement("rp");
rp1.TextContent = ")"; rp1.TextContent = ")";
el.AppendChild(rp2); el.AppendChild(rp2);
return el; return el;
} }
private static INode MfmFnUnixtime(MfmFnNode node) private static INode MfmFnUnixtime(MfmFnNode node, IDocument document)
{ {
var el = CreateElement("time"); var el = document.CreateElement("time");
if (node.Children.Length != 1) return el; if (node.Children.Length != 1) return el;
var childText = GetNodeText(node.Children[0]); var childText = GetNodeText(node.Children[0]);

View file

@ -36,7 +36,7 @@ internal class NoteStore : NoteMessageProvider, IDisposable
note.Poll = noteResponse.Poll; note.Poll = noteResponse.Poll;
AnyNoteChanged?.Invoke(this, note); AnyNoteChanged?.Invoke(this, note);
NoteChangedHandlers.FirstOrDefault(p => p.Key == note.Id).Value?.Invoke(this, note); NoteChangedHandlers.First(p => p.Key == note.Id).Value.Invoke(this, note);
} }
} }
public void Delete(string id) public void Delete(string id)

View file

@ -87,7 +87,7 @@ internal class NotificationStore : NoteMessageProvider, IAsyncDisposable
el.Value.Note.Attachments = noteResponse.Attachments; el.Value.Note.Attachments = noteResponse.Attachments;
el.Value.Note.Reactions = noteResponse.Reactions; el.Value.Note.Reactions = noteResponse.Reactions;
el.Value.Note.Poll = noteResponse.Poll; el.Value.Note.Poll = noteResponse.Poll;
NoteChangedHandlers.FirstOrDefault(p => p.Key == noteResponse.Id).Value?.Invoke(this, el.Value.Note); NoteChangedHandlers.First(p => p.Key == noteResponse.Id).Value.Invoke(this, el.Value.Note);
} }
} }
} }

View file

@ -37,7 +37,7 @@ internal class RelatedStore : NoteMessageProvider, IDisposable
note.Reactions = noteResponse.Reactions; note.Reactions = noteResponse.Reactions;
note.Poll = noteResponse.Poll; note.Poll = noteResponse.Poll;
NoteChangedHandlers.FirstOrDefault(p => p.Key == note.Id).Value?.Invoke(this, note); NoteChangedHandlers.First(p => p.Key == note.Id).Value.Invoke(this, note);
NoteChanged?.Invoke(this, note); NoteChanged?.Invoke(this, note);
} }
} }

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using Iceshrimp.Shared.Schemas.Web; using Iceshrimp.Shared.Schemas.Web;
namespace Iceshrimp.Frontend.Core.Services.NoteStore; namespace Iceshrimp.Frontend.Core.Services.NoteStore;
@ -17,8 +16,6 @@ internal class StateSynchronizer: IAsyncDisposable
public void Broadcast(NoteBase note) public void Broadcast(NoteBase note)
{ {
// Trace Logging for broadcast note is null;
if (note == null) throw new UnreachableException("Note null when not nullable");
NoteChanged?.Invoke(this, note); NoteChanged?.Invoke(this, note);
} }

View file

@ -27,7 +27,7 @@
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="BlazorIntersectionObserver" Version="3.1.0" /> <PackageReference Include="BlazorIntersectionObserver" Version="3.1.0" />
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" /> <PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
<PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.20" /> <PackageReference Include="Iceshrimp.MfmSharp" Version="1.2.18" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" />