fix(api,ui): a refused write says why, wherever the refusal happens
Round 5 taught `notifyActionError` to discard a plain-string `detail` in favour
of the caller's translated fallback, because `req` leaves 403/404/429 to the
caller and those routes hand-write English. Three places did not get the
message.
**The demo account lost its explanation.** `require_human` refused with a plain
string, so from that commit on the shared demo account got its caller's generic
"couldn't do that" and no hint that the demo account was the reason — and the
gate is not backed by a UI gate everywhere: the comfort shelf has no `is_demo`
check, so the pin button is live and simply fails. A structured `{code: "demo"}`
and an `errors.demo` string in both locales. The two auth routes that hand-roll
the same check were swept with it.
**`ChatThread` carried a second copy of the contract.** It filtered 404/429
itself and then did `e.detail || t(…)`, the pre-fix order, so the central fix
could not reach it and "That member hasn't set up messaging yet." stayed English
under a Hungarian UI. It goes through the shared helper now, and a contract test
pins the remaining raw-`detail` consumers as an explicit SET — never a count —
so a new copy fails the gate instead of shipping. The five on that list are
inline surfaces (a form field's error line, a setup banner) and are deferred
with the ~60 backend routes whose details they print.
**The Plex "Add to playlist" dialog was still silent.** Same shape as the
collection editor round 5 fixed and the YouTube twin that already had the fix:
no catch, so a playlist deleted on another device answered a structured 404 that
nothing reported — the tick never flipped, the gone row stayed, and the floating
promise became an unhandled rejection.
Also: the `ABORTED` sentinel's VALUE was asserted nowhere. The consumer test
hand-built the error it expected, so replacing `xhr.onabort`'s rejection with a
friendlier message left the lane green — and every upload the user cancels would
then raise a red "the upload failed" toast, which is what the sentinel prevents.
Both producer tests assert through `isAborted` now.
And two backend tests were pinning fewer sites than their own docstrings claimed:
the channel-404 loop said "all five" and issued three requests (explore and
subscribe unexercised, the excuse for subscribe naming a different call site
with a different reason slug), and the recipient/job test named three refusals
and requested two — `share_download`'s `not_found("recipient")` had no test in
the suite at all, nor did the locale keys added beside it. Every site named now
has a request behind it.
6 tests, mutation-checked against 7 mutations: the demo detail at both the gate
and an auth route, a re-hand-rolled ChatThread, the silent Plex dialog, the
abort sentinel, and each unpinned 404 — all red on the test that names them.
This commit is contained in:
@@ -3,13 +3,13 @@ import { qk } from "../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Send } from "lucide-react";
|
||||
import { api, HttpError, type Message, type MessageUser } from "../lib/api";
|
||||
import { api, type Message, type MessageUser } from "../lib/api";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { Button } from "./ui/Button";
|
||||
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import { relativeTime } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging";
|
||||
import * as e2ee from "../lib/e2ee";
|
||||
import KeyGate from "./KeyGate";
|
||||
@@ -132,9 +132,14 @@ export default function ChatThread({
|
||||
// The draft is kept (only cleared on success) so the user can retry. 400/409/422/500 already
|
||||
// raised the global error dialog; 404 (recipient gone) and 429 (rate-limited) are "caller-
|
||||
// handled" in api.req (no modal), so surface those here or the send fails silently.
|
||||
if (e instanceof HttpError && (e.status === 404 || e.status === 429)) {
|
||||
notify({ level: "error", message: e.detail || t("messages.sendFailed") });
|
||||
}
|
||||
//
|
||||
// Through the shared helper, which knows both of those rules: it no-ops on anything already
|
||||
// reported, and it prefers the server's reason only when that reason is a STRUCTURED detail.
|
||||
// The hand-written copy that used to live here did `e.detail || …`, so a Hungarian reader got
|
||||
// the route's English sentence — "That member hasn't set up messaging yet." — which is
|
||||
// exactly what round 5 changed `notifyActionError` to stop doing. A second copy of a contract
|
||||
// is how the fix misses a caller (round 6).
|
||||
notifyActionError(e, t("messages.sendFailed"));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user