Files
decePubClient/Models/FeedPost.cs
T
thepraandClaude Opus 5.5 a4d9831691 Notifications in groups, and what is unread
The Notifications tab reads grouped notifications (/api/v2/notifications):
likes and boosts of one post, or follows of one hour, are one card naming
both accounts of a pair, or the newest and how many others, with their
avatars. A notification the stream tells joins its group and waits on top.
The tab's badge counts what is unread; opening the tab moves the
notifications marker to the newest shown, which reads it on PrivaPub.

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

149 lines
6.2 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; set; }
/// <summary>For a notification standing for a group (likes of one post, follows of one hour): how many, and a few of
/// the accounts, newest first.</summary>
public int GroupCount { get; set; } = 1;
public List<Account> GroupAccounts { get; set; } = [];
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.GroupKey ?? notification.Id,
CursorId = notification.Id,
Status = notification.Status?.Reblog ?? notification.Status,
Notification = notification,
GroupAccounts = notification.Account is null ? [] : [notification.Account]
};
/// <summary>A group of notifications as one item: its newest notification, how many it holds, and a few accounts.</summary>
public static FeedPost Of(NotificationGroup group, IReadOnlyDictionary<string, Account> accounts, IReadOnlyDictionary<string, Status> statuses)
{
var sample = group.SampleAccountIds.Select(accounts.GetValueOrDefault).Where(a => a is not null).ToList();
var status = group.StatusId is null ? null : statuses.GetValueOrDefault(group.StatusId);
return new()
{
Key = group.GroupKey,
CursorId = group.MostRecentNotificationId,
Status = status?.Reblog ?? status,
Notification = new Notification
{
Id = group.MostRecentNotificationId,
Type = group.Type,
CreatedAt = group.LatestPageNotificationAt,
Account = sample.FirstOrDefault(),
Status = status,
Emoji = group.Emoji,
GroupKey = group.GroupKey
},
GroupCount = Math.Max(1, group.NotificationsCount),
GroupAccounts = sample
};
}
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; }
}