Files
decePubClient/Models/FeedPost.cs
T
thepraandClaude Opus 5.5 827ecb770e Wire the client to PrivaPub: one sign-in, real feeds, a globe, nerd stats
One sign-in for both of PrivaPub's APIs: the root logs in on /clientapi,
picks a persona, and each persona gets its own Mastodon token in
exchange for the JWT (PersonaExchange). HttpService picks credentials
and JSON by route (JWT and camelCase for /clientapi, the persona token
and snake_case for /api, nothing for /oauth), retries a refused persona
token once, and records every request for the nerd stats.

The feed page is three columns: navigation with a persona switcher, the
feed with a tab per kind (home, local, federated, nearby, communities
and circles, direct, notifications, favourites, bookmarks, hashtags),
and a 3D globe that looks at the spherical mean of the posts in view
and draws a pulse and an arc to a hovered one. Posts sit at their own
place, their event's venue, the reader (located posts) or their
author's server as PrivaPub publishes it. Cards favourite, boost,
bookmark, reply, mute, block and delete; the composer posts with a
content warning, visibility, media and alt text, or as a located post.
Older pages load as the end nears; newer ones are polled while visible.
Nerd stats in tooltips: provenance, author and server, place, media,
counts, paging, the globe's camera and frame rate.

The mock (Faker, MessagesService, the Message models and store) and the
OIDC template leftovers are gone. Strings in English and Italian.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-04 11:03:14 +02:00

104 lines
4.0 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;
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
}
public sealed record FeedLocation(double Latitude, double Longitude, LocationSource Source, string Label, double PrecisionKm);
/// <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; }
}