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
76 lines
3.3 KiB
C#
76 lines
3.3 KiB
C#
using System.ComponentModel;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Text.Json.Serialization;
|
|
|
|
using decePubClient.Models.Types;
|
|
using decePubClient.Resources;
|
|
|
|
namespace decePubClient.Models;
|
|
|
|
/// <summary>
|
|
/// A new post or reply. It goes through the Mastodon API (plain text, a content warning, a visibility and media) unless
|
|
/// it is located (text only, kept within RangeKm of where it was written, never federated) or meant for a group; those
|
|
/// go through /clientapi/post/insert.
|
|
/// </summary>
|
|
public class ComposeForm
|
|
{
|
|
public const int MaxContentLength = 5_000;
|
|
|
|
/// <summary>The status a reply answers.</summary>
|
|
public string InReplyToId { get; set; }
|
|
/// <summary>The community or circle the post is for.</summary>
|
|
public string GroupId { get; set; }
|
|
|
|
[Required(ErrorMessageResourceName = ValidationNames.Required, ErrorMessageResourceType = typeof(ErrorMessages))]
|
|
public MessageType MessageType { get; set; } = MessageType.Public;
|
|
public bool HasContentWarning { get; set; }
|
|
// its media marked sensitive without a warning: the persona's default ("always mark media as sensitive"), or the post's own when edited
|
|
public bool Sensitive { get; set; }
|
|
[StringLength(500, ErrorMessageResourceName = ValidationNames.MaxLength, ErrorMessageResourceType = typeof(ErrorMessages))]
|
|
public string SpoilerText { get; set; }
|
|
[StringLength(MaxContentLength, ErrorMessageResourceName = ValidationNames.MaxLength, ErrorMessageResourceType = typeof(ErrorMessages))]
|
|
public string Content { get; set; }
|
|
public List<UploadMedia> Media { get; set; } = [];
|
|
|
|
/// <summary>A located post: shown only to readers within RangeKm of Latitude/Longitude.</summary>
|
|
public bool IsLocated { get; set; }
|
|
[Range(1, 50)]
|
|
public double RangeKm { get; set; } = 5;
|
|
public double? Latitude { get; set; }
|
|
public double? Longitude { get; set; }
|
|
|
|
/// <summary>A poll instead of media: two to four choices, open ExpiresIn seconds, one choice or several.</summary>
|
|
public bool HasPoll { get; set; }
|
|
public List<string> PollOptions { get; set; } = ["", ""];
|
|
public int PollExpiresIn { get; set; } = 86400;
|
|
public bool PollMultiple { get; set; }
|
|
|
|
/// <summary>The language the post is written in (ISO 639-1); null leaves it to PrivaPub.</summary>
|
|
public string Language { get; set; }
|
|
|
|
/// <summary>The post this one quotes.</summary>
|
|
public string QuotedStatusId { get; set; }
|
|
|
|
/// <summary>Whether the post waits for ScheduledAt (a datetime-local value, the reader's own time) instead of going now.</summary>
|
|
public bool IsScheduled { get; set; }
|
|
public string ScheduledAt { get; set; }
|
|
|
|
/// <summary>When the post is to be published, in UTC, or null when it goes now or ScheduledAt is not a time.</summary>
|
|
public DateTime? ScheduledAtUtc => IsScheduled && DateTime.TryParse(ScheduledAt, System.Globalization.CultureInfo.InvariantCulture,
|
|
System.Globalization.DateTimeStyles.AssumeLocal, out var local) ? local.ToUniversalTime() : null;
|
|
|
|
[JsonIgnore, Bindable(false)]
|
|
public MediaType MediaType { get; set; } = MediaType.Images;
|
|
[JsonIgnore, Bindable(false)]
|
|
public bool IsScopeOptionsOpen { get; set; }
|
|
|
|
/// <summary>The Mastodon visibility of MessageType.</summary>
|
|
public string Visibility => MessageType switch
|
|
{
|
|
MessageType.Direct => "direct",
|
|
MessageType.FollowersOnly => "private",
|
|
MessageType.Unlisted => "unlisted",
|
|
_ => "public"
|
|
};
|
|
}
|