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
58 lines
2.4 KiB
C#
58 lines
2.4 KiB
C#
using System.Globalization;
|
|
|
|
namespace decePubClient.Components.Nerd;
|
|
|
|
/// <summary>One line of a NerdTip; a row without a value is not shown.</summary>
|
|
public sealed record NerdRow(string Label, string Value)
|
|
{
|
|
public static NerdRow Of(string label, object value) => new(label, value switch
|
|
{
|
|
null => null,
|
|
DateTime time => Time(time),
|
|
double number => number.ToString("0.###", CultureInfo.InvariantCulture),
|
|
bool flag => flag ? "yes" : "no",
|
|
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
|
_ => value.ToString()
|
|
});
|
|
|
|
/// <summary>A UTC time to the millisecond, as PrivaPub records it.</summary>
|
|
public static string Time(DateTime time) =>
|
|
DateTime.SpecifyKind(time, time.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : time.Kind).ToUniversalTime()
|
|
.ToString("yyyy-MM-dd HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
|
|
|
|
/// <summary>A duration the way a person reads it: 850 ms, 12.4 s, 3 min 5 s, 2 h 10 min, 4 d 3 h.</summary>
|
|
public static string Duration(TimeSpan span)
|
|
{
|
|
var sign = span < TimeSpan.Zero ? "-" : string.Empty;
|
|
span = span.Duration();
|
|
return sign + (span.TotalSeconds switch
|
|
{
|
|
< 1 => $"{span.TotalMilliseconds:0} ms",
|
|
< 60 => $"{span.TotalSeconds.ToString("0.0", CultureInfo.InvariantCulture)} s",
|
|
< 3600 => $"{span.Minutes} min {span.Seconds} s",
|
|
< 86400 => $"{(int)span.TotalHours} h {span.Minutes} min",
|
|
_ => $"{(int)span.TotalDays} d {span.Hours} h"
|
|
});
|
|
}
|
|
|
|
/// <summary>A byte count: 912 B, 14.2 KiB, 3.1 MiB.</summary>
|
|
public static string Bytes(long? bytes) => bytes switch
|
|
{
|
|
null => null,
|
|
< 1024 => $"{bytes} B",
|
|
< 1024 * 1024 => $"{(bytes.Value / 1024d).ToString("0.0", CultureInfo.InvariantCulture)} KiB",
|
|
_ => $"{(bytes.Value / 1048576d).ToString("0.0", CultureInfo.InvariantCulture)} MiB"
|
|
};
|
|
|
|
/// <summary>The great-circle distance between two points in kilometres (haversine, Earth radius 6371 km).</summary>
|
|
public static double Kilometres(double latitude1, double longitude1, double latitude2, double longitude2)
|
|
{
|
|
const double radius = 6371;
|
|
double Radians(double degrees) => degrees * Math.PI / 180;
|
|
var dLatitude = Radians(latitude2 - latitude1);
|
|
var dLongitude = Radians(longitude2 - longitude1);
|
|
var a = Math.Pow(Math.Sin(dLatitude / 2), 2) + Math.Cos(Radians(latitude1)) * Math.Cos(Radians(latitude2)) * Math.Pow(Math.Sin(dLongitude / 2), 2);
|
|
return 2 * radius * Math.Asin(Math.Min(1, Math.Sqrt(a)));
|
|
}
|
|
}
|