@inherits LocalizableComponentBase @using System.Globalization @* The server's own custom emoji, for administrators (/clientapi/admin/emoji, EmojiService), grouped by category: each with its picture, its :shortcode:, whether its picker offers it and whether it is disabled (kept, but offered and shown nowhere). One is added with a shortcode, a category and a picture streamed from the file (PNG, GIF, WebP or JPEG, 256 KB at most), and changed in the same dialog (its category, the picker, disabled, a new picture; never its shortcode, its id on other servers), which shows PrivaPub's refusals: 409 for a shortcode taken, 422 for a picture it won't take. Deleting one is asked first: the posts and profiles using it show its shortcode as text. Every change reads the server's emoji for everyone again (EmojiService.Refresh), so this client's picker has it at once. *@
@Localizer["Add an emoji"] @Localizer["Read again"]

@Localizer["Everyone here can use them in posts, names and reactions, and other servers show them. The picker offers those in it; one written out as :shortcode: works too, unless it is disabled."]

@if (IsLoading) {

@Localizer["loading"]

} else if (emojis.Count == 0) {

@Localizer["No custom emoji yet."]

} @foreach (var group in Groups) {

@(group.Key is { Length: > 0 } category ? category : Localizer["No category"])

}
@if (isEditing) { @(editedEmoji is null ? Localizer["New emoji"] : Localizer["Edit :{0}:", editedEmoji.Shortcode])
@Localizer["Picture"]
@if ((preview ?? editedEmoji?.Url) is { Length: > 0 } shown) { @Localizer[ }
@Localizer["PNG, GIF, WebP or JPEG, {0} at most. The server fits it within 256 px and keeps a GIF's animation.", EmojiService.MaxPictureBytes.GetFileSize(Localizer)] @if (editedEmoji is not null) { @Localizer["Without a new one, it keeps its picture."] }
@Localizer["Offered in the picker of every client. Out of it, it is still taken when its shortcode is written."]
@Localizer["Kept, but offered nowhere, taken into no new post and shown in none."]
@Localizer["Cancel"] @if (isSaving) { } @Localizer["Save"]
} @code { [Inject] EmojiService EmojiService { get; set; } [Inject] ConfirmService Confirm { get; set; } [Inject] ILoggingService Logger { get; set; } static readonly string Accept = string.Join(',', EmojiService.PictureTypes.Concat([".png", ".gif", ".webp", ".jpg", ".jpeg"])); List emojis = []; HashSet busy = []; // the dialog: a new emoji (no editedEmoji) or the one being changed, as the form will save it bool isEditing; ViewLocalEmoji editedEmoji; LocalEmojiForm form = new(); // the picture picked, streamed from the file when the form is saved, and a small copy of it to show IBrowserFile picture; string preview; List pictureErrors = []; // what PrivaPub answered, and its status (data-emoji-error: 409 a shortcode taken, 422 a picture refused, 400 invalid) string error; string errorStatus; bool isSaving; // those with no category first, then by category List> Groups => [ .. emojis.OrderBy(e => e.Shortcode, StringComparer.OrdinalIgnoreCase) .GroupBy(e => e.Category ?? string.Empty) .OrderBy(g => g.Key.Length == 0 ? 0 : 1) .ThenBy(g => g.Key, StringComparer.CurrentCultureIgnoreCase) ]; List Categories => [.. emojis.Select(e => e.Category).Where(c => c is { Length: > 0 }).Distinct().Order(StringComparer.CurrentCultureIgnoreCase)]; static string StateOf(ViewLocalEmoji emoji) => emoji.Disabled ? "disabled" : emoji.VisibleInPicker ? "picker" : "hidden"; protected override async Task OnInitializedAsync() { if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); IsLoading = false; } async Task LoadData() => emojis = await EmojiService.List(); async Task Reload() { IsLoading = true; await LoadData(); IsLoading = false; } void New() => Open(null, new LocalEmojiForm()); void Edit(ViewLocalEmoji emoji) => Open(emoji, new LocalEmojiForm { Shortcode = emoji.Shortcode, Category = emoji.Category, VisibleInPicker = emoji.VisibleInPicker, Disabled = emoji.Disabled }); void Open(ViewLocalEmoji emoji, LocalEmojiForm editing) { editedEmoji = emoji; form = editing; picture = null; preview = null; pictureErrors = []; error = null; errorStatus = null; isEditing = true; } void Close() { isEditing = false; picture = null; preview = null; } void Closed(bool open) { if (!open) Close(); } async Task Pick(InputFileChangeEventArgs eventArgs) { pictureErrors = []; picture = null; preview = null; var file = eventArgs.File; if (!EmojiService.PictureTypes.Contains(EmojiService.TypeOf(file))) { pictureErrors.Add(Localizer["\"{0}\" is not a kind of file this server takes.", file.Name]); return; } if (file.Size > EmojiService.MaxPictureBytes) { pictureErrors.Add(Localizer["Supplied file \"{0}\" with size {1} exceeds the maximum of {2}.", file.Name, file.Size.GetFileSize(Localizer), EmojiService.MaxPictureBytes.GetFileSize(Localizer)]); return; } picture = file; preview = await Preview(file); } // a small copy the browser makes (a GIF's first frame); none when it can't async Task Preview(IBrowserFile file) { try { var small = await file.RequestImageFileAsync("image/png", 96, 96); await using var stream = small.OpenReadStream(EmojiService.MaxPictureBytes); using var buffer = new MemoryStream(); await stream.CopyToAsync(buffer); return $"data:image/png;base64,{Convert.ToBase64String(buffer.ToArray())}"; } catch (Exception ex) { await Logger.ProcessWarning($"No preview of {file.Name}: {ex.Message}", $"{nameof(EmojiPanel)}.{nameof(Preview)}"); return default; } } async Task Save() { error = null; errorStatus = null; if (editedEmoji is null && picture is null) { pictureErrors = [Localizer["Choose a picture."]]; return; } isSaving = true; var result = editedEmoji is null ? await EmojiService.Insert(form, picture) : await EmojiService.Update(form, picture); isSaving = false; if (result.IsInvalid) { error = result.ErrorMessage; errorStatus = result.StatusCode.ToString(CultureInfo.InvariantCulture); return; } var added = editedEmoji is null; Close(); Toast.AddResponseSuccess(added ? Localizer["Emoji added."] : Localizer["Emoji saved."]); if (result.Data is not ViewLocalEmoji saved) { await LoadData(); return; } emojis = [.. emojis.Where(e => e.Shortcode != saved.Shortcode), saved]; } async Task Delete(ViewLocalEmoji emoji) { if (!await Confirm.Ask(Localizer["Delete :{0}:?", emoji.Shortcode], Localizer["It is offered no more, and the posts and profiles that use it show the text :{0}: instead. Other servers keep their copy until they read those posts again.", emoji.Shortcode], Localizer["Delete"], true)) return; busy.Add(emoji.Shortcode); var result = await EmojiService.Delete(emoji.Shortcode); busy.Remove(emoji.Shortcode); if (result.IsInvalid) { Toast.AddResponseError(result); return; } emojis = [.. emojis.Where(e => e.Shortcode != emoji.Shortcode)]; Toast.AddResponseSuccess(Localizer["Emoji deleted."]); } }