@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services.NoteStore @using Iceshrimp.Frontend.Localization @using Iceshrimp.MfmSharp @using Iceshrimp.Shared.Schemas.Web @using Microsoft.Extensions.Localization @inject IJSRuntime Js @inject ApiService ApiService @inject ComposeService ComposeService @inject EmojiService EmojiService @inject SessionService SessionService @inject MetadataService MetadataService @inject IStringLocalizer Loc; @inject GlobalComponentSvc GlobalComponentSvc @inject SettingsService Settings; @inject NoteActions NoteActions;
@Loc["ComposeNote"] @Loc["Sending"] @Loc["Done"] @Loc["Retry"]
@if (ReplyOrQuote != null && !Preview) {
} @if (NoteDraft.Cw != null) {
} @if (AttachedQuote != null) {
@Loc["Attached quote"]
} @if (UploadingFiles != 0) {
@(UploadingFiles == 1 ? Loc["Uploading file"] : Loc["Uploading {0} files", UploadingFiles])
} @if (Attachments.Count != 0) {
@foreach (var attachment in Attachments) { }
} @if (Preview) {
@if (string.IsNullOrWhiteSpace(NoteDraft.Text)) { @Loc["Nothing to preview"] } else { }
}
@code { private ElementReference Dialog { get; set; } private IJSObjectReference _module = null!; private IList Attachments { get; set; } = []; private InputFile UploadInput { get; set; } = null!; private NoteBase? ReplyOrQuote { get; set; } private string? TextPlaceholder { get; set; } private ElementReference Textarea { get; set; } private ElementReference EmojiButton { get; set; } private StateButton SendButton { get; set; } = null!; private bool Preview { get; set; } private List EmojiList { get; set; } = []; private int NoteLength { get; set; } private bool SendLock { get; set; } = false; private int UploadingFiles { get; set; } private string? AttachedQuote { get; set; } private NoteCreateRequest NoteDraft { get; set; } = new() { Text = "", Visibility = NoteVisibility.Followers, // FIXME: Default to visibilty in settings Cw = null }; private Dictionary AvailablePlaceholders { get; set; } = new() { { "default", "What's on your mind?" }, { "reply", "Reply goes here!" }, { "quote", "Quote this post!" } }; private bool SendingDisabled() => NoteLength - ((NoteDraft.Cw?.Length ?? 0) + NoteDraft.Text.Length) < 0 || UploadingFiles != 0; private async Task HandleKeyDown(KeyboardEventArgs e) { if (e is { Code: "Enter", CtrlKey: true } or { Code: "Enter", MetaKey: true }) { if (!SendingDisabled()) await SendNote(); } } RenderFragment DropdownIcon(NoteVisibility vis) { return vis switch { NoteVisibility.Public => (@), NoteVisibility.Home => (@), NoteVisibility.Followers => (@), NoteVisibility.Specified => (@), _ => throw new ArgumentOutOfRangeException() }; } RenderFragment DropdownContent(NoteVisibility vis) { return vis switch { NoteVisibility.Public => (@Public), NoteVisibility.Home => (@Unlisted), NoteVisibility.Followers => (@Followers), NoteVisibility.Specified => (@Direct), _ => throw new ArgumentOutOfRangeException() }; } private IList> DropDownCreate() { return Enum.GetValues() .Select(vis => new DropdownElement { #pragma warning disable BL0005 // Setting this outside the component is fine until this is reworked Icon = DropdownIcon(vis), Content = DropdownContent(vis), Selection = vis #pragma warning restore BL0005 }) .ToList(); } // The Component is hidden, and triggered by a sepperate button. // That way we get it's functionality, without the styling limitations of the InputFile component private async Task OpenUpload() { await _module.InvokeVoidAsync("openUpload", UploadInput.Element); } public async Task OpenDialogRedraft(NoteResponse note) { await ResetState(); NoteDraft.Text = note.Text ?? ""; NoteDraft.Cw = note.Cw; NoteDraft.Visibility = note.Visibility; NoteDraft.MediaIds = note.Attachments.Select(p => p.Id).ToList(); NoteDraft.RenoteId = note.RenoteId; NoteDraft.ReplyId = note.ReplyId; UploadingFiles = 0; StateHasChanged(); await _module.InvokeVoidAsync("openDialog", Dialog); } public async Task OpenDialog(NoteBase? replyTo = null, NoteBase? quote = null) { var settings = await Settings.GetUserSettingsAsync(); if (replyTo != null) { var mentions = await EnumerateMentions(replyTo); await ResetState(); ReplyOrQuote = replyTo; NoteDraft.ReplyId = replyTo.Id; NoteDraft.Visibility = settings.DefaultNoteVisibility > replyTo.Visibility ? settings.DefaultNoteVisibility : replyTo.Visibility; NoteDraft.Cw = replyTo.Cw; TextPlaceholder = AvailablePlaceholders["reply"]; foreach (var el in mentions) { NoteDraft.Text += $"@{el} "; } } else if (quote != null) { await ResetState(); ReplyOrQuote = quote; NoteDraft.RenoteId = quote.Id; NoteDraft.Visibility = settings.DefaultNoteVisibility > quote.Visibility ? settings.DefaultNoteVisibility : quote.Visibility; TextPlaceholder = AvailablePlaceholders["quote"]; } else { await ResetState(); } StateHasChanged(); await _module.InvokeVoidAsync("openDialog", Dialog); } private async Task> EnumerateMentions(NoteBase noteBase) { List mentions = []; if (noteBase.User.Id != SessionService.Current!.Id) { var userMention = noteBase.User.Username; if (noteBase.User.Host != null) { userMention += $"@{noteBase.User.Host}"; } mentions.Add(userMention); } var instance = await MetadataService.Instance.Value; var mfmNodes = noteBase.Text != null ? MfmParser.Parse(noteBase.Text) : []; foreach (var node in mfmNodes) { if (node is MfmMentionNode mentionNode) { mentions.Add(mentionNode.Acct.Replace($"@{instance.AccountDomain}", "")); } } mentions = mentions.Distinct().ToList(); mentions.Remove(SessionService.Current.Username); return mentions; } private async Task ResetState() { var settings = await Settings.GetUserSettingsAsync(); ReplyOrQuote = null; Attachments = new List(); NoteDraft = new NoteCreateRequest { Text = "", Visibility = settings.DefaultNoteVisibility, Cw = null }; TextPlaceholder = AvailablePlaceholders["default"]; SendButton.State = StateButton.StateEnum.Initial; AttachedQuote = null; } private async Task CloseDialog() { await _module.InvokeVoidAsync("closeDialog", Dialog); } private async Task SendNote() { if (SendLock) return; SendLock = true; SendButton.State = StateButton.StateEnum.Loading; if (Attachments.Count > 0) { NoteDraft.MediaIds = Attachments.Select(x => x.Id).ToList(); } NoteDraft.RenoteId ??= AttachedQuote; try { await ApiService.Notes.CreateNoteAsync(NoteDraft); } catch (ApiException) { SendButton.State = StateButton.StateEnum.Failed; return; } if (ReplyOrQuote != null) { await NoteActions.RefetchNoteAsync(ReplyOrQuote.Id); } SendButton.State = StateButton.StateEnum.Success; await CloseDialog(); SendLock = false; SendButton.State = StateButton.StateEnum.Initial; // FIXME: Implement timeline refresh and call it here. } private void ToggleCw() { NoteDraft.Cw = NoteDraft.Cw == null ? "" : null; } private async Task Upload(InputFileChangeEventArgs e) { UploadingFiles += 1; var res = await ApiService.Drive.UploadFileAsync(e.File); Attachments.Add(res); UploadingFiles -= 1; } private void RemoveAttachment(string id) { var attachment = Attachments.FirstOrDefault(p => p.Id == id); if (attachment == null) return; Attachments.Remove(attachment); StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _module = await Js.InvokeAsync("import", "./Components/Compose.razor.js"); EmojiList = await EmojiService.GetEmojiAsync(); var instance = await MetadataService.Instance.Value; NoteLength = instance.Limits.NoteLength; ComposeService.ComposeDialog = this; } } private void ToggleEmojiPicker() { GlobalComponentSvc.EmojiPicker?.Open(EmojiButton, new EventCallback(this, AddEmoji)); } private async Task AddQuote() => await GlobalComponentSvc.PromptDialog?.Prompt(new EventCallback(this, AddQuoteCallback), Loc["Add quote"], Loc["Link to note"], "")!; private async Task AddQuoteCallback(string? url) { if (url == null) return; try { var res = await ApiService.Search.LookupAsync(url); if (res != null) { if (!res.TargetUrl.StartsWith("/notes/")) { await GlobalComponentSvc.NoticeDialog?.Display(Loc["You cannot quote a user profile"], NoticeDialog.NoticeType.Error)!; return; } AttachedQuote = res.TargetUrl[7..]; } } catch (ApiException e) { await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!; } } private void RemoveQuote() { AttachedQuote = null; } private async Task AddEmoji(EmojiResponse emoji) { var pos = await _module.InvokeAsync("getSelectionStart", Textarea); var text = NoteDraft.Text; var emojiString = $":{emoji.Name}: "; NoteDraft.Text = text.Insert(pos, emojiString); StateHasChanged(); } private async Task AddInlineMedia(string url) { var pos = await _module.InvokeAsync("getSelectionStart", Textarea); var text = NoteDraft.Text; var mediaString = $"$[media {url} ] "; NoteDraft.Text = text.Insert(pos, mediaString); StateHasChanged(); } protected override void OnInitialized() { TextPlaceholder = AvailablePlaceholders["default"]; } }