The Sun lights the globe where it really is (wwwroot/js/sun.js, the Astronomical Almanac's low-precision solar coordinates): the day map shaded by the Sun's height and the Earth–Sun distance, warm city lights on the night side, and a twilight blend about 13° wide between them. It shows now, refreshed every 30 s, or the moment of the hovered, focused or pinned post: a click on a card pins it, and the subsolar point travels there the shorter way round. The panel's top corner shows that moment, the phase of the day and solar time at the post's place, and the season in its hemisphere (or in both, for a post without a place). Its tooltip shows the subsolar point, declination, axial tilt, distance, irradiance, day length and the next equinox or solstice. Land borders are thin paths. Country names are in the reader's language and appear as the camera comes closer: largest countries first, away from the globe's edge, never overlapping. They come from world-atlas 2.0.2 (Natural Earth 1:50m), prepared by tools/globe/prepare-countries.py, and are vendored with their hashes in SOURCES.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
119 lines
4.9 KiB
C#
119 lines
4.9 KiB
C#
using decePubClient.Models.Mastodon;
|
|
|
|
namespace decePubClient.Models;
|
|
|
|
/// <summary>
|
|
/// One item of a feed, whatever its source: a status (a boost shows its original, BoostedBy names the booster), a
|
|
/// notification, a direct conversation, or a located post from /clientapi/post/nearby (Located, with no Mastodon
|
|
/// actions: PrivaPub keeps those posts off the Mastodon API). Location is filled by InstanceService once the author's
|
|
/// server is known.
|
|
/// </summary>
|
|
public class FeedPost
|
|
{
|
|
/// <summary>Unique within a feed: the status, notification or conversation id.</summary>
|
|
public string Key { get; init; }
|
|
/// <summary>The id the feed pages by (max_id/min_id).</summary>
|
|
public string CursorId { get; init; }
|
|
/// <summary>The post shown; null for a notification without one (a follow).</summary>
|
|
public Status Status { get; set; }
|
|
public Account BoostedBy { get; init; }
|
|
public Notification Notification { get; init; }
|
|
public Conversation Conversation { get; init; }
|
|
/// <summary>A located post: within its range of the reader, never on the Mastodon API.</summary>
|
|
public bool Located { get; init; }
|
|
public double? DistanceKm { get; init; }
|
|
public double? RangeKm { get; init; }
|
|
public FeedLocation Location { get; set; }
|
|
|
|
/// <summary>The account the item is about: the post's author, or whoever caused the notification.</summary>
|
|
public Account Author => Status?.Account ?? Notification?.Account;
|
|
|
|
public string Host => Author?.Host;
|
|
|
|
/// <summary>When the post was written (for a follow, when it happened), in UTC; the globe shows the Sun of that moment.</summary>
|
|
public DateTime? WrittenAt => (Status?.CreatedAt ?? Notification?.CreatedAt) is { } at
|
|
? (at.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(at, DateTimeKind.Utc) : at.ToUniversalTime())
|
|
: null;
|
|
|
|
public bool IsOwnedBy(string personaId) => personaId is not null && Status?.Account?.Id == personaId;
|
|
|
|
public static FeedPost Of(Status status) => status.Reblog is { } original
|
|
? new() { Key = status.Id, CursorId = status.Id, Status = original, BoostedBy = status.Account }
|
|
: new() { Key = status.Id, CursorId = status.Id, Status = status };
|
|
|
|
public static FeedPost Of(Notification notification) => new()
|
|
{
|
|
Key = notification.Id,
|
|
CursorId = notification.Id,
|
|
Status = notification.Status?.Reblog ?? notification.Status,
|
|
Notification = notification
|
|
};
|
|
|
|
public static FeedPost Of(Conversation conversation) => new()
|
|
{
|
|
Key = conversation.Id,
|
|
CursorId = conversation.LastStatus?.Id,
|
|
Status = conversation.LastStatus,
|
|
Conversation = conversation
|
|
};
|
|
}
|
|
|
|
/// <summary>Where a feed item is drawn on the globe, and how sure that is.</summary>
|
|
public enum LocationSource
|
|
{
|
|
/// <summary>The post's own place (Pixelfed): exact.</summary>
|
|
Place,
|
|
/// <summary>An event's venue: exact.</summary>
|
|
Event,
|
|
/// <summary>The author's server, to 0.1°.</summary>
|
|
Server,
|
|
/// <summary>The author's server, known only by country: a point in that country.</summary>
|
|
Country,
|
|
/// <summary>A located post, within its range of the reader: drawn at the reader.</summary>
|
|
NearYou,
|
|
/// <summary>The author's server is behind a CDN, whose address says nothing of its place: drawn with the CDN's group.</summary>
|
|
Cdn
|
|
}
|
|
|
|
/// <summary>
|
|
/// Where a feed item is drawn. For a CDN-fronted server (Source Cdn) Latitude/Longitude mean nothing: the globe groups it
|
|
/// under CdnKey, and Before is where the server was before the CDN, when PrivaPub saw it.
|
|
/// </summary>
|
|
public sealed record FeedLocation(double Latitude, double Longitude, LocationSource Source, string Label, double PrecisionKm, string CdnKey = default,
|
|
InstancePlace Before = default)
|
|
{
|
|
public bool HasPoint => Source is not LocationSource.Cdn;
|
|
}
|
|
|
|
/// <summary>What the client keeps about a server (from /api/privapub/v1/instances), cached for a day.</summary>
|
|
public class ServerInfo
|
|
{
|
|
public string Host { get; set; }
|
|
public string Software { get; set; }
|
|
public string Version { get; set; }
|
|
public string NodeName { get; set; }
|
|
public bool? OpenRegistrations { get; set; }
|
|
public long? Users { get; set; }
|
|
public long? ActiveMonth { get; set; }
|
|
public long? LocalPosts { get; set; }
|
|
public InstanceLocation Geo { get; set; }
|
|
public int ConsecutiveFailures { get; set; }
|
|
public DateTime? LastSuccessAt { get; set; }
|
|
public DateTime? LastFailureAt { get; set; }
|
|
public DateTime? DescribedAt { get; set; }
|
|
public string DescriptionError { get; set; }
|
|
public DateTime FetchedAt { get; set; }
|
|
/// <summary>True when PrivaPub did not know the server: nothing to show, asked again later.</summary>
|
|
public bool Unknown { get; set; }
|
|
}
|
|
|
|
/// <summary>A page of a feed and the cursors around it.</summary>
|
|
public class FeedPage
|
|
{
|
|
public List<FeedPost> Posts { get; set; } = [];
|
|
/// <summary>Older items start below this id; null at the end of the feed.</summary>
|
|
public string MaxId { get; set; }
|
|
/// <summary>Newer items start above this id.</summary>
|
|
public string MinId { get; set; }
|
|
}
|