Iceshrimp.NET/Iceshrimp.Frontend/Components/Compose.razor

309 lines
11 KiB
Text

@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.Parsing
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@inject IJSRuntime Js
@inject ApiService ApiService
@inject ComposeService ComposeService
@inject SessionService SessionService
@inject IStringLocalizer<Localization> Loc;
@inject GlobalComponentSvc GlobalComponentSvc
@inject MessageService MessageService
<dialog class="dialog" @ref="Dialog">
<div class="compose">
<div class="header">
<button class="btn" title="@Loc["Close"]" @onclick="CloseDialog" aria-label="close">
<Icon Name="Icons.X"/>
</button>
<Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/>
<StateButton OnClick="SendNote" @ref="SendButton" ExtraClasses="post-btn" AriaLabel="post">
<Initial>
@Loc["ComposeNote"]<Icon Name="Icons.PaperPlaneRight"/>
</Initial>
<Loading>
@Loc["Sending"]<Icon Name="Icons.Spinner"/>
</Loading>
<Success>
@Loc["Done"]<Icon Name="Icons.Check"/>
</Success>
<Failed>
@Loc["Retry"]<Icon Name="Icons.X"/>
</Failed>
</StateButton>
</div>
@if (ReplyOrQuote != null)
{
<div class="reply-or-quote">
<NoteComponent Note="ReplyOrQuote" AsQuote="true"/>
</div>
}
@if (NoteDraft.Cw != null)
{
<input @bind="NoteDraft.Cw" class="input cw-field" placeholder="Content Warning"
aria-label="content warning"/>
<hr class="separator"/>
}
<textarea @ref="Textarea" @bind="NoteDraft.Text" class="textarea" placeholder="@TextPlaceholder" rows="5"
cols="35" autofocus="autofocus" aria-label="note text"></textarea>
<div class="footer">
<button class="btn" title="@Loc["Upload file"]" @onclick="OpenUpload" aria-label="upload">
<Icon Name="Icons.Upload" Size="1.3rem"></Icon>
</button>
<button class="btn" title="@Loc["Content warning"]" @onclick="ToggleCw" aria-label="content warning">
<Icon Name="Icons.EyeSlash" Size="1.3rem"></Icon>
</button>
<button @ref="EmojiButton" class="btn" title="@Loc["Emoji"]" @onclick="ToggleEmojiPicker"
aria-label="emoji">
<Icon Name="Icons.Smiley" Size="1.3rem"></Icon>
</button>
<div class="file-input">
<InputFile @ref="UploadInput" OnChange="Upload">Upload!</InputFile>
</div>
</div>
<div @onclick="CloseDialog" class="backdrop"></div>
</div>
</dialog>
@code {
private ElementReference Dialog { get; set; }
private IJSObjectReference _module = null!;
private IList<DriveFileResponse> 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 NoteCreateRequest NoteDraft { get; set; } = new()
{
Text = "",
Visibility = NoteVisibility.Followers, // FIXME: Default to visibilty in settings
Cw = null
};
private Dictionary<string, string> AvailablePlaceholders { get; set; } = new()
{
{ "default", "What's on your mind?" },
{ "reply", "Reply goes here!" },
{ "quote", "Quote this post!" }
};
RenderFragment DropdownIcon(NoteVisibility vis)
{
return vis switch
{
NoteVisibility.Public => (@<Icon Name="Icons.Globe"/>),
NoteVisibility.Home => (@<Icon Name="Icons.House"/>),
NoteVisibility.Followers => (@<Icon Name="Icons.Lock"/>),
NoteVisibility.Specified => (@<Icon Name="Icons.Envelope"/>),
_ => throw new ArgumentOutOfRangeException()
};
}
RenderFragment DropdownContent(NoteVisibility vis)
{
return vis switch
{
NoteVisibility.Public => (@<span class="dropdown-title">Public</span>),
NoteVisibility.Home => (@<span class="dropdown-title">Unlisted</span>),
NoteVisibility.Followers => (@<span class="dropdown-title">Followers</span>),
NoteVisibility.Specified => (@<span class="dropdown-title">Direct</span>),
_ => throw new ArgumentOutOfRangeException()
};
}
private IList<DropdownElement<NoteVisibility>> DropDownCreate()
{
return Enum.GetValues<NoteVisibility>()
.Select(vis =>
new DropdownElement<NoteVisibility>
{
#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 <InputFile> 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)
{
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;
StateHasChanged();
await _module.InvokeVoidAsync("openDialog", Dialog);
}
public async Task OpenDialog(NoteBase? replyTo = null, NoteBase? quote = null)
{
if (replyTo != null)
{
var mentions = EnumerateMentions(replyTo);
ResetState();
ReplyOrQuote = replyTo;
NoteDraft.ReplyId = replyTo.Id;
NoteDraft.Visibility = replyTo.Visibility;
NoteDraft.Cw = replyTo.Cw;
TextPlaceholder = AvailablePlaceholders["reply"];
foreach (var el in mentions)
{
NoteDraft.Text += $"@{el} ";
}
StateHasChanged();
}
else if (quote != null)
{
ResetState();
ReplyOrQuote = quote;
NoteDraft.RenoteId = quote.Id;
NoteDraft.Visibility = quote.Visibility;
TextPlaceholder = AvailablePlaceholders["quote"];
StateHasChanged();
}
else
{
ResetState();
StateHasChanged();
}
await _module.InvokeVoidAsync("openDialog", Dialog);
}
private List<string> EnumerateMentions(NoteBase noteBase)
{
List<string> 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 current = $"@{SessionService.Current.Username}@{SessionService.Current.Host}";
var mfmNodes = noteBase.Text != null ? Mfm.parse(noteBase.Text) : [];
foreach (var node in mfmNodes)
{
if (node is MfmNodeTypes.MfmMentionNode mentionNode)
{
mentions.Add(mentionNode.Acct);
}
}
mentions = mentions.Distinct().ToList();
mentions.Remove(current);
return mentions;
}
private void ResetState()
{
ReplyOrQuote = null;
Attachments = new List<DriveFileResponse>();
NoteDraft = new NoteCreateRequest
{
Text = "",
Visibility = NoteVisibility.Followers,
Cw = null
};
TextPlaceholder = AvailablePlaceholders["default"];
}
private async Task CloseDialog()
{
await _module.InvokeVoidAsync("closeDialog", Dialog);
}
private async Task SendNote()
{
SendButton.State = StateButton.StateEnum.Loading;
if (Attachments.Count > 0)
{
NoteDraft.MediaIds = Attachments.Select(x => x.Id).ToList();
}
try
{
await ApiService.Notes.CreateNoteAsync(NoteDraft);
}
catch (ApiException)
{
SendButton.State = StateButton.StateEnum.Failed;
return;
}
if (ReplyOrQuote != null)
{
var res = await ApiService.Notes.GetNoteAsync(ReplyOrQuote.Id);
if (res != null) _ = MessageService.UpdateNoteAsync(res);
}
SendButton.State = StateButton.StateEnum.Success;
await CloseDialog();
SendButton.State = StateButton.StateEnum.Success;
// FIXME: Implement timeline refresh and call it here.
}
private void ToggleCw()
{
NoteDraft.Cw = NoteDraft.Cw == null ? "" : null;
}
private async Task Upload(InputFileChangeEventArgs e)
{
var res = await ApiService.Drive.UploadFileAsync(e.File);
Attachments.Add(res);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await Js.InvokeAsync<IJSObjectReference>("import",
"./Components/Compose.razor.js");
ComposeService.ComposeDialog = this;
}
}
private void ToggleEmojiPicker()
{
GlobalComponentSvc.EmojiPicker?.Open(EmojiButton, new EventCallback<EmojiResponse>(this, AddEmoji));
}
private async Task AddEmoji(EmojiResponse emoji)
{
var pos = await _module.InvokeAsync<int>("getSelectionStart", Textarea);
var text = NoteDraft.Text;
var emojiString = $":{emoji.Name}: ";
NoteDraft.Text = text.Insert(pos, emojiString);
StateHasChanged();
}
protected override void OnInitialized()
{
TextPlaceholder = AvailablePlaceholders["default"];
}
}