Compare commits

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

10 commits

20 changed files with 136 additions and 60 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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