574 lines
22 KiB
Text
574 lines
22 KiB
Text
@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<Localization> Loc;
|
|
@inject GlobalComponentSvc GlobalComponentSvc
|
|
@inject SettingsService Settings;
|
|
@inject NoteActions NoteActions;
|
|
<dialog @onkeydown="HandleKeyDown" class="dialog" @ref="Dialog">
|
|
<div class="compose">
|
|
<div class="header">
|
|
<button class="btn" title="@Loc["Close"]" @onclick="CloseDialog" aria-label="close" disabled="@(SendLock || UploadingFiles != 0)">
|
|
<Icon Name="Icons.X"/>
|
|
</button>
|
|
<Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/>
|
|
<StateButton OnClick="SendNote" @ref="SendButton" ExtraClasses="post-btn" AriaLabel="post"
|
|
Disabled="@SendingDisabled()">
|
|
<Initial>
|
|
@Loc["ComposeNote"]<Icon Name="Icons.PaperPlaneRight"/>
|
|
</Initial>
|
|
<Loading>
|
|
@Loc["Sending"]<LoadingSpinner />
|
|
</Loading>
|
|
<Success>
|
|
@Loc["Done"]<Icon Name="Icons.Check"/>
|
|
</Success>
|
|
<Failed>
|
|
@Loc["Retry"]<Icon Name="Icons.X"/>
|
|
</Failed>
|
|
</StateButton>
|
|
</div>
|
|
@if (ReplyOrQuote != null && !Preview)
|
|
{
|
|
<div class="reply-or-quote">
|
|
<NoteComponent Note="ReplyOrQuote" AsQuote="true" OpenNote="false"/>
|
|
</div>
|
|
}
|
|
@if (NoteDraft.Cw != null)
|
|
{
|
|
<input @bind="NoteDraft.Cw" @bind:event="oninput" class="input cw-field" placeholder="Content Warning"
|
|
aria-label="content warning"/>
|
|
<hr class="separator"/>
|
|
}
|
|
<textarea @ref="Textarea" @bind="NoteDraft.Text" @bind:event="oninput" class="textarea"
|
|
placeholder="@TextPlaceholder" rows="5"
|
|
cols="35" autofocus="autofocus" aria-label="note text"></textarea>
|
|
<div class="footer">
|
|
<div class="buttons">
|
|
<button class="btn" title="@Loc["Upload file"]" @onclick="OpenUpload" aria-label="upload">
|
|
<Icon Name="Icons.Upload" Size="1.3rem"></Icon>
|
|
</button>
|
|
<div class="file-input">
|
|
<InputFile @ref="UploadInput" OnChange="Upload">Upload!</InputFile>
|
|
</div>
|
|
<button class="btn" title="@Loc["Poll"]" @onclick="TogglePoll" aria-label="poll">
|
|
<Icon Name="Icons.MicrophoneStage" 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>
|
|
@if (NoteDraft.RenoteId == null && AttachedQuote == null)
|
|
{
|
|
<button class="btn" title="@Loc["Add quote"]" @onclick="AddQuote" aria-label="add quote">
|
|
<Icon Name="Icons.Quotes" Size="1.3rem"></Icon>
|
|
</button>
|
|
}
|
|
</div>
|
|
<div class="buttons">
|
|
<span title="@Loc["Character limit"]"
|
|
aria-label="character limit">@(NoteLength - ((NoteDraft.Cw?.Length ?? 0) + NoteDraft.Text.Length))</span>
|
|
<button class="btn" title="@Loc["Preview"]" @onclick="() => Preview = !Preview"
|
|
aria-label="preview">
|
|
<Icon Name="Icons.Binoculars" Size="1.3rem"></Icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@if (AttachedQuote != null)
|
|
{
|
|
<div>
|
|
<span>
|
|
<Icon Name="Icons.Quotes"/>
|
|
<a href="@($"/notes/{AttachedQuote}")" target="_blank">@Loc["Attached quote"]</a>
|
|
<button class="btn" title="@Loc["Remove quote"]" @onclick="RemoveQuote" aria-label="remove quote">
|
|
<Icon Name="Icons.Trash" Size="1rem"></Icon>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
}
|
|
@if (UploadingFiles != 0)
|
|
{
|
|
<div>
|
|
<span><LoadingSpinner/> @(UploadingFiles == 1 ? Loc["Uploading file"] : Loc["Uploading {0} files", UploadingFiles])</span>
|
|
</div>
|
|
}
|
|
@if (Attachments.Count != 0)
|
|
{
|
|
<div class="attachments">
|
|
@foreach (var attachment in Attachments)
|
|
{
|
|
<ComposeAttachment File="@attachment" AddInlineMedia="AddInlineMedia" RemoveAttachment="RemoveAttachment"/>
|
|
}
|
|
</div>
|
|
}
|
|
@if (NoteDraft.Poll != null)
|
|
{
|
|
<div class="poll">
|
|
<div class="choices">
|
|
@foreach (var entry in PollChoices)
|
|
{
|
|
<div class="choice">
|
|
<input class="input" placeholder="@Loc["Choice"]" @bind="@entry.Value"/>
|
|
<button class="button" title="@Loc["Delete Choice"]" @onclick="() => DeletePollChoice(entry)">
|
|
<Icon Name="Icons.Trash"/>
|
|
</button>
|
|
</div>
|
|
}
|
|
@if (PollChoices.Count < 10)
|
|
{
|
|
<div class="choice">
|
|
<input class="input" placeholder="@Loc["Choice"]" @bind="@PollEntry"/>
|
|
<button class="button" title="@Loc["Add Choice"]" @onclick="AddPollChoice">
|
|
<Icon Name="Icons.Plus"/>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
<input type="checkbox" id="poll-multiple" @bind="NoteDraft.Poll.Multiple"/>
|
|
<label for="poll-multiple">@Loc["Multiple choice"]</label>
|
|
<div class="expire">
|
|
<div>
|
|
<label for="poll-expires">@Loc["End poll"]</label>
|
|
<select @bind="PollExpires" id="poll-expires">
|
|
<option value="@PollExpire.Never">@Loc["Never"]</option>
|
|
<option value="@PollExpire.ExpiresAt">@Loc["End at"]</option>
|
|
<option value="@PollExpire.ExpiresAfter">@Loc["End after"]</option>
|
|
</select>
|
|
</div>
|
|
@switch (PollExpires)
|
|
{
|
|
case PollExpire.Never:
|
|
break;
|
|
case PollExpire.ExpiresAt:
|
|
<input type="datetime-local" class="input" @bind="PollExpireTime"/>
|
|
break;
|
|
case PollExpire.ExpiresAfter:
|
|
<input type="number" class="input" placeholder="@Loc["Duration"]" min="@(PollExpiresAft == PollExpireAfter.Minutes ? 5 : 1)" @bind="PollExpireLen"/>
|
|
<select @bind="PollExpiresAft">
|
|
<option value="@PollExpireAfter.Minutes">@Loc["Minutes"]</option>
|
|
<option value="@PollExpireAfter.Hours">@Loc["Hours"]</option>
|
|
<option value="@PollExpireAfter.Days">@Loc["Days"]</option>
|
|
</select>
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (Preview)
|
|
{
|
|
<div class="preview">
|
|
@if (string.IsNullOrWhiteSpace(NoteDraft.Text))
|
|
{
|
|
<span>@Loc["Nothing to preview"]</span>
|
|
}
|
|
else
|
|
{
|
|
<MfmText Text="@NoteDraft.Text.ReplaceLineEndings("\n")" Emoji="EmojiList"/>
|
|
}
|
|
</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 bool Preview { get; set; }
|
|
private List<EmojiResponse> 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 List<Choice> PollChoices { get; set; } = [];
|
|
private string PollEntry { get; set; } = "";
|
|
private PollExpire PollExpires { get; set; }
|
|
private DateTime PollExpireTime { get; set; }
|
|
private int PollExpireLen { get; set; }
|
|
private PollExpireAfter PollExpiresAft { get; set; }
|
|
|
|
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!" } };
|
|
|
|
private class Choice
|
|
{
|
|
public required string Value { get; set; }
|
|
}
|
|
|
|
private enum PollExpire
|
|
{
|
|
Never,
|
|
ExpiresAt,
|
|
ExpiresAfter
|
|
}
|
|
|
|
private enum PollExpireAfter
|
|
{
|
|
Minutes,
|
|
Hours,
|
|
Days,
|
|
}
|
|
|
|
private bool SendingDisabled() =>
|
|
NoteLength - ((NoteDraft.Cw?.Length ?? 0) + NoteDraft.Text.Length) < 0 || UploadingFiles != 0 || NoteDraft.Poll != null && PollChoices.Count(p => !string.IsNullOrWhiteSpace(p.Value)) < 2;
|
|
|
|
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 => (@<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)
|
|
{
|
|
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<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 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<DriveFileResponse>();
|
|
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;
|
|
|
|
if (NoteDraft.Poll != null)
|
|
{
|
|
NoteDraft.Poll.Choices = PollChoices.Select(p => p.Value).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
|
NoteDraft.Poll.ExpiresAt = PollExpires switch
|
|
{
|
|
PollExpire.ExpiresAt => PollExpireTime.ToUniversalTime(),
|
|
PollExpire.ExpiresAfter => PollExpiresAft switch
|
|
{
|
|
PollExpireAfter.Minutes => DateTime.UtcNow.AddMinutes(Math.Min(PollExpireLen, 5)),
|
|
PollExpireAfter.Hours => DateTime.UtcNow.AddHours(Math.Min(PollExpireLen, 1)),
|
|
PollExpireAfter.Days => DateTime.UtcNow.AddDays(Math.Min(PollExpireLen, 1)),
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
},
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
try
|
|
{
|
|
await ApiService.Notes.CreateNoteAsync(NoteDraft);
|
|
}
|
|
catch (ApiException e)
|
|
{
|
|
SendButton.State = StateButton.StateEnum.Failed;
|
|
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred while posting"], NoticeDialog.NoticeType.Error)!;
|
|
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 TogglePoll()
|
|
{
|
|
if (NoteDraft.Poll != null)
|
|
{
|
|
NoteDraft.Poll = null;
|
|
}
|
|
else
|
|
{
|
|
NoteDraft.Poll = new PollRequest
|
|
{
|
|
ExpiresAt = null,
|
|
Multiple = false,
|
|
Choices = []
|
|
};
|
|
PollChoices = [];
|
|
PollEntry = "";
|
|
PollExpires = PollExpire.Never;
|
|
PollExpireTime = DateTime.Now;
|
|
PollExpireLen = 7;
|
|
PollExpiresAft = PollExpireAfter.Days;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private void AddPollChoice()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(PollEntry)) return;
|
|
PollChoices.Add(new Choice { Value = PollEntry });
|
|
PollEntry = "";
|
|
}
|
|
|
|
private void DeletePollChoice(Choice choice)
|
|
{
|
|
PollChoices.Remove(choice);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
_module = await Js.InvokeAsync<IJSObjectReference>("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<EmojiResponse>(this, AddEmoji));
|
|
}
|
|
|
|
private async Task AddQuote() =>
|
|
await GlobalComponentSvc.PromptDialog?.Prompt(new EventCallback<string?>(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<int>("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<int>("getSelectionStart", Textarea);
|
|
var text = NoteDraft.Text;
|
|
var mediaString = $"$[media {url} ] ";
|
|
NoteDraft.Text = text.Insert(pos, mediaString);
|
|
StateHasChanged();
|
|
}
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
TextPlaceholder = AvailablePlaceholders["default"];
|
|
}
|
|
}
|