@inherits LocalizableComponentBase @* A new post or a reply, as the current persona. Through the Mastodon API it is plain text with a content warning, a visibility and media (each uploaded first, with its alt text). A located post is text only, kept within its range of this device's position and never federated; a group post goes to the group. Both of those go through /clientapi. *@ @if (form.HasContentWarning) { }
@ContentLength/@ComposeForm.MaxContentLength
@if (form.Media.Count > 0) {
@foreach (var media in form.Media) {
@if (media.Base64Preview is { Length: > 0 } preview && media.ContentType.StartsWith("image")) { @media.AltText } else { }

@media.FileName · @media.ContentType · @media.Size.GetFileSize(Localizer)

}
} @if (form.IsLocated) {
@if (form.Latitude is null) { @Localizer["Asking this device for its position…"] } else { @Localizer["Shown only to people within {0} km of here, never sent to other servers.", form.RangeKm.ToString("0")] }
}
@if (!form.IsLocated && GroupId is null) { @foreach (var messageType in Enum.GetValues()) { } } @if (!form.IsLocated && GroupId is null) { @* A label styled as a button opens the hidden file input. *@ @foreach (var mediaType in new[] { MediaType.Images, MediaType.Video, MediaType.Audio }) { } } @if (InReplyTo is null && GroupId is null) { }
@if (isPublishing) { } @Localizer[InReplyTo is null ? "Post" : "Reply"]
@code { [Inject] StatusService StatusService { get; set; } [Inject] ILoggingService Logger { get; set; } [Inject] IJSRuntime JS { get; set; } /// The status this answers; the reply starts with its visibility (never wider) and content warning. [Parameter] public Status InReplyTo { get; set; } /// The community or circle the post is for. [Parameter] public string GroupId { get; set; } /// The published post, to show it; a located or group post comes back as null (it is not on the Mastodon API). [Parameter] public EventCallback OnPublished { get; set; } ComposeForm form = new(); List fileErrors = []; string error; bool isPublishing; int ContentLength => form.Content?.Length ?? 0; int MaximumFileCount => form.MediaType is MediaType.Images ? 4 : 1; long MaximumFileSize => form.MediaType switch { MediaType.Images => 16_777_216, MediaType.Audio => 41_943_040, _ => 99_614_720 }; string AcceptedFileTypes => form.MediaType switch { MediaType.Images => ".jpg,.jpeg,.png,.gif,.webp,.avif", MediaType.Video => ".mp4,.m4v,.webm,.mov", MediaType.Audio => ".mp3,.ogg,.oga,.opus,.wav,.flac,.m4a", _ => default }; protected override void OnInitialized() { IsLoading = false; form.GroupId = GroupId; if (InReplyTo is null) return; form.InReplyToId = InReplyTo.Id; form.MessageType = InReplyTo.Visibility switch { "direct" => MessageType.Direct, "private" => MessageType.FollowersOnly, "unlisted" => MessageType.Unlisted, _ => MessageType.Public }; form.HasContentWarning = InReplyTo.SpoilerText is { Length: > 0 }; form.SpoilerText = InReplyTo.SpoilerText; var mentions = InReplyTo.Mentions.Select(m => m.Acct).Prepend(InReplyTo.Account?.Acct) .Where(acct => acct is not null && acct != CascadingState.Persona?.Acct).Distinct().Select(acct => "@" + acct); form.Content = string.Join(' ', mentions) is { Length: > 0 } prefix ? prefix + " " : null; } void SetMessageType(MessageType messageType) { form.MessageType = messageType; form.IsScopeOptionsOpen = false; } async Task ToggleLocated() { form.IsLocated = !form.IsLocated; if (!form.IsLocated || form.Latitude is not null) return; var position = await JS.InvokeAsync("feedPosition"); if (position?.Error is { Length: > 0 } positionError) { form.IsLocated = false; error = Localizer["This device did not give its position: {0}", positionError]; return; } form.Latitude = position?.Latitude; form.Longitude = position?.Longitude; } async Task OnFileChange(InputFileChangeEventArgs eventArgs) { fileErrors = []; if (eventArgs.FileCount + form.Media.Count > MaximumFileCount) { fileErrors.Add(Localizer["The maximum number of files accepted is {0}, but {1} were supplied.", MaximumFileCount, form.Media.Count + eventArgs.FileCount]); return; } foreach (var file in eventArgs.GetMultipleFiles(MaximumFileCount)) { if (file.Size > MaximumFileSize) { fileErrors.Add(Localizer["Supplied file \"{0}\" with size {1} exceeds the maximum of {2}.", file.Name, file.Size.GetFileSize(Localizer), MaximumFileSize.GetFileSize(Localizer)]); continue; } if (await ReadFile(file) is { } media) form.Media.Add(media); else fileErrors.Add(Localizer["The file \"{0}\" could not be read.", file.Name]); } } /// Reads a picked file into memory; null when the browser can't read it (logged). async Task ReadFile(IBrowserFile file, CancellationToken cancellationToken = default) { try { using var memoryStream = new MemoryStream(); await using var fileStream = file.OpenReadStream(MaximumFileSize, cancellationToken); await fileStream.CopyToAsync(memoryStream, cancellationToken); var media = new UploadMedia { FileName = file.Name, ContentType = file.ContentType, Size = file.Size, Blob = memoryStream.ToArray() }; if (file.ContentType.StartsWith("image")) media.Base64Preview = $"data:{media.ContentType};base64,{Convert.ToBase64String(media.Blob)}"; return media; } catch (Exception ex) { await Logger.ProcessError(ex, $"{nameof(Composer)}.{nameof(ReadFile)}:{file.Name}", cancellationToken); return default; } } async Task Publish() { error = default; if (string.IsNullOrWhiteSpace(form.Content) && form.Media.Count == 0) { error = Localizer["Missing content, either message or media"]; return; } if (CascadingState.PersonaId is null) { error = Localizer["Choose a persona first."]; return; } isPublishing = true; var result = await StatusService.Publish(form, CascadingState.PersonaId); isPublishing = false; if (result.IsInvalid) { error = result.ErrorMessage; return; } Toast.AddResponseSuccess(form.IsLocated ? Localizer["Located post published."] : Localizer["Published."]); var published = result.Data as FeedPost; form = new() { GroupId = GroupId }; await OnPublished.InvokeAsync(published); } sealed class GeoPosition { public double? Latitude { get; set; } public double? Longitude { get; set; } public double? Accuracy { get; set; } public string Error { get; set; } } }