House rules: numbers through one formatter, no made-up server errors, and the rules fail the build
Build / Build (push) Successful in 33s
Build / Build (push) Successful in 33s
- Every number shown on a page goes through NumbersExtensions: counts, percentages, the nerd readouts and the values written into a style. - A failure the client builds itself (offline, timeout, retries run out, an answer it can't read) is a 400 that names its kind; only a server's own 5XX passes through. - Login, sign-up, recovery and password confirmations leave autocomplete to the browser, so it can offer the saved login. - The Daisy components start with empty lists and attributes, and a decorative image still renders alt="". - Arasaka.HouseRules.Analyzers checks all of this in every build, together with PrivaPub.ClientModels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
This commit is contained in:
1 parent
697b2c8897
commit
3827d903a3
37 files changed
+165
-111
No files matched your search
@@ -849,22 +849,24 @@ in both builds, kept out of the precache; see **Web Push**): `push` shows the no
|
||||
the app on that persona's notifications, `pushsubscriptionchange` subscribes again with the same key. They control no
|
||||
page and have no fetch handler.
|
||||
|
||||
## Development rules
|
||||
## House rules
|
||||
|
||||
These rules are adapted from the owner's b2b.next project. They are enforced by review: the build doesn't check them,
|
||||
so follow them in every change.
|
||||
The owner's rules for every .NET project (`~/RiderProjects/HouseRules/RULES.md`, repo `Arasaka/HouseRules`) apply
|
||||
here and win over anything else in this file. `Arasaka.HouseRules.Analyzers` (`Directory.Build.props`) reports the
|
||||
ones with an HR id as errors in every build and in Rider, here and in `PrivaPub.ClientModels`; the rest are reviewed.
|
||||
|
||||
| Area | Rule |
|
||||
|---|---|
|
||||
| All | `var` everywhere; uninitialised locals are `var x = default(T);` |
|
||||
| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields |
|
||||
| All | Every async method takes a `CancellationToken` as its last parameter |
|
||||
| All | `var` everywhere; uninitialised locals are `var x = default(T);` (HR0001) |
|
||||
| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields (HR0004) |
|
||||
| All | Every async method takes a `CancellationToken` as its last parameter (HR0003) |
|
||||
| All | Early return on error: guard, handle, `return`; never if/else for success vs failure |
|
||||
| Models / state | Lists and arrays `= []`, never null; no `= string.Empty` / `""` defaults; explicit enum defaults |
|
||||
| Components | No `@{ }` blocks in markup: locals become `@code` members |
|
||||
| Components | `[Inject]` in `@code`, never `@inject` |
|
||||
| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state |
|
||||
| Components | Every text/number input declares `autocomplete` |
|
||||
| Models / state | Lists and arrays `= []`, never null (HR0006); no `= string.Empty` / `""` defaults (HR0005); explicit enum defaults |
|
||||
| Components | No `@{ }` blocks in markup: locals become `@code` members (HR0102) |
|
||||
| Components | `[Inject]` in `@code`, never `@inject` (HR0101) |
|
||||
| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state (HR0104) |
|
||||
| Components | Every text/number input declares `autocomplete`, except credentials (HR0103, HR0105) |
|
||||
| Services | A failure the client builds itself is a 400 that names its kind in `ErrorCode` (`FailureCodes`), never a 5XX; only a server's own 5XX passes through (HR0007) |
|
||||
| Services | `Task<WebResult>` for mutations, `Task<List<T>>` (`[]` on error) for lists, `Task<T>` with a default for values |
|
||||
| Localization | Every user-facing string through `Localizer`, its key in **both** `AllStrings.resx` and `AllStrings.it.resx` |
|
||||
|
||||
@@ -881,7 +883,7 @@ List<FeedPost> posts = []; // DO
|
||||
@(posts?.Count ?? 0) // DON'T
|
||||
```
|
||||
|
||||
`= ""` is allowed only as a documented exception, with the reason in a comment (e.g. a decorative image's `Alt`).
|
||||
A string that must render as `""` coalesces in the markup instead: a decorative image is `alt="@(Alt ?? "")"`.
|
||||
|
||||
**Pages that load data.** Pages fetch in `LoadData` and keep lifecycle concerns in the lifecycle method:
|
||||
|
||||
@@ -930,22 +932,27 @@ join a list into one string with `<br/>` or `\n`.
|
||||
|
||||
**Autocomplete values:**
|
||||
- `autocomplete="off"` for search, filter and free text;
|
||||
- `new-password` when setting a password;
|
||||
- `new-password` when setting a password; `off` on a password field is wrong, since it doesn't stop saved passwords
|
||||
(HR0105);
|
||||
- `one-time-code` for OTP (DOtp sets it);
|
||||
- `username` and `current-password` on the login form, so password managers fill it;
|
||||
- omitted on login credentials, contact e-mails and password confirmations, so the browser can help: the files are
|
||||
listed in `HouseRulesAutocompleteOmit` (Login, Join, Setup, PasswordRecovery, AccountEditor, TotpEditor, Groups,
|
||||
ActDialog, BackupsPanel);
|
||||
- inside `Components/Daisy`, attributes pass through from the caller.
|
||||
|
||||
**Localization:**
|
||||
- Keys are English source strings. Grep both resx files for an existing key before adding one; match keys exactly.
|
||||
- Never add a key that differs from an existing one only by case ("users" next to "Users"): resource lookups collide
|
||||
on them and one of the two silently shows the other's text. Reuse the existing key.
|
||||
- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value`.
|
||||
- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value` (HR0009).
|
||||
- Enum members shown through `Localizer[value.ToString()]` are keys too: when you add a member, add its key.
|
||||
- Language names in the language picker stay in their own language.
|
||||
|
||||
**Formatting.** Date formats come from `SUtility.dateTimeFormat` / `timeFormat` / `dateTimeFormatExt`. Relative times
|
||||
and sizes come from `GetPassedTime` / `GetFileSize` (`Extensions/GenericExtensions.cs`). No inline format strings for
|
||||
display.
|
||||
**Formatting.** Numbers go through `NumbersExtensions` (`Extensions/NumbersExtensions.cs`, HR0011): `ToDefaultCount`,
|
||||
`Compact` (999, 1.2k), `ToDefaultPercent`, `ToWholePercent`, `ToSignedPercent`, `ToReadout` (nerd tips), `ToFixed`,
|
||||
and `ToCssNumber` for values written into a style. Date formats come from `SUtility.dateTimeFormat` / `timeFormat` /
|
||||
`dateTimeFormatExt`. Relative times and sizes come from `GetPassedTime` / `GetFileSize`
|
||||
(`Extensions/GenericExtensions.cs`). No inline format strings for display.
|
||||
|
||||
**JSON.** Register every `/clientapi` or localStorage type in `ClientJsonContext.cs` (camelCase: `theme.js` reads
|
||||
`pageSettings` straight from localStorage), every Mastodon API type in `MastodonJsonContext.cs` (snake_case).
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
@if (AskPassword)
|
||||
{
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="password" Immediate="true" autocomplete="current-password" maxlength="200"
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="password" Immediate="true" maxlength="200"
|
||||
placeholder="@(Staff ? Localizer["Your password"] : Localizer["Current password"])"
|
||||
aria-label="@(Staff ? Localizer["Your password"] : Localizer["Current password"])"
|
||||
data-admin-password="@(Staff ? "" : null)" data-own-password="@(Staff ? null : "")" />
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
@if (ticket is null)
|
||||
{
|
||||
<p class="text-sm">@Localizer["It holds every persona's private keys and private posts: keep it where only you can read it. Your password, to get a link:"]</p>
|
||||
<DInput TValue="string" type="password" @bind-Value="password" Immediate="true" autocomplete="current-password" maxlength="200"
|
||||
<DInput TValue="string" type="password" @bind-Value="password" Immediate="true" maxlength="200"
|
||||
placeholder="@Localizer["Password"]" data-backup-password />
|
||||
<ErrorViewer AdditionalError="@dialogError" />
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
{
|
||||
<p>@Localizer["{0} logins were deleted since; a restore deletes them again.", restoring.RootsDeletedSince]</p>
|
||||
}
|
||||
<DInput TValue="string" type="password" @bind-Value="password" Immediate="true" autocomplete="current-password" maxlength="200"
|
||||
<DInput TValue="string" type="password" @bind-Value="password" Immediate="true" maxlength="200"
|
||||
placeholder="@Localizer["Password"]" data-backup-password />
|
||||
<DInput TValue="string" @bind-Value="typedHost" Immediate="true" autocomplete="off" maxlength="253"
|
||||
placeholder="@Localizer["Type {0} to confirm", backups.Host ?? ""]" data-backup-host />
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<DStats class="w-full overflow-x-auto neo-raised neo-xs" data-statistics-overview data-scroll-x>
|
||||
<DStat>
|
||||
<Heading>@Localizer["Deliveries that arrived"]</Heading>
|
||||
<ChildContent>@(overview.DeliverySuccess is { } success ? $"{success * 100:0.#}%" : "—")</ChildContent>
|
||||
<ChildContent>@(overview.DeliverySuccess is { } success ? (success * 100).ToDefaultPercent() : "—")</ChildContent>
|
||||
<Desc>@Localizer["p50 {0} ms, p95 {1} ms", Ms(overview.DeliveryLatencyMs.P50), Ms(overview.DeliveryLatencyMs.P95)]</Desc>
|
||||
</DStat>
|
||||
<DStat>
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Localizer["Shown only to people within {0} km of here, never sent to other servers.", form.RangeKm.ToString("0")]</span>
|
||||
<span>@Localizer["Shown only to people within {0} km of here, never sent to other servers.", form.RangeKm.ToDefaultCount()]</span>
|
||||
}
|
||||
</span>
|
||||
<DRange TValue="double" Size="DSize.Xs" Min="1" Max="50" Step="1" @bind-Value="form.RangeKm" />
|
||||
|
||||
@@ -804,7 +804,7 @@
|
||||
var option = poll.Options[i];
|
||||
var share = poll.VotesCount > 0 && option.VotesCount is { } votes ? votes * 100d / poll.VotesCount : 0;
|
||||
<div class="relative rounded-md overflow-hidden neo-inset neo-2xs px-2 py-1">
|
||||
<div class="absolute inset-y-0 left-0 bg-primary/20" style="width:@(share.ToString("0.#", System.Globalization.CultureInfo.InvariantCulture))%"></div>
|
||||
<div class="absolute inset-y-0 left-0 bg-primary/20" style="width:@(share.ToCssNumber())%"></div>
|
||||
<span class="relative flex justify-between gap-2">
|
||||
<span>
|
||||
@option.Title
|
||||
@@ -813,7 +813,7 @@
|
||||
<i class="ion-md-checkmark-circle text-primary" title="@Localizer["Your vote"]" aria-label="@Localizer["Your vote"]"></i>
|
||||
}
|
||||
</span>
|
||||
<span>@share.ToString("0")%</span>
|
||||
<span>@share.ToWholePercent()</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -1318,7 +1318,7 @@
|
||||
|
||||
// where a cropped picture keeps its subject: Mastodon's focus runs from -1 to 1, left to right and bottom to top
|
||||
static string FocalPoint(MediaAttachment media) => media.Meta?.Focus is { } focus
|
||||
? string.Create(System.Globalization.CultureInfo.InvariantCulture, $"object-position: {(focus.X + 1) * 50:0.#}% {(1 - focus.Y) * 50:0.#}%")
|
||||
? string.Create(System.Globalization.CultureInfo.InvariantCulture, $"object-position: {((focus.X + 1) * 50).ToCssNumber()}% {((1 - focus.Y) * 50).ToCssNumber()}%")
|
||||
: null;
|
||||
|
||||
static bool IsWebLink(string url) => Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme is "https" or "http";
|
||||
@@ -1474,7 +1474,7 @@
|
||||
yield return NerdRow.Of(Localizer["server"], server.NodeName is { Length: > 0 } name && name != server.Host ? $"{server.Host} ({name})" : server.Host);
|
||||
yield return NerdRow.Of(Localizer["software"], $"{server.Software} {server.Version}".Trim());
|
||||
yield return NerdRow.Of(Localizer["Users"], server.Users is { } users
|
||||
? server.ActiveMonth is { } active ? $"{users:N0} ({active:N0} {Localizer["active this month"]})" : users.ToString("N0")
|
||||
? server.ActiveMonth is { } active ? $"{users.ToDefaultCount()} ({active.ToDefaultCount()} {Localizer["active this month"]})" : users.ToDefaultCount()
|
||||
: null);
|
||||
yield return NerdRow.Of(Localizer["local posts"], server.LocalPosts);
|
||||
yield return NerdRow.Of(Localizer["registrations"], server.OpenRegistrations is { } open ? open ? Localizer["open"] : Localizer["closed"] : null);
|
||||
@@ -1553,7 +1553,7 @@
|
||||
_ => Localizer["Near you"]
|
||||
});
|
||||
yield return NerdRow.Of("lat, lng", $"{Coordinate(location.Latitude)}, {Coordinate(location.Longitude)}");
|
||||
yield return NerdRow.Of(Localizer["precision"], $"±{location.PrecisionKm:0} km");
|
||||
yield return NerdRow.Of(Localizer["precision"], $"±{location.PrecisionKm.ToDefaultCount()} km");
|
||||
if (location.Source is LocationSource.Server or LocationSource.Country && ServerGeo is { } geo)
|
||||
{
|
||||
yield return NerdRow.Of(Localizer["network"], geo.Network);
|
||||
@@ -1561,7 +1561,7 @@
|
||||
yield return NerdRow.Of(Localizer["located at"], geo.LocatedAt);
|
||||
}
|
||||
if (InstanceService.Known(InstanceService.SelfHost)?.Geo is { Latitude: { } selfLatitude, Longitude: { } selfLongitude } && location.Source is not LocationSource.NearYou)
|
||||
yield return new(Localizer["from PrivaPub"], $"{NerdRow.Kilometres(selfLatitude, selfLongitude, location.Latitude, location.Longitude):N0} km");
|
||||
yield return new(Localizer["from PrivaPub"], $"{NerdRow.Kilometres(selfLatitude, selfLongitude, location.Latitude, location.Longitude).ToDefaultCount()} km");
|
||||
}
|
||||
|
||||
static IEnumerable<NerdRow> MediaRows(MediaAttachment media)
|
||||
@@ -1570,7 +1570,7 @@
|
||||
yield return NerdRow.Of("size", media.Meta?.Original?.Size);
|
||||
yield return NerdRow.Of("aspect", media.Meta?.Original?.Aspect);
|
||||
yield return NerdRow.Of("length", media.Meta?.Length);
|
||||
yield return NerdRow.Of("focus", media.Meta?.Focus is { } focus ? $"{focus.X:0.##}, {focus.Y:0.##}" : null);
|
||||
yield return NerdRow.Of("focus", media.Meta?.Focus is { } focus ? $"{focus.X.ToReadout()}, {focus.Y.ToReadout()}" : null);
|
||||
yield return NerdRow.Of("blurhash", media.Blurhash);
|
||||
yield return NerdRow.Of("served by", Uri.TryCreate(media.Url, UriKind.Absolute, out var served) ? served.Host : null);
|
||||
yield return NerdRow.Of("origin", Uri.TryCreate(media.RemoteUrl, UriKind.Absolute, out var origin) ? origin.Host : null);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
[Parameter] public bool CloseOnContentClick { get; set; }
|
||||
[Parameter] public string TriggerClass { get; set; }
|
||||
/// <summary>Attributes of the trigger itself (an aria-label, a data- hook); the outer element takes the unmatched ones.</summary>
|
||||
[Parameter] public IReadOnlyDictionary<string, object> TriggerAttributes { get; set; }
|
||||
[Parameter] public IReadOnlyDictionary<string, object> TriggerAttributes { get; set; } = new Dictionary<string, object>();
|
||||
[Parameter] public string ContentClass { get; set; }
|
||||
/// <summary>Below md, the content is a sheet at the bottom of the screen instead of hanging from the trigger.</summary>
|
||||
[Parameter] public bool SheetOnNarrow { get; set; }
|
||||
|
||||
@@ -10,7 +10,7 @@ public abstract class DComponentBase : ComponentBase
|
||||
{
|
||||
[Parameter] public string Class { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
|
||||
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
protected static string Cx(params string[] classes) => DCx.Join(classes);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="@Class">
|
||||
@if (Src is not null)
|
||||
{
|
||||
<img src="@Src" alt="@Alt" />
|
||||
<img src="@Src" alt="@(Alt ?? "")" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -19,7 +19,7 @@
|
||||
@code {
|
||||
[Parameter] public string Src { get; set; }
|
||||
// "" on purpose (the documented exception to "no string defaults"): an empty alt marks a decorative image.
|
||||
[Parameter] public string Alt { get; set; } = "";
|
||||
[Parameter] public string Alt { get; set; }
|
||||
/// <summary>Shown in the frame when there is no Src, e.g. initials with Placeholder.</summary>
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] public DAvatarStatus Status { get; set; }
|
||||
|
||||
@@ -4,19 +4,16 @@
|
||||
across it shows the others. Up to 10 images of the same size, from Images or as <img> children; cap its width in
|
||||
Class (max-w-60), or it fills the container. *@
|
||||
<figure @attributes="AdditionalAttributes" class="@Cx("hover-gallery", Class)">
|
||||
@if (Images is not null)
|
||||
@foreach (var src in Images.Take(10))
|
||||
{
|
||||
foreach (var src in Images.Take(10))
|
||||
{
|
||||
<img src="@src" alt="@Alt" />
|
||||
}
|
||||
<img src="@src" alt="@(Alt ?? "")" />
|
||||
}
|
||||
@ChildContent
|
||||
</figure>
|
||||
|
||||
@code {
|
||||
[Parameter] public IEnumerable<string> Images { get; set; }
|
||||
[Parameter] public IEnumerable<string> Images { get; set; } = [];
|
||||
// "" on purpose (the documented exception to "no string defaults"): an empty alt marks a decorative image.
|
||||
[Parameter] public string Alt { get; set; } = "";
|
||||
[Parameter] public string Alt { get; set; }
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
@@ -5,19 +5,16 @@
|
||||
duration-* in Class (default 10s). *@
|
||||
<span @attributes="AdditionalAttributes" class="@Cx("text-rotate", Class)">
|
||||
<span class="@(Centered ? "justify-items-center" : null)">
|
||||
@if (Items is not null)
|
||||
@foreach (var item in Items.Take(6))
|
||||
{
|
||||
foreach (var item in Items.Take(6))
|
||||
{
|
||||
<span>@item</span>
|
||||
}
|
||||
<span>@item</span>
|
||||
}
|
||||
@ChildContent
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@code {
|
||||
[Parameter] public IEnumerable<string> Items { get; set; }
|
||||
[Parameter] public IEnumerable<string> Items { get; set; } = [];
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
/// <summary>Centre the lines horizontally.</summary>
|
||||
[Parameter] public bool Centered { get; set; }
|
||||
|
||||
@@ -7,19 +7,16 @@
|
||||
<div @attributes="InputAttributes" class="@Cx("filter", CssClass)">
|
||||
<input type="radio" name="@GroupName" class="@Cx("btn", "filter-reset", SizeClass)" aria-label="@ResetText"
|
||||
disabled="@Disabled" @onchange="() => CurrentValue = default" />
|
||||
@if (Options is not null)
|
||||
@foreach (var option in Options)
|
||||
{
|
||||
foreach (var option in Options)
|
||||
{
|
||||
<input type="radio" name="@GroupName" class="@Cx("btn", SizeClass)" aria-label="@(TextOf?.Invoke(option) ?? option?.ToString())"
|
||||
checked="@IsSelected(option)" disabled="@Disabled"
|
||||
@onchange="() => CurrentValue = option" />
|
||||
}
|
||||
<input type="radio" name="@GroupName" class="@Cx("btn", SizeClass)" aria-label="@(TextOf?.Invoke(option) ?? option?.ToString())"
|
||||
checked="@IsSelected(option)" disabled="@Disabled"
|
||||
@onchange="() => CurrentValue = option" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public IEnumerable<TValue> Options { get; set; }
|
||||
[Parameter] public IEnumerable<TValue> Options { get; set; } = [];
|
||||
/// <summary>An option's button text; ToString() when null.</summary>
|
||||
[Parameter] public Func<TValue, string> TextOf { get; set; }
|
||||
[Parameter] public string ResetText { get; set; } = "×";
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
/// <summary>The choices can't be made now (the caller is busy with the last one).</summary>
|
||||
[Parameter] public bool ChoicesDisabled { get; set; }
|
||||
/// <summary>Passed to the dropdown: its class, a data- hook.</summary>
|
||||
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
|
||||
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
bool isOpen;
|
||||
string query;
|
||||
|
||||
@@ -251,9 +251,9 @@
|
||||
yield return NerdRow.Of(Localizer["axial tilt"], $"{Fixed(sun.Obliquity)}°");
|
||||
yield return NerdRow.Of(Localizer["ecliptic longitude"], $"{Fixed(sun.EclipticLongitude)}°");
|
||||
yield return NerdRow.Of(Localizer["Earth–Sun distance"],
|
||||
$"{sun.Distance.ToString("0.00000", CultureInfo.InvariantCulture)} AU ({sun.Distance * 149_597_870.7:N0} km)");
|
||||
$"{sun.Distance.ToFixed(5)} AU ({(sun.Distance * 149_597_870.7).ToDefaultCount()} km)");
|
||||
yield return NerdRow.Of(Localizer["irradiance"],
|
||||
$"{sun.Irradiance * 1361:0} W/m² ({Math.Round((sun.Irradiance - 1) * 100, 1).ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture)}%)");
|
||||
$"{(sun.Irradiance * 1361).ToDefaultCount()} W/m² ({Math.Round((sun.Irradiance - 1) * 100, 1).ToSignedPercent()})");
|
||||
yield return NerdRow.Of(Localizer["Place"], sun.Place is { } place ? $"{place} ({Fixed(sun.PlaceLatitude)}, {Fixed(sun.PlaceLongitude)})" : null);
|
||||
yield return NerdRow.Of(Localizer["elevation"], sun.Elevation is { } elevation ? $"{Fixed(elevation)}°" : null);
|
||||
yield return NerdRow.Of(Localizer["day length"], sun.DayLength is { } hours ? NerdRow.Duration(TimeSpan.FromHours(hours)) : null);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Globalization;
|
||||
|
||||
using decePubClient.Extensions;
|
||||
|
||||
namespace decePubClient.Components.Nerd;
|
||||
|
||||
/// <summary>One line of a NerdTip; a row without a value is not shown.</summary>
|
||||
@@ -9,7 +11,7 @@ public sealed record NerdRow(string Label, string Value)
|
||||
{
|
||||
null => null,
|
||||
DateTime time => Time(time),
|
||||
double number => number.ToString("0.###", CultureInfo.InvariantCulture),
|
||||
double number => number.ToReadout(),
|
||||
bool flag => flag ? "yes" : "no",
|
||||
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||
_ => value.ToString()
|
||||
@@ -27,8 +29,8 @@ public sealed record NerdRow(string Label, string Value)
|
||||
span = span.Duration();
|
||||
return sign + (span.TotalSeconds switch
|
||||
{
|
||||
< 1 => $"{span.TotalMilliseconds:0} ms",
|
||||
< 60 => $"{span.TotalSeconds.ToString("0.0", CultureInfo.InvariantCulture)} s",
|
||||
< 1 => $"{span.TotalMilliseconds.ToFixed(0)} ms",
|
||||
< 60 => $"{span.TotalSeconds.ToFixed(1)} s",
|
||||
< 3600 => $"{span.Minutes} min {span.Seconds} s",
|
||||
< 86400 => $"{(int)span.TotalHours} h {span.Minutes} min",
|
||||
_ => $"{(int)span.TotalDays} d {span.Hours} h"
|
||||
@@ -40,8 +42,8 @@ public sealed record NerdRow(string Label, string Value)
|
||||
{
|
||||
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"
|
||||
< 1024 * 1024 => $"{(bytes.Value / 1024d).ToFixed(1)} KiB",
|
||||
_ => $"{(bytes.Value / 1048576d).ToFixed(1)} MiB"
|
||||
};
|
||||
|
||||
/// <summary>The great-circle distance between two points in kilometres (haversine, Earth radius 6371 km).</summary>
|
||||
|
||||
@@ -58,9 +58,9 @@
|
||||
{
|
||||
<EditForm Model="change" OnValidSubmit="ChangeEmail" class="flex flex-col gap-2" data-account-email-change>
|
||||
<DataAnnotationsValidator />
|
||||
<DInput TValue="string" Type="email" @bind-Value="change.Email" Immediate="true" autocomplete="email" inputmode="email" autocapitalize="none"
|
||||
<DInput TValue="string" Type="email" @bind-Value="change.Email" Immediate="true" inputmode="email" autocapitalize="none"
|
||||
spellcheck="false" placeholder="@Localizer["New email address"]" data-account-email-new />
|
||||
<DInput TValue="string" Type="password" @bind-Value="change.Password" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" Type="password" @bind-Value="change.Password" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-account-email-password />
|
||||
<ErrorViewer />
|
||||
<div class="flex flex-wrap justify-end gap-2 pointer-coarse:gap-2">
|
||||
@@ -75,7 +75,7 @@
|
||||
{
|
||||
<EditForm Model="emailRemoval" OnValidSubmit="RemoveEmail" class="flex flex-col gap-2" data-account-email-remove>
|
||||
<DataAnnotationsValidator />
|
||||
<DInput TValue="string" Type="password" @bind-Value="emailRemoval.Password" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" Type="password" @bind-Value="emailRemoval.Password" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-account-email-remove-password />
|
||||
<ErrorViewer />
|
||||
<div class="flex flex-wrap justify-end gap-2 pointer-coarse:gap-2">
|
||||
@@ -107,7 +107,7 @@
|
||||
<DataAnnotationsValidator />
|
||||
<b class="text-sm">@Localizer["Change the password"]</b>
|
||||
<p class="text-xs opacity-70">@Localizer["Every other device signed in to this login is signed out."]</p>
|
||||
<DInput TValue="string" type="password" @bind-Value="password.OldPassword" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" type="password" @bind-Value="password.OldPassword" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-account-current />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<DInput TValue="string" type="password" class="flex-1 min-w-40" @bind-Value="password.NewPassword" Immediate="true" autocomplete="new-password"
|
||||
@@ -135,7 +135,7 @@
|
||||
<DataAnnotationsValidator />
|
||||
<b class="text-sm text-error">@Localizer["Delete this login"]</b>
|
||||
<p class="text-sm">@Localizer["Every persona of this login goes with it, and everything public they had, here and on the servers that hold it. It cannot be undone."]</p>
|
||||
<DInput TValue="string" type="password" @bind-Value="removal.Password" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" type="password" @bind-Value="removal.Password" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-account-delete-password />
|
||||
<DCheckbox @bind-Value="understood" Size="DSize.Sm" LabelText="@Localizer["I understand: delete this login and every persona of it"]" data-account-delete-understood />
|
||||
<ErrorViewer />
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
<EditForm Model="disable" OnValidSubmit="Disable" class="flex flex-col gap-2" data-totp-disable>
|
||||
<DataAnnotationsValidator />
|
||||
<DInput TValue="string" Type="password" @bind-Value="disable.Password" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" Type="password" @bind-Value="disable.Password" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-totp-disable-password />
|
||||
@if (disableWithRecoveryCode)
|
||||
{
|
||||
@@ -133,7 +133,7 @@
|
||||
{
|
||||
<EditForm Model="setupPassword" OnValidSubmit="Setup" class="flex flex-col gap-2" data-totp-setup>
|
||||
<DataAnnotationsValidator />
|
||||
<DInput TValue="string" Type="password" @bind-Value="setupPassword.Password" Immediate="true" autocomplete="current-password"
|
||||
<DInput TValue="string" Type="password" @bind-Value="setupPassword.Password" Immediate="true"
|
||||
placeholder="@Localizer["Current password"]" data-totp-setup-password />
|
||||
<ErrorViewer />
|
||||
<div class="flex flex-wrap justify-end gap-2 pointer-coarse:gap-2">
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<HouseRulesFormatHelpers>NumbersExtensions</HouseRulesFormatHelpers>
|
||||
<HouseRulesAutocompleteOmit>Login.razor,Join.razor,Setup.razor,PasswordRecovery.razor,AccountEditor.razor,TotpEditor.razor,Groups.razor,ActDialog.razor,BackupsPanel.razor</HouseRulesAutocompleteOmit>
|
||||
<WarningsAsErrors>$(WarningsAsErrors);HR0001;HR0002;HR0003;HR0004;HR0005;HR0006;HR0007;HR0008;HR0009;HR0010;HR0011;HR0101;HR0102;HR0103;HR0104;HR0105;HR0106;HR0201</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Arasaka.HouseRules.Analyzers" Version="1.0.4" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -30,15 +30,6 @@ public static class GenericExtensions
|
||||
};
|
||||
}
|
||||
|
||||
// a count as a phone's narrow action buttons show it: 999, 1.2k, 12k, 1.2M
|
||||
public static string Compact(this int count) => count switch
|
||||
{
|
||||
< 1000 => count.ToString(CultureInfo.CurrentCulture),
|
||||
< 10_000 => (count / 1000d).ToString("0.#", CultureInfo.CurrentCulture) + "k",
|
||||
< 1_000_000 => (count / 1000).ToString(CultureInfo.CurrentCulture) + "k",
|
||||
_ => (count / 1_000_000d).ToString("0.#", CultureInfo.CurrentCulture) + "M"
|
||||
};
|
||||
|
||||
public static string GetFileSize(this long size, CoalescingStringLocalizer localizer) => size switch
|
||||
{
|
||||
>= 1_048_576 => localizer["{0}MB", size / 1_048_576],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace decePubClient.Extensions;
|
||||
|
||||
public static class NumbersExtensions
|
||||
{
|
||||
public static string ToDefaultCount(this int value) => value.ToString("N0", CultureInfo.CurrentCulture);
|
||||
|
||||
public static string ToDefaultCount(this long value) => value.ToString("N0", CultureInfo.CurrentCulture);
|
||||
|
||||
public static string ToDefaultCount(this double value) => value.ToString("N0", CultureInfo.CurrentCulture);
|
||||
|
||||
public static string Compact(this int count) => count switch
|
||||
{
|
||||
< 1000 => count.ToString(CultureInfo.CurrentCulture),
|
||||
< 10_000 => (count / 1000d).ToString("0.#", CultureInfo.CurrentCulture) + "k",
|
||||
< 1_000_000 => (count / 1000).ToString(CultureInfo.CurrentCulture) + "k",
|
||||
_ => (count / 1_000_000d).ToString("0.#", CultureInfo.CurrentCulture) + "M"
|
||||
};
|
||||
|
||||
public static string ToDefaultPercent(this double percent) => percent.ToString("0.#", CultureInfo.CurrentCulture) + "%";
|
||||
|
||||
public static string ToWholePercent(this double percent) => percent.ToString("0", CultureInfo.CurrentCulture) + "%";
|
||||
|
||||
public static string ToSignedPercent(this double percent) => percent.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + "%";
|
||||
|
||||
public static string ToCssNumber(this double value) => value.ToString("0.#", CultureInfo.InvariantCulture);
|
||||
|
||||
public static string ToReadout(this double value) => value.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
public static string ToFixed(this double value, int decimals) =>
|
||||
value.ToString("F" + decimals.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@
|
||||
placeholder="@Localizer["Invitation code"]" data-group-code>
|
||||
<Prefix><i class="ion-md-key"></i></Prefix>
|
||||
</DInput>
|
||||
<DInput TValue="string" type="password" @bind-Value="joining.InvitationPassword" Immediate="true" autocomplete="current-password" maxlength="128"
|
||||
<DInput TValue="string" type="password" @bind-Value="joining.InvitationPassword" Immediate="true" maxlength="128"
|
||||
placeholder="@Localizer["Invitation password, if it has one"]" />
|
||||
<DButton Type="submit" Size="DSize.Sm" class="self-end" Disabled="@(IsDefaultDisabled || isSaving || string.IsNullOrWhiteSpace(joining.InvitationCode))" data-group-join-submit>
|
||||
<i class="ion-md-log-in"></i><span>@Localizer["Join"]</span>
|
||||
|
||||
+1
-1
@@ -692,7 +692,7 @@
|
||||
if (RequestStats.Latest(1).FirstOrDefault() is { } last)
|
||||
{
|
||||
yield return NerdRow.Of(Localizer["last request"], $"{last.Method} {last.Route}");
|
||||
yield return NerdRow.Of(Localizer["answer"], $"{last.Status} · {last.DurationMs:0} ms · {NerdRow.Bytes(last.Bytes) ?? "?"}");
|
||||
yield return NerdRow.Of(Localizer["answer"], $"{last.Status} · {last.DurationMs.ToDefaultCount()} ms · {NerdRow.Bytes(last.Bytes) ?? "?"}");
|
||||
yield return NerdRow.Of("X-RateLimit-Remaining", last.RateLimitRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -29,13 +29,13 @@
|
||||
<DInput TValue="string" class="w-full font-mono text-xs" @bind-Value="form.InvitationCode" autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
maxlength="@PrivaPub.ClientModels.Constants.GroupInvitationLength" data-join-code />
|
||||
</label>
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="form.InvitationPassword" autocomplete="off"
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="form.InvitationPassword"
|
||||
placeholder="@Localizer["Invitation password, if it has one"]" data-join-code-password />
|
||||
|
||||
<b class="text-sm">@Localizer["Your login"]</b>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Username"]</span>
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocomplete="username" autocapitalize="none" spellcheck="false" data-join-username>
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocapitalize="none" spellcheck="false" data-join-username>
|
||||
<Prefix><i class="ion-md-person"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
|
||||
+2
-2
@@ -60,13 +60,13 @@
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Username"]</span>
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocomplete="username" autocapitalize="none" spellcheck="false">
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocapitalize="none" spellcheck="false">
|
||||
<Prefix><i class="ion-md-person"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Password"]</span>
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="form.Password" autocomplete="current-password">
|
||||
<DInput TValue="string" Type="password" class="w-full" @bind-Value="form.Password">
|
||||
<Prefix><i class="ion-md-key"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Username"]</span>
|
||||
<DInput TValue="string" class="w-full" Value="request.UserName" ValueChanged="v => request.UserName = NullIfBlank(v)" ValueExpression="() => request.UserName"
|
||||
Immediate="true" disabled="@request.IsUsernameDisabled" autocomplete="username" autocapitalize="none" spellcheck="false" data-recovery-username>
|
||||
Immediate="true" disabled="@request.IsUsernameDisabled" autocapitalize="none" spellcheck="false" data-recovery-username>
|
||||
<Prefix><i class="ion-md-person"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
@@ -43,7 +43,7 @@
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Email address"]</span>
|
||||
<DInput TValue="string" Type="email" class="w-full" Value="request.Email" ValueChanged="v => request.Email = NullIfBlank(v)" ValueExpression="() => request.Email"
|
||||
Immediate="true" disabled="@request.IsEmailDisabled" autocomplete="email" inputmode="email" autocapitalize="none" spellcheck="false" data-recovery-email>
|
||||
Immediate="true" disabled="@request.IsEmailDisabled" inputmode="email" autocapitalize="none" spellcheck="false" data-recovery-email>
|
||||
<Prefix><i class="ion-md-mail"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
<b class="text-sm">@Localizer["Your login"]</b>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">@Localizer["Username"]</span>
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocomplete="username" autocapitalize="none" spellcheck="false" data-setup-username>
|
||||
<DInput TValue="string" class="w-full" @bind-Value="form.UserName" autocapitalize="none" spellcheck="false" data-setup-username>
|
||||
<Prefix><i class="ion-md-person"></i></Prefix>
|
||||
</DInput>
|
||||
</label>
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace decePubClient.Services
|
||||
|
||||
var account = await response.MastodonReadFromJsonAsync<Account>(cancellationToken);
|
||||
return account?.Id is null
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway)
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest)
|
||||
: new WebResult { Data = account };
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ namespace decePubClient.Services
|
||||
|
||||
var account = await response.MastodonReadFromJsonAsync<Account>(cancellationToken);
|
||||
return account?.Id is null
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway)
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest)
|
||||
: new WebResult { Data = account };
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace decePubClient.Services
|
||||
|
||||
var jwtUser = await response.DefaultReadFromJsonAsync<JwtUser>(cancellationToken);
|
||||
if (string.IsNullOrEmpty(jwtUser?.Token))
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest);
|
||||
|
||||
await authStateProvider.SetSession(jwtUser, cancellationToken);
|
||||
return new WebResult { Data = jwtUser };
|
||||
@@ -262,7 +262,7 @@ namespace decePubClient.Services
|
||||
|
||||
var setup = await response.DefaultReadFromJsonAsync<ViewTotpSetup>(cancellationToken);
|
||||
if (string.IsNullOrEmpty(setup?.Secret))
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest);
|
||||
|
||||
return new WebResult { Data = setup };
|
||||
}
|
||||
@@ -282,7 +282,7 @@ namespace decePubClient.Services
|
||||
|
||||
var enabled = await response.DefaultReadFromJsonAsync<ViewTotpEnabled>(cancellationToken);
|
||||
if (string.IsNullOrEmpty(enabled?.Session?.Token))
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest);
|
||||
|
||||
await ContinueSession(enabled.Session, nameof(TotpEnable), cancellationToken);
|
||||
return new WebResult { Data = enabled.RecoveryCodes };
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace decePubClient.Services
|
||||
if (string.IsNullOrEmpty(token?.AccessToken))
|
||||
{
|
||||
response.Dispose();
|
||||
return (SUtility.ClassifiedFailure(FailureCodes.ServerError, localizer["The server sent an invalid response."], HttpStatusCode.BadGateway), null);
|
||||
return (SUtility.ClassifiedFailure(FailureCodes.ServerError, localizer["The server sent an invalid response."], HttpStatusCode.BadRequest), null);
|
||||
}
|
||||
|
||||
return (response, token.AccessToken);
|
||||
@@ -205,7 +205,7 @@ namespace decePubClient.Services
|
||||
{
|
||||
await logger.ProcessError(ex, where, cancellationToken);
|
||||
return SUtility.ClassifiedFailure(FailureCodes.ClientError,
|
||||
localizer["The request could not be prepared. Reload the page and try again."], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["The request could not be prepared. Reload the page and try again."], HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
var delay = attempt < RetryRampUpDelaysMs.Length ? RetryRampUpDelaysMs[attempt] : RetryIndefiniteDelayMs;
|
||||
@@ -216,7 +216,7 @@ namespace decePubClient.Services
|
||||
if (transportException is not null)
|
||||
return await DescribeUnreachable(transportException, attempt + 1, cancellationToken);
|
||||
return SUtility.ClassifiedFailure(FailureCodes.RetriesExhausted,
|
||||
localizer["The server is still not answering after {0} attempts. Try again later.", attempt + 1], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["The server is still not answering after {0} attempts. Try again later.", attempt + 1], HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
await logger.ProcessWarning($"Attempt {attempt + 1} failed, retrying in {delay}ms. {failure}", where, cancellationToken);
|
||||
@@ -245,18 +245,18 @@ namespace decePubClient.Services
|
||||
{
|
||||
if (!await statusService.IsOnline(cancellationToken))
|
||||
return SUtility.ClassifiedFailure(FailureCodes.DeviceOffline,
|
||||
localizer["This device is offline. Check the connection and try again."], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["This device is offline. Check the connection and try again."], HttpStatusCode.BadRequest);
|
||||
|
||||
if (exception is TaskCanceledException { InnerException: TimeoutException })
|
||||
return SUtility.ClassifiedFailure(FailureCodes.Timeout,
|
||||
localizer["The server took too long to answer. Try again later."], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["The server took too long to answer. Try again later."], HttpStatusCode.BadRequest);
|
||||
|
||||
if (attempts > 1)
|
||||
return SUtility.ClassifiedFailure(FailureCodes.RetriesExhausted,
|
||||
localizer["The server is still not answering after {0} attempts. Try again later.", attempts], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["The server is still not answering after {0} attempts. Try again later.", attempts], HttpStatusCode.BadRequest);
|
||||
|
||||
return SUtility.ClassifiedFailure(FailureCodes.ApiUnreachable,
|
||||
localizer["The server can't be reached. Try again later."], HttpStatusCode.ServiceUnavailable);
|
||||
localizer["The server can't be reached. Try again later."], HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
Task<HttpResponseMessage> SendAnonymousRequest(Func<HttpRequestMessage> requestFactory, CancellationToken cancellationToken) =>
|
||||
@@ -403,7 +403,7 @@ namespace decePubClient.Services
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(request.Content is MultipartContent ? UploadTimeout : RequestTimeout);
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
HttpResponseMessage response;
|
||||
var response = default(HttpResponseMessage);
|
||||
try
|
||||
{
|
||||
response = await httpClient.SendAsync(request, timeout.Token);
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace decePubClient.Services
|
||||
if (!current.IsSuccessStatusCode)
|
||||
return await current.Content.ReadWebResult(current.StatusCode, localizer, cancellationToken);
|
||||
if (await current.DefaultReadFromJsonAsync<ViewServerSettings>(cancellationToken) is not { } latest)
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
||||
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest);
|
||||
|
||||
var form = FormOf(latest);
|
||||
change(form);
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace decePubClient.Services
|
||||
{
|
||||
var attachment = await response.MastodonReadFromJsonAsync<MediaAttachment>(cancellationToken);
|
||||
return attachment?.Id is null
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway)
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest)
|
||||
: new WebResult { Data = attachment };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace decePubClient.Services
|
||||
|
||||
public async Task CloseTop(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Entry top;
|
||||
var top = default(Entry);
|
||||
lock (open)
|
||||
top = open.LastOrDefault();
|
||||
if (top is null)
|
||||
@@ -40,7 +40,7 @@ namespace decePubClient.Services
|
||||
|
||||
public async Task CloseAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Entry[] all;
|
||||
var all = default(Entry[]);
|
||||
lock (open)
|
||||
{
|
||||
all = [.. open];
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace decePubClient.Services
|
||||
if (personaIds.Count > 0 && key is null)
|
||||
{
|
||||
await Save(device, cancellationToken);
|
||||
return result.Invalidate(localizer["This server does not offer push notifications."], (int)HttpStatusCode.NotImplemented);
|
||||
return result.Invalidate(localizer["This server does not offer push notifications."], (int)HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
foreach (var personaId in personaIds)
|
||||
@@ -252,7 +252,7 @@ namespace decePubClient.Services
|
||||
{
|
||||
if (isNew)
|
||||
await Js("unsubscribe", false, [entry.Slot], cancellationToken);
|
||||
return new WebResult().Invalidate(localizer["This browser did not subscribe: {0}", browser?.Error ?? "?"], (int)HttpStatusCode.ServiceUnavailable);
|
||||
return new WebResult().Invalidate(localizer["This browser did not subscribe: {0}", browser?.Error ?? "?"], (int)HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
var result = await Tell(device, entry.PersonaId, browser, cancellationToken);
|
||||
|
||||
@@ -332,7 +332,7 @@ namespace decePubClient.Services
|
||||
|
||||
var status = await response.MastodonReadFromJsonAsync<Status>(cancellationToken);
|
||||
return status is null
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway)
|
||||
? new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadRequest)
|
||||
: new WebResult { Data = status };
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ namespace decePubClient.Services
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
WebSocketReceiveResult received;
|
||||
var received = default(WebSocketReceiveResult);
|
||||
do
|
||||
{
|
||||
received = await socket.ReceiveAsync(buffer, cancellationToken);
|
||||
@@ -229,7 +229,7 @@ namespace decePubClient.Services
|
||||
|
||||
/// <summary>Closes the socket; the page polls from then on. The announcements stay asked for: the next Follow opens
|
||||
/// the socket for them too.</summary>
|
||||
public async Task Stop()
|
||||
public async Task Stop(CancellationToken cancellationToken = default)
|
||||
{
|
||||
current = null;
|
||||
if (running is not null)
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace decePubClient.Services
|
||||
catch (Exception ex)
|
||||
{
|
||||
await logger.ProcessError(ex, $"{nameof(TimelineService)}.{nameof(Load)}:{query.Type}", cancellationToken);
|
||||
return new WebResult().Invalidate(localizer["The feed could not be read."], (int)HttpStatusCode.BadGateway, exception: ex);
|
||||
return new WebResult().Invalidate(localizer["The feed could not be read."], (int)HttpStatusCode.BadRequest, exception: ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="nget" value="https://nget.thepra.dev/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nget">
|
||||
<package pattern="Arasaka.*" />
|
||||
</packageSource>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
Reference in new issue
Block a user