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">
<option name="myName" value="Project Default" />
<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" />
</profile>
</component>

View file

@ -14,7 +14,7 @@ public readonly record struct MfmRenderData(MarkupString Html, List<MfmInlineMed
[UsedImplicitly]
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,
List<PreviewAttachment>? media = null
)
@ -27,14 +27,14 @@ public class MfmRenderer(MfmConverter converter, FlagService flags) : ISingleton
flags.SupportsInlineMedia.Value = true;
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);
}
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;
}
}

View file

@ -30,31 +30,31 @@ public class NoteRenderer(
var attachments = await GetAttachmentsAsync(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,
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
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 res = new PreviewNote
{
User = users.First(p => p.Id == note.User.Id),
Text = renderedText?.Html,
Cw = note.Cw,
RawText = note.Text,
Uri = note.Uri ?? note.GetPublicUri(instance.Value),
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
User = users.First(p => p.Id == note.User.Id),
Text = renderedText?.Html,
Cw = note.Cw,
RawText = note.Text,
Uri = note.Uri ?? note.GetPublicUri(instance.Value),
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false,
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
Poll = polls.GetValueOrDefault(note.Id),
CreatedAt = note.CreatedAt.ToDisplayStringTz(),
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
Poll = polls.GetValueOrDefault(note.Id),
CreatedAt = note.CreatedAt.ToDisplayStringTz(),
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
};
return res;
@ -143,6 +143,8 @@ public class NoteRenderer(
var emoji = await GetEmojiAsync(allNotes);
var attachments = await GetAttachmentsAsync(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;
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 ?? [];
@ -37,8 +37,8 @@ public class UserRenderer(
AvatarUrl = user.GetAvatarUrl(instance.Value),
BannerUrl = user.GetBannerUrl(instance.Value),
RawDisplayName = user.DisplayName,
DisplayName = mfm.RenderSimple(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
Bio = mfm.RenderSimple(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
DisplayName = await mfm.RenderSimpleAsync(user.DisplayName, user.Host, mentions, emoji[user.Id], "span"),
Bio = await mfm.RenderSimpleAsync(user.UserProfile?.Description, user.Host, mentions, emoji[user.Id], "span"),
MovedToUri = user.MovedToUri
};
// @formatter:on
@ -64,6 +64,6 @@ public class UserRenderer(
public async Task<List<PreviewUser>> RenderManyAsync(List<User> 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();
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;
}

View file

@ -1,7 +1,6 @@
using System.Net;
using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
@ -9,7 +8,6 @@ using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
@ -159,44 +157,4 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
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)
{
(content, inlineMedia) = mfmConverter.ToHtml(text ?? "", mentionedUsers, note.UserHost, quoteUri,
quoteInaccessible, replyInaccessible, media: inlineMedia);
(content, inlineMedia) = await mfmConverter.ToHtmlAsync(text ?? "", mentionedUsers, note.UserHost, quoteUri,
quoteInaccessible, replyInaccessible, media: inlineMedia);
attachments.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url)));
}
@ -258,7 +258,7 @@ public class NoteRenderer(
_ => MfmInlineMedia.MediaType.Other
}, 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)));
var entry = new StatusEdit

View file

@ -1,11 +1,9 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@ -15,8 +13,7 @@ public class UserRenderer(
IOptions<Config.InstanceSection> config,
IOptionsSnapshot<Config.SecuritySection> security,
MfmConverter mfmConverter,
DatabaseContext db,
FlagService flags
DatabaseContext db
) : IScopedService
{
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 mentions = profile?.Mentions ?? [];
var fields = profile?.Fields
.Select(p => new Field
{
Name = p.Name,
Value = (mfmConverter.ToHtml(p.Value, mentions, user.Host)).Html,
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
? DateTime.Now.ToStringIso8601Like()
: null
});
var fields = profile != null
? await profile.Fields
.Select(async p => new Field
{
Name = p.Name,
Value = (await mfmConverter.ToHtmlAsync(p.Value, mentions, user.Host)).Html,
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
? DateTime.Now.ToStringIso8601Like()
: null
})
.AwaitAllAsync()
: null;
var fieldsSource = source
? 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 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
{
Id = user.Id,
@ -84,7 +65,7 @@ public class UserRenderer(
FollowersCount = user.FollowersCount,
FollowingCount = user.FollowingCount,
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),
Uri = user.Uri ?? user.GetPublicUri(config.Value),
AvatarStaticUrl = user.GetAvatarUrl(config.Value), //TODO
@ -96,30 +77,7 @@ public class UserRenderer(
IsBot = user.IsBot,
IsDiscoverable = user.IsExplorable,
Fields = fields?.ToList() ?? [],
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
Emoji = profileEmoji
};
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 J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
@ -6,32 +5,30 @@ namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
public class AccountEntity : IIdentifiable
{
[J("username")] public required string Username { get; set; }
[J("acct")] public required string Acct { get; set; }
[J("fqn")] public required string FullyQualifiedName { get; set; }
[J("display_name")] public required string DisplayName { get; set; }
[J("locked")] public required bool IsLocked { get; set; }
[J("created_at")] public required string CreatedAt { get; set; }
[J("followers_count")] public required long FollowersCount { get; set; }
[J("following_count")] public required long FollowingCount { get; set; }
[J("statuses_count")] public required long StatusesCount { get; set; }
[J("note")] public required string Note { get; set; }
[J("url")] public required string Url { get; set; }
[J("uri")] public required string Uri { get; set; }
[J("avatar")] public required string AvatarUrl { get; set; }
[J("avatar_static")] public required string AvatarStaticUrl { get; set; }
[J("header")] public required string HeaderUrl { get; set; }
[J("header_static")] public required string HeaderStaticUrl { get; set; }
[J("moved")] public required AccountEntity? MovedToAccount { get; set; }
[J("bot")] public required bool IsBot { get; set; }
[J("discoverable")] public required bool IsDiscoverable { get; set; }
[J("fields")] public required List<Field> Fields { get; set; }
[J("source")] public AccountSource? Source { get; set; }
[J("emojis")] public required List<EmojiEntity> Emoji { get; set; }
[J("id")] public required string Id { 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("username")] public required string Username { get; set; }
[J("acct")] public required string Acct { get; set; }
[J("fqn")] public required string FullyQualifiedName { get; set; }
[J("display_name")] public required string DisplayName { get; set; }
[J("locked")] public required bool IsLocked { get; set; }
[J("created_at")] public required string CreatedAt { get; set; }
[J("followers_count")] public required long FollowersCount { get; set; }
[J("following_count")] public required long FollowingCount { get; set; }
[J("statuses_count")] public required long StatusesCount { get; set; }
[J("note")] public required string Note { get; set; }
[J("url")] public required string Url { get; set; }
[J("uri")] public required string Uri { get; set; }
[J("avatar")] public required string AvatarUrl { get; set; }
[J("avatar_static")] public required string AvatarStaticUrl { get; set; }
[J("header")] public required string HeaderUrl { get; set; }
[J("header_static")] public required string HeaderStaticUrl { get; set; }
[J("moved")] public required AccountEntity? MovedToAccount { get; set; }
[J("bot")] public required bool IsBot { get; set; }
[J("discoverable")] public required bool IsDiscoverable { get; set; }
[J("fields")] public required List<Field> Fields { get; set; }
[J("source")] public AccountSource? Source { get; set; }
[J("emojis")] public required List<EmojiEntity> Emoji { get; set; }
[J("id")] public required string Id { get; set; }
[J("last_status_at")] public string? LastStatusAt { get; set; }
[J("avatar_description")] public required string AvatarDescription { 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,
Tags = tags,
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,
Source = rawText != null
? new ASNoteSource { Content = rawText, MediaType = "text/x.misskeymarkdown" }
@ -214,7 +214,7 @@ public class NoteRenderer(
To = to,
Tags = tags,
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,
Source = rawText != null
? 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.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.MfmSharp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@ -85,7 +84,7 @@ public class UserRenderer(
.ToList();
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;
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 AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using Iceshrimp.MfmSharp;
using Iceshrimp.Backend.Core.Services;
using Iceshrimp.MfmSharp.Helpers;
using Microsoft.Extensions.Options;
using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser;
using HtmlParser = AngleSharp.Html.Parser.HtmlParser;
@ -48,15 +47,7 @@ public class MfmConverter(
FlagService flags
) : ISingletonService
{
private static readonly HtmlParser Parser = new();
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(
public static async Task<HtmlMfmData> FromHtmlAsync(
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
html = html.ReplaceLineEndings("\n");
var dom = Parser.ParseDocument(html);
var dom = await new HtmlParser().ParseDocumentAsync(html);
if (dom.Body == null) return new HtmlMfmData("", media);
var sb = new StringBuilder();
@ -82,7 +73,7 @@ public class MfmConverter(
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 [];
@ -90,7 +81,7 @@ public class MfmConverter(
var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase);
html = regex.Replace(html, "\n");
var dom = Parser.ParseDocument(html);
var dom = await new HtmlParser().ParseDocumentAsync(html);
if (dom.Body == null) return [];
var parser = new HtmlMentionsExtractor();
@ -100,26 +91,28 @@ public class MfmConverter(
return parser.Mentions;
}
public MfmHtmlData ToHtml(
public async Task<MfmHtmlData> ToHtmlAsync(
IMfmNode[] nodes, List<Note.MentionedUser> mentions, string? host, string? quoteUri = null,
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
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;
if (replyInaccessible)
{
var wrapper = CreateElement("span");
var re = CreateElement("span");
var wrapper = document.CreateElement("span");
var re = document.CreateElement("span");
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
wrapper.AppendChild(re);
if (hasContent)
{
wrapper.AppendChild(CreateElement("br"));
wrapper.AppendChild(CreateElement("br"));
wrapper.AppendChild(document.CreateElement("br"));
wrapper.AppendChild(document.CreateElement("br"));
}
element.AppendChild(wrapper);
@ -127,23 +120,23 @@ public class MfmConverter(
var usedMedia = new List<MfmInlineMedia>();
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)
{
var a = CreateElement("a");
var a = document.CreateElement("a");
a.SetAttribute("href", quoteUri);
a.TextContent = quoteUri.StartsWith("https://") ? quoteUri[8..] : quoteUri[7..];
var quote = CreateElement("span");
var quote = document.CreateElement("span");
quote.ClassList.Add("quote-inline");
if (hasContent)
{
quote.AppendChild(CreateElement("br"));
quote.AppendChild(CreateElement("br"));
quote.AppendChild(document.CreateElement("br"));
quote.AppendChild(document.CreateElement("br"));
}
var re = CreateElement("span");
var re = document.CreateElement("span");
re.TextContent = "RE: ";
quote.AppendChild(re);
quote.AppendChild(a);
@ -151,47 +144,39 @@ public class MfmConverter(
}
else if (quoteInaccessible)
{
var wrapper = CreateElement("span");
var re = CreateElement("span");
var wrapper = document.CreateElement("span");
var re = document.CreateElement("span");
re.TextContent = "RE: \ud83d\udd12"; // lock emoji
if (hasContent)
{
wrapper.AppendChild(CreateElement("br"));
wrapper.AppendChild(CreateElement("br"));
wrapper.AppendChild(document.CreateElement("br"));
wrapper.AppendChild(document.CreateElement("br"));
}
wrapper.AppendChild(re);
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,
bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p",
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
)
{
var nodes = MfmParser.Parse(mfm);
return ToHtml(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();
return await ToHtmlAsync(nodes, mentions, host, quoteUri, quoteInaccessible,
replyInaccessible, rootElement, emoji, media);
}
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
)
{
@ -209,7 +194,7 @@ public class MfmConverter(
if (!flags.SupportsInlineMedia.Value || current.Type == MfmInlineMedia.MediaType.Other)
{
var el = CreateElement("a");
var el = document.CreateElement("a");
el.SetAttribute("href", current.Src);
if (current.Type == MfmInlineMedia.MediaType.Other)
@ -236,7 +221,7 @@ public class MfmConverter(
_ => throw new ArgumentOutOfRangeException()
};
var el = CreateElement(nodeName);
var el = document.CreateElement(nodeName);
el.SetAttribute("src", current.Src);
el.SetAttribute("alt", current.Alt);
return el;
@ -245,16 +230,16 @@ public class MfmConverter(
}
{
var el = CreateInlineFormattingElement("i");
AddHtmlMarkup(el, "*");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*");
var el = CreateInlineFormattingElement(document, "i");
AddHtmlMarkup(document, el, "*");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(document, el, "*");
return el;
}
}
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)
return Fallback();
@ -269,55 +254,55 @@ public class MfmConverter(
IElement Fallback()
{
AddHtmlMarkup(el, "*");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*");
AddHtmlMarkup(document, el, "*");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(document, el, "*");
return el;
}
}
case MfmBoldNode:
{
var el = CreateInlineFormattingElement("b");
AddHtmlMarkup(el, "**");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "**");
var el = CreateInlineFormattingElement(document, "b");
AddHtmlMarkup(document, el, "**");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(document, el, "**");
return el;
}
case MfmSmallNode:
{
var el = CreateElement("small");
AppendChildren(el, node, mentions, host, usedMedia);
var el = document.CreateElement("small");
AppendChildren(el, document, node, mentions, host, usedMedia);
return el;
}
case MfmStrikeNode:
{
var el = CreateInlineFormattingElement("del");
AddHtmlMarkup(el, "~~");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "~~");
var el = CreateInlineFormattingElement(document, "del");
AddHtmlMarkup(document, el, "~~");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(document, el, "~~");
return el;
}
case MfmItalicNode:
case MfmFnNode:
{
var el = CreateInlineFormattingElement("i");
AddHtmlMarkup(el, "*");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkup(el, "*");
var el = CreateInlineFormattingElement(document, "i");
AddHtmlMarkup(document, el, "*");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkup(document, el, "*");
return el;
}
case MfmCodeBlockNode codeBlockNode:
{
var el = CreateInlineFormattingElement("pre");
var inner = CreateInlineFormattingElement("code");
var el = CreateInlineFormattingElement(document, "pre");
var inner = CreateInlineFormattingElement(document, "code");
inner.TextContent = codeBlockNode.Code;
el.AppendNodes(inner);
return el;
}
case MfmCenterNode:
{
var el = CreateElement("div");
AppendChildren(el, node, mentions, host, usedMedia);
var el = document.CreateElement("div");
AppendChildren(el, document, node, mentions, host, usedMedia);
return el;
}
case MfmEmojiCodeNode emojiCodeNode:
@ -325,8 +310,8 @@ public class MfmConverter(
var punyHost = host?.ToPunycodeLower();
if (emoji?.FirstOrDefault(p => p.Name == emojiCodeNode.Name && p.Host == punyHost) is { } hit)
{
var el = CreateElement("span");
var inner = CreateElement("img");
var el = document.CreateElement("span");
var inner = document.CreateElement("img");
inner.SetAttribute("src", mediaProxy.GetProxyUrl(hit));
inner.SetAttribute("alt", hit.Name);
el.AppendChild(inner);
@ -334,11 +319,11 @@ public class MfmConverter(
return el;
}
return CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B");
return document.CreateTextNode($"\u200B:{emojiCodeNode.Name}:\u200B");
}
case MfmHashtagNode hashtagNode:
{
var el = CreateElement("a");
var el = document.CreateElement("a");
el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}");
el.TextContent = $"#{hashtagNode.Hashtag}";
el.SetAttribute("rel", "tag");
@ -347,32 +332,32 @@ public class MfmConverter(
}
case MfmInlineCodeNode inlineCodeNode:
{
var el = CreateInlineFormattingElement("code");
var el = CreateInlineFormattingElement(document, "code");
el.TextContent = inlineCodeNode.Code;
return el;
}
case MfmInlineMathNode inlineMathNode:
{
var el = CreateInlineFormattingElement("code");
var el = CreateInlineFormattingElement(document, "code");
el.TextContent = inlineMathNode.Formula;
return el;
}
case MfmMathBlockNode mathBlockNode:
{
var el = CreateInlineFormattingElement("code");
var el = CreateInlineFormattingElement(document, "code");
el.TextContent = mathBlockNode.Formula;
return el;
}
case MfmLinkNode linkNode:
{
var el = CreateElement("a");
var el = document.CreateElement("a");
el.SetAttribute("href", linkNode.Url);
el.TextContent = linkNode.Text;
return el;
}
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
var finalHost = mentionNode.Host ?? host ?? config.Value.AccountDomain;
@ -393,10 +378,10 @@ public class MfmConverter(
{
el.ClassList.Add("h-card");
el.SetAttribute("translate", "no");
var a = CreateElement("a");
var a = document.CreateElement("a");
a.ClassList.Add("u-url", "mention");
a.SetAttribute("href", mention.Url ?? mention.Uri);
var span = CreateElement("span");
var span = document.CreateElement("span");
span.TextContent = $"@{mention.Username}";
a.AppendChild(span);
el.AppendChild(a);
@ -406,25 +391,25 @@ public class MfmConverter(
}
case MfmQuoteNode:
{
var el = CreateInlineFormattingElement("blockquote");
AddHtmlMarkup(el, "> ");
AppendChildren(el, node, mentions, host, usedMedia);
AddHtmlMarkupTag(el, "br");
AddHtmlMarkupTag(el, "br");
var el = CreateInlineFormattingElement(document, "blockquote");
AddHtmlMarkup(document, el, "> ");
AppendChildren(el, document, node, mentions, host, usedMedia);
AddHtmlMarkupTag(document, el, "br");
AddHtmlMarkupTag(document, el, "br");
return el;
}
case MfmTextNode textNode:
{
var el = CreateElement("span");
var el = document.CreateElement("span");
var nodes = textNode.Text.Split("\r\n")
.SelectMany(p => p.Split('\r'))
.SelectMany(p => p.Split('\n'))
.Select(CreateTextNode);
.Select(document.CreateTextNode);
foreach (var htmlNode in nodes)
{
el.AppendNodes(htmlNode);
el.AppendNodes(CreateElement("br"));
el.AppendNodes(document.CreateElement("br"));
}
if (el.LastChild != null)
@ -433,25 +418,17 @@ public class MfmConverter(
}
case MfmUrlNode urlNode:
{
if (
!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");
var el = document.CreateElement("a");
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;
}
case MfmPlainNode:
{
var el = CreateElement("span");
AppendChildren(el, node, mentions, host, usedMedia);
var el = document.CreateElement("span");
AppendChildren(el, document, node, mentions, host, usedMedia);
return el;
}
default:
@ -462,32 +439,32 @@ public class MfmConverter(
}
private void AppendChildren(
INode element, IMfmNode parent,
INode element, IDocument document, IMfmNode parent,
List<Note.MentionedUser> mentions, string? host, List<MfmInlineMedia> usedMedia,
List<Emoji>? emoji = null, List<MfmInlineMedia>? media = null
)
{
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;
var el = CreateElement("span");
el.AppendChild(CreateTextNode(chars));
var el = document.CreateElement("span");
el.AppendChild(document.CreateTextNode(chars));
node.AppendChild(el);
}
private void AddHtmlMarkupTag(IElement node, string tag)
private void AddHtmlMarkupTag(IDocument document, IElement node, string tag)
{
if (flags.SupportsHtmlFormatting.Value) return;
var el = CreateElement(tag);
var el = document.CreateElement(tag);
node.AppendChild(el);
}
}

View file

@ -1028,7 +1028,7 @@ public class NoteService(
.ToList()
?? [];
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags);
(text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
}
var cw = note.Summary;
@ -1128,7 +1128,7 @@ public class NoteService(
.ToList()
?? [];
(text, htmlInlineMedia) = MfmConverter.FromHtml(note.Content, mentionData.Mentions, hashtags);
(text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions, hashtags);
}
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 ([], []);
var parsedFields = fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
.Select(MfmConverter.ExtractMentionsFromHtml);
var parsedFields = await fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
.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 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 fields = actor.Attachments?.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.Select(p => new UserProfile.Field
{
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm
});
var fields = actor.Attachments != null
? await actor.Attachments
.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.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 ?? "");
@ -166,7 +170,7 @@ public class UserService(
.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);
@ -314,12 +318,16 @@ public class UserService(
?? throw new
Exception("User host must not be null at this stage"));
var fields = actor.Attachments?.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.Select(p => new UserProfile.Field
{
Name = p.Name!, Value = MfmConverter.FromHtml(p.Value).Mfm
});
var fields = actor.Attachments != null
? await actor.Attachments
.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.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 ?? "");
@ -340,7 +348,7 @@ public class UserService(
.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;
@ -1124,17 +1132,21 @@ public class UserService(
{
var (mentions, splitDomainMapping) =
await bgMentionsResolver.ResolveMentionsAsync(actor, bgUser.Host);
var fields = actor.Attachments?.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.Select(p => new UserProfile.Field
{
Name = p.Name!,
Value = MfmConverter.FromHtml(p.Value, mentions).Mfm
});
var fields = actor.Attachments != null
? await actor.Attachments
.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.Select(async p => new UserProfile.Field
{
Name = p.Name!,
Value = (await MfmConverter.FromHtmlAsync(p.Value, mentions)).Mfm
})
.AwaitAllAsync()
: null;
var description = actor.MkSummary != null
? 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.Fields = fields?.ToArray() ?? [];

View file

@ -56,7 +56,7 @@
<PackageReference Include="Ulid" Version="1.3.4" />
<PackageReference Include="Iceshrimp.Assets.Branding" Version="1.0.1" />
<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.MimeTypes" Version="1.0.1" />
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />

View file

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

View file

@ -7,8 +7,8 @@ ListenHost = localhost
;;ListenSocketPerms = 660
;; Caution: changing these settings after initial setup *will* break federation
WebDomain = localhost:3000
AccountDomain = localhost:3000
WebDomain = shrimp.example.org
AccountDomain = example.org
;; End of problematic settings block
;; Additional domains this instance allows API access from, separated by commas.
@ -184,7 +184,7 @@ ProxyRemoteMedia = true
[Storage:Local]
;; 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]
;;Endpoint = endpoint.example.org

View file

@ -15,7 +15,7 @@
if (Text != null)
{
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)
{
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 {
z-index: 5;
z-index: 1;
width: 100%;
margin-top: 0.5em;
background-color: var(--background-color);

View file

@ -86,54 +86,54 @@
</button>
<button @ref="MenuButton" class="btn" @onclick="ToggleMenu" @onclick:stopPropagation="true" aria-label="more">
<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>
<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>
@code {

View file

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

View file

@ -1,8 +1,6 @@
using System.Text.RegularExpressions;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using AngleSharp.Text;
using Iceshrimp.MfmSharp;
using Iceshrimp.Shared.Schemas.Web;
@ -12,37 +10,34 @@ namespace Iceshrimp.Frontend.Core.Miscellaneous;
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
)
{
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();
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(
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.ClassName = "mfm";
foreach (var node in nodes)
{
try
{
el.AppendNodes(RenderNode(node, emoji, accountDomain, simple));
el.AppendNodes(RenderNode(node, document, emoji, accountDomain, simple));
}
catch (NotImplementedException e)
{
var fallback = CreateElement("span");
var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
el.AppendNodes(fallback);
}
@ -52,32 +47,32 @@ public static partial class MfmRenderer
}
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
// @formatter:off
var rendered = node switch
{
MfmCenterNode _ => MfmCenterNode(),
MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode),
MfmCenterNode _ => MfmCenterNode(document),
MfmCodeBlockNode mfmCodeBlockNode => MfmCodeBlockNode(mfmCodeBlockNode, document),
MfmMathBlockNode mfmMathBlockNode => throw new NotImplementedException($"{mfmMathBlockNode.GetType()}"),
MfmQuoteNode mfmQuoteNode => MfmQuoteNode(mfmQuoteNode),
MfmQuoteNode mfmQuoteNode => MfmQuoteNode(mfmQuoteNode, document),
IMfmBlockNode mfmBlockNode => throw new NotImplementedException($"{mfmBlockNode.GetType()}"),
MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode),
MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, emoji, simple),
MfmFnNode mfmFnNode => MfmFnNode(mfmFnNode),
MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode),
MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode),
MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode),
MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode),
MfmBoldNode mfmBoldNode => MfmBoldNode(mfmBoldNode, document),
MfmEmojiCodeNode mfmEmojiCodeNode => MfmEmojiCodeNode(mfmEmojiCodeNode, document, emoji, simple),
MfmFnNode mfmFnNode => MfmFnNode(mfmFnNode, document),
MfmHashtagNode mfmHashtagNode => MfmHashtagNode(mfmHashtagNode, document),
MfmInlineCodeNode mfmInlineCodeNode => MfmInlineCodeNode(mfmInlineCodeNode, document),
MfmItalicNode mfmItalicNode => MfmItalicNode(mfmItalicNode, document),
MfmLinkNode mfmLinkNode => MfmLinkNode(mfmLinkNode, document),
MfmInlineMathNode mfmInlineMathNode => throw new NotImplementedException($"{mfmInlineMathNode.GetType()}"),
MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, accountDomain),
MfmPlainNode mfmPlainNode => MfmPlainNode(mfmPlainNode),
MfmSmallNode _ => MfmSmallNode(),
MfmStrikeNode mfmStrikeNode => MfmStrikeNode(mfmStrikeNode),
MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode),
MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode),
MfmMentionNode mfmMentionNode => MfmMentionNode(mfmMentionNode, document, accountDomain),
MfmPlainNode mfmPlainNode => MfmPlainNode(mfmPlainNode, document),
MfmSmallNode _ => MfmSmallNode(document),
MfmStrikeNode mfmStrikeNode => MfmStrikeNode(mfmStrikeNode, document),
MfmTextNode mfmTextNode => MfmTextNode(mfmTextNode, document),
MfmUrlNode mfmUrlNode => MfmUrlNode(mfmUrlNode, document),
IMfmInlineNode mfmInlineNode => throw new NotImplementedException($"{mfmInlineNode.GetType()}"),
_ => throw new ArgumentOutOfRangeException(nameof(node))
};
@ -89,11 +84,11 @@ public static partial class MfmRenderer
{
try
{
rendered.AppendNodes(RenderNode(childNode, emoji, accountDomain, simple));
rendered.AppendNodes(RenderNode(childNode, document, emoji, accountDomain, simple));
}
catch (NotImplementedException e)
{
var fallback = CreateElement("span");
var fallback = document.CreateElement("span");
fallback.TextContent = $"[Node type <{e.Message}> not implemented]";
rendered.AppendNodes(fallback);
}
@ -103,56 +98,56 @@ public static partial class MfmRenderer
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";
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");
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";
var childEl = CreateElement("code");
var childEl = document.CreateElement("code");
childEl.TextContent = node.Code;
el.AppendChild(childEl);
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";
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;
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.ClassName = "hashtag-node";
el.TextContent = "#" + node.Hashtag;
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("target", "_blank");
el.ClassName = "link-node";
@ -160,18 +155,18 @@ public static partial class MfmRenderer
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");
return el;
}
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";
var emoji = emojiList.Find(p => p.Name == node.Name);
@ -181,7 +176,7 @@ public static partial class MfmRenderer
}
else
{
var image = CreateElement("img");
var image = document.CreateElement("img");
image.SetAttribute("src", emoji.PublicUrl);
image.SetAttribute("alt", node.Name);
image.SetAttribute("title", $":{emoji.Name}:");
@ -191,9 +186,9 @@ public static partial class MfmRenderer
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("target", "_blank");
el.ClassName = "url-node";
@ -201,47 +196,47 @@ public static partial class MfmRenderer
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;
}
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;");
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;
}
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;
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",
node.Host != null && node.Host != accountDomain
? $"/@{node.Acct}"
: $"/@{node.User}");
link.ClassName = "mention";
var userPart = CreateElement("span");
var userPart = document.CreateElement("span");
userPart.ClassName = "user";
userPart.TextContent = $"@{node.User}";
link.AppendChild(userPart);
if (node.Host != null && node.Host != accountDomain)
{
var hostPart = CreateElement("span");
var hostPart = document.CreateElement("span");
hostPart.ClassName = "host";
hostPart.TextContent = $"@{node.Host}";
link.AppendChild(hostPart);
@ -250,46 +245,46 @@ public static partial class MfmRenderer
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
var args = node.Args ?? [];
return node.Name switch {
"flip" => MfmFnFlip(args),
"font" => MfmFnFont(args),
"x2" => MfmFnX(node.Name),
"x3" => MfmFnX(node.Name),
"x4" => MfmFnX(node.Name),
"blur" => MfmFnBlur(),
"jelly" => MfmFnAnimation(node.Name, args),
"tada" => MfmFnAnimation(node.Name, args),
"jump" => MfmFnAnimation(node.Name, args, "0.75s"),
"bounce" => MfmFnAnimation(node.Name, args, "0.75s"),
"spin" => MfmFnSpin(args),
"shake" => MfmFnAnimation(node.Name, args, "0.5s"),
"twitch" => MfmFnAnimation(node.Name, args, "0.5s"),
"rainbow" => MfmFnAnimation(node.Name, args),
"flip" => MfmFnFlip(args, document),
"font" => MfmFnFont(args, document),
"x2" => MfmFnX(node.Name, document),
"x3" => MfmFnX(node.Name, document),
"x4" => MfmFnX(node.Name, document),
"blur" => MfmFnBlur(document),
"jelly" => MfmFnAnimation(node.Name, args, document),
"tada" => MfmFnAnimation(node.Name, args, document),
"jump" => MfmFnAnimation(node.Name, args, document, "0.75s"),
"bounce" => MfmFnAnimation(node.Name, args, document, "0.75s"),
"spin" => MfmFnSpin(args, document),
"shake" => MfmFnAnimation(node.Name, args, document, "0.5s"),
"twitch" => MfmFnAnimation(node.Name, args, document, "0.5s"),
"rainbow" => MfmFnAnimation(node.Name, args, document),
"sparkle" => throw new NotImplementedException($"{node.Name}"),
"rotate" => MfmFnRotate(args),
"fade" => MfmFnFade(args),
"crop" => MfmFnCrop(args),
"position" => MfmFnPosition(args),
"scale" => MfmFnScale(args),
"fg" => MfmFnFg(args),
"bg" => MfmFnBg(args),
"border" => MfmFnBorder(args),
"ruby" => MfmFnRuby(node),
"unixtime" => MfmFnUnixtime(node),
"center" => MfmCenterNode(),
"small" => MfmSmallNode(),
"rotate" => MfmFnRotate(args, document),
"fade" => MfmFnFade(args, document),
"crop" => MfmFnCrop(args, document),
"position" => MfmFnPosition(args, document),
"scale" => MfmFnScale(args, document),
"fg" => MfmFnFg(args, document),
"bg" => MfmFnBg(args, document),
"border" => MfmFnBorder(args, document),
"ruby" => MfmFnRuby(node, document),
"unixtime" => MfmFnUnixtime(node, document),
"center" => MfmCenterNode(document),
"small" => MfmSmallNode(document),
_ => 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"))
el.ClassName = "fn-flip h v";
@ -301,9 +296,9 @@ public static partial class MfmRenderer
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"))
el.SetAttribute("style", "font-family: serif;");
@ -317,9 +312,9 @@ public static partial class MfmRenderer
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
{
@ -333,9 +328,9 @@ public static partial class MfmRenderer
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";
@ -343,10 +338,10 @@ public static partial class MfmRenderer
}
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";
@ -363,9 +358,9 @@ public static partial class MfmRenderer
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";
@ -390,9 +385,9 @@ public static partial class MfmRenderer
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";
@ -405,9 +400,9 @@ public static partial class MfmRenderer
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";
@ -422,9 +417,9 @@ public static partial class MfmRenderer
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"}%";
el.SetAttribute("style", $"display: inline-block; clip-path: inset({inset});");
@ -432,9 +427,9 @@ public static partial class MfmRenderer
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 translateY = args.GetValueOrDefault("y") ?? "0";
@ -443,9 +438,9 @@ public static partial class MfmRenderer
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 scaleY = args.GetValueOrDefault("y") ?? "1";
@ -462,9 +457,9 @@ public static partial class MfmRenderer
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))
el.SetAttribute("style", $"display: inline-block; color: #{color};");
@ -472,9 +467,9 @@ public static partial class MfmRenderer
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))
el.SetAttribute("style", $"display: inline-block; background-color: #{color};");
@ -482,9 +477,9 @@ public static partial class MfmRenderer
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 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;
var childText = GetNodeText(node.Children[0]);
@ -517,24 +512,24 @@ public static partial class MfmRenderer
el.TextContent = split[0];
var rp1 = CreateElement("rp");
var rp1 = document.CreateElement("rp");
rp1.TextContent = "(";
el.AppendChild(rp1);
var rt = CreateElement("rt");
var rt = document.CreateElement("rt");
rt.TextContent = split[1];
el.AppendChild(rt);
var rp2 = CreateElement("rp");
var rp2 = document.CreateElement("rp");
rp1.TextContent = ")";
el.AppendChild(rp2);
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;
var childText = GetNodeText(node.Children[0]);

View file

@ -36,7 +36,7 @@ internal class NoteStore : NoteMessageProvider, IDisposable
note.Poll = noteResponse.Poll;
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)

View file

@ -87,7 +87,7 @@ internal class NotificationStore : NoteMessageProvider, IAsyncDisposable
el.Value.Note.Attachments = noteResponse.Attachments;
el.Value.Note.Reactions = noteResponse.Reactions;
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.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);
}
}

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using Iceshrimp.Shared.Schemas.Web;
namespace Iceshrimp.Frontend.Core.Services.NoteStore;
@ -17,8 +16,6 @@ internal class StateSynchronizer: IAsyncDisposable
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);
}

View file

@ -27,7 +27,7 @@
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="BlazorIntersectionObserver" Version="3.1.0" />
<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.WebAssembly" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" />