Media survive failures: uploaded when attached, the server's limits, alt text asked for
The composer uploads each file as soon as it is attached (streamed from the browser file, under its own ten-minute
timeout), shows it uploading, processing (a 202 followed until ready), ready or failed with Retry, and cancels one
removed; publishing sends the ids, so a failed publish or a retried upload sends nothing twice. Size limits and types
come from /api/v2/instance, a file without a browser type gets one from its extension, and each refusal names the file
and why. Previews are 320 px thumbnails, not whole files in memory. A picture without a description is asked about once
("Publish anyway"; a setting turns it off), descriptions are sent with PUT, and an edit keeps the post's media, language
and sensitive flag.
Profile pictures are checked against the server's types and can be removed. A card shows "Media unavailable" with the
original's link when a picture fails, the blurhash behind a sensitive placeholder, and plays a GIF only on tap under
reduced motion. A failed export says so instead of downloading part of a file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
9d662f702b
commit
70cb389537
26 files changed
+883
-132
No files matched your search
+33
-13
@@ -33,27 +33,25 @@ namespace decePubClient.Services
|
||||
/// </summary>
|
||||
public class StatusService(IHttpService httpService, MediaService mediaService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
||||
{
|
||||
/// <summary>Publishes the form as the persona; its media are uploaded first.</summary>
|
||||
/// <summary>Publishes the form as the persona. Its media are uploaded already (the composer uploads a file as soon as
|
||||
/// it is picked); a description changed since is sent first.</summary>
|
||||
public async Task<WebResult> Publish(ComposeForm form, string personaId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (form.IsLocated || form.GroupId is not null)
|
||||
return await PublishThroughClientApi(form, personaId, cancellationToken);
|
||||
|
||||
var mediaIds = new List<string>();
|
||||
foreach (var media in form.Media)
|
||||
{
|
||||
var uploaded = await mediaService.Upload(media, cancellationToken);
|
||||
if (uploaded.IsInvalid)
|
||||
return uploaded;
|
||||
mediaIds.Add(((MediaAttachment)uploaded.Data).Id);
|
||||
}
|
||||
if (form.Media.Any(m => m.State != UploadState.Ready))
|
||||
return new WebResult().Invalidate(localizer["Wait for the files to finish uploading, or remove the ones that failed."]);
|
||||
if (await Describe(form, cancellationToken) is { IsInvalid: true } undescribed)
|
||||
return undescribed;
|
||||
var mediaIds = form.Media.Select(m => m.Id).ToList();
|
||||
|
||||
using var response = await httpService.Post(APIs.Mastodon.POST_Statuses, new PostStatusForm
|
||||
{
|
||||
Status = form.Content ?? string.Empty,
|
||||
MediaIds = mediaIds,
|
||||
InReplyToId = form.InReplyToId,
|
||||
Sensitive = form.HasContentWarning,
|
||||
Sensitive = form.HasContentWarning || form.Sensitive && form.Media.Count > 0,
|
||||
SpoilerText = form.HasContentWarning ? form.SpoilerText : null,
|
||||
Visibility = form.Visibility,
|
||||
Language = form.Language ?? CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
|
||||
@@ -70,6 +68,20 @@ namespace decePubClient.Services
|
||||
return new WebResult { Data = FeedPost.Of(status) };
|
||||
}
|
||||
|
||||
// a new upload whose description changed after it was picked tells PrivaPub before the post (a posted one is
|
||||
// described again through the edit's media_attributes)
|
||||
async Task<WebResult> Describe(ComposeForm form, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var media in form.Media.Where(m => !m.Posted && m.AltText != m.SentAltText))
|
||||
{
|
||||
var described = await mediaService.Describe(media.Id, media.AltText, cancellationToken);
|
||||
if (described.IsInvalid)
|
||||
return described;
|
||||
media.SentAltText = media.AltText;
|
||||
}
|
||||
return new WebResult();
|
||||
}
|
||||
|
||||
/// <summary>The persona's own text of a post, to edit it; null when it cannot be read.</summary>
|
||||
public async Task<StatusSource> Source(string statusId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -104,15 +116,23 @@ namespace decePubClient.Services
|
||||
return await response.MastodonReadFromJsonAsync<List<Account>>(cancellationToken) ?? [];
|
||||
}
|
||||
|
||||
/// <summary>Edits the persona's own post: new words and warning, the media kept; Data is the edited Status.</summary>
|
||||
/// <summary>Edits the persona's own post, sent whole as Mastodon wants it: its words and warning, its sensitive flag
|
||||
/// and language as they were unless changed, the media it keeps (new ones uploaded already) and the descriptions
|
||||
/// changed; Data is the edited Status.</summary>
|
||||
public async Task<WebResult> Edit(string statusId, ComposeForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (form.Media.Any(m => m.State != UploadState.Ready))
|
||||
return new WebResult().Invalidate(localizer["Wait for the files to finish uploading, or remove the ones that failed."]);
|
||||
if (await Describe(form, cancellationToken) is { IsInvalid: true } undescribed)
|
||||
return undescribed;
|
||||
using var response = await httpService.Put(APIs.Mastodon.PUT_M_Status(statusId), new StatusEditForm
|
||||
{
|
||||
Status = form.Content ?? string.Empty,
|
||||
SpoilerText = form.HasContentWarning ? form.SpoilerText : null,
|
||||
Sensitive = form.HasContentWarning,
|
||||
Language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName
|
||||
Sensitive = form.HasContentWarning || form.Sensitive && form.Media.Count > 0,
|
||||
Language = form.Language ?? CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
|
||||
MediaIds = [.. form.Media.Select(m => m.Id)],
|
||||
MediaAttributes = [.. form.Media.Where(m => m.Posted && m.AltText != m.SentAltText).Select(m => new MediaAttributeForm { Id = m.Id, Description = m.AltText ?? string.Empty })]
|
||||
}, cancellationToken: cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
|
||||
Reference in new issue
Block a user