[frontend/pages] Add page for managing local and remote custom emojis

This commit is contained in:
pancakes 2025-01-27 23:50:03 +10:00 committed by Laura Hausmann
parent 91e0f09bc5
commit fdca264241
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
4 changed files with 203 additions and 0 deletions

View file

@ -27,6 +27,12 @@
<span class="text">@Loc["Overview"]</span>
</div>
</NavLink>
<NavLink href="/mod/emojis">
<div class="sidebar-btn">
<Icon Name="Icons.Smiley"/>
<span class="text">@Loc["Custom Emojis"]</span>
</div>
</NavLink>
</div>
@code {

View file

@ -9,6 +9,9 @@ internal class EmojiControllerModel(ApiClient api)
public Task<List<EmojiResponse>> GetAllEmojiAsync() =>
api.CallAsync<List<EmojiResponse>>(HttpMethod.Get, "/emoji");
public Task<List<EmojiResponse>> GetRemoteEmojiAsync() =>
api.CallAsync<List<EmojiResponse>>(HttpMethod.Get, "/emoji/remote");
public Task<EmojiResponse> UploadEmojiAsync(IBrowserFile file) =>
api.CallAsync<EmojiResponse>(HttpMethod.Post, "/emoji", data: file);

View file

@ -0,0 +1,120 @@
@page "/mod/emojis"
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Shared.Schemas.Web
@using Iceshrimp.Frontend.Components
@attribute [Authorize(Roles = "moderator")]
@layout ModerationLayout
@inject ApiService Api;
@inject IStringLocalizer<Localization> Loc;
<SectionContent SectionName="top-bar">
<Icon Name="Icons.Smiley"></Icon>
@Loc["Custom Emojis"]
</SectionContent>
@if (State is State.Loaded)
{
<div class="body">
<div class="emoji-search">
<input @bind="EmojiFilter" @bind:event="oninput" @bind:after="FilterEmojis" class="search" type="text" placeholder="Search" aria-label="search"/>
@if (Source == "remote")
{
<input @bind="HostFilter" @bind:event="oninput" @bind:after="FilterEmojis" class="search" type="text" placeholder="Host" aria-label="host"/>
}
<select class="search-from" @bind="Source" @bind:after="GetEmojis">
<option value="local">@Loc["Local"]</option>
<option value="remote">@Loc["Remote"]</option>
</select>
</div>
@foreach (var category in Categories)
{
<span class="category-name">@category.Key</span>
<div class="emoji-list">
@foreach (var emoji in category.Value)
{
<div class="emoji-entry">
<InlineEmoji Name="@emoji.Name" Url="@emoji.PublicUrl" Size="3rem"/>
<div class="emoji-details">
<span class="emoji-name">@emoji.Name</span>
<span>
@foreach (var alias in emoji.Aliases)
{
<span class="emoji-alias">@alias</span>
}
</span>
<span class="labels">
@if (Source == "remote")
{
<Icon Name="Icons.ArrowSquareOut" title="@Loc["Remote"]"/>
}
@if (emoji.Sensitive)
{
<Icon Name="Icons.EyeSlash" title="@Loc["Sensitive"]"/>
}
@if (!string.IsNullOrWhiteSpace(emoji.License))
{
<Icon Name="Icons.Article" title="@emoji.License"/>
}
</span>
</div>
</div>
}
</div>
}
</div>
}
@if (State is State.Empty)
{
<div class="body">
<i>This instance has no emojis</i>
</div>
}
@code {
private List<EmojiResponse> Emojis { get; set; } = [];
private Dictionary<string, List<EmojiResponse>> Categories { get; set; } = new();
private string EmojiFilter { get; set; } = "";
private string HostFilter { get; set; } = "";
private string Source { get; set; } = "local";
private State State { get; set; }
private void FilterEmojis()
{
Categories = Emojis
.Where(p => p.Name.Contains(EmojiFilter.Trim()) || p.Aliases.Count(a => a.Contains(EmojiFilter.Trim())) != 0)
.OrderBy(p => p.Name)
.ThenBy(p => p.Id)
.GroupBy(p => p.Category)
.Where(p => Source == "local" || (Source == "remote" && (p.Key?.Contains(HostFilter) ?? false)))
.OrderBy(p => string.IsNullOrEmpty(p.Key))
.ThenBy(p => p.Key)
.ToDictionary(p => p.Key ?? "Other", p => p.ToList());
}
private async Task GetEmojis()
{
State = State.Loading;
Emojis = Source == "remote" ? await Api.Emoji.GetRemoteEmojiAsync() : await Api.Emoji.GetAllEmojiAsync();
if (Emojis.Count == 0)
{
State = State.Empty;
}
else
{
FilterEmojis();
State = State.Loaded;
}
}
protected override async Task OnInitializedAsync()
{
await GetEmojis();
}
}

View file

@ -0,0 +1,74 @@
.emoji-search {
display: flex;
margin-top: 1rem;
}
.search {
display: inline-block;
width: 100%;
}
.search-from {
display: inline-block;
}
.category-name {
display: inline-block;
margin: 1rem 0 0.5rem;
font-weight: bold;
}
.emoji-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
width: 100%;
}
.emoji-entry {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
background-color: var(--foreground-color);
border-radius: 0.5rem;
}
.emoji-details {
display: flex;
flex-direction: column;
text-wrap: wrap;
word-break: break-all;
word-wrap: break-word;
}
.emoji-name::before,
.emoji-name::after {
display: inline;
content: ":";
opacity: 0.7;
}
.emoji-alias {
opacity: 0.7;
font-size: 0.8em;
}
.emoji-alias::before,
.emoji-alias::after {
display: inline;
content: ":";
}
::deep {
.labels .ph {
display: inline-block;
line-height: 1;
color: var(--notice-color);
}
}
@media (max-width: 1000px) {
.emoji-list {
grid-template-columns: 1fr;
}
}