@civitai/blocks-react 0.47.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -6
- package/dist/hooks/SfwGate.d.ts +16 -9
- package/dist/hooks/SfwGate.d.ts.map +1 -1
- package/dist/hooks/SfwGate.js +14 -8
- package/dist/hooks/SfwGate.js.map +1 -1
- package/dist/hooks/useCollectionFollow.d.ts +141 -0
- package/dist/hooks/useCollectionFollow.d.ts.map +1 -0
- package/dist/hooks/useCollectionFollow.js +202 -0
- package/dist/hooks/useCollectionFollow.js.map +1 -0
- package/dist/hooks/useDomainMaturity.d.ts +57 -17
- package/dist/hooks/useDomainMaturity.d.ts.map +1 -1
- package/dist/hooks/useDomainMaturity.js +37 -11
- package/dist/hooks/useDomainMaturity.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/iframeTransport.d.ts.map +1 -1
- package/dist/internal/iframeTransport.js +5 -2
- package/dist/internal/iframeTransport.js.map +1 -1
- package/dist/internal/liveHost.d.ts.map +1 -1
- package/dist/internal/liveHost.js +33 -0
- package/dist/internal/liveHost.js.map +1 -1
- package/dist/internal/mockHost.d.ts +39 -2
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +61 -0
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/requestTimeouts.d.ts.map +1 -1
- package/dist/internal/requestTimeouts.js +8 -0
- package/dist/internal/requestTimeouts.js.map +1 -1
- package/dist/internal/transport.d.ts +28 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +25 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts +20 -0
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +60 -0
- package/dist/internal/validate.js.map +1 -1
- package/dist/ui/FollowButton.d.ts +85 -0
- package/dist/ui/FollowButton.d.ts.map +1 -0
- package/dist/ui/FollowButton.js +162 -0
- package/dist/ui/FollowButton.js.map +1 -0
- package/dist/ui/TipButton.d.ts +115 -0
- package/dist/ui/TipButton.d.ts.map +1 -0
- package/dist/ui/TipButton.js +308 -0
- package/dist/ui/TipButton.js.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -518,6 +518,58 @@ if (error instanceof WildcardPackError && error.code === 'busy') void refetch();
|
|
|
518
518
|
if (!loading && pack) console.log(Object.keys(pack.lists));
|
|
519
519
|
```
|
|
520
520
|
|
|
521
|
+
### `useCollectionFollow()`
|
|
522
|
+
|
|
523
|
+
Follow / unfollow a collection **for the viewer**, host-mediated over
|
|
524
|
+
`SET_COLLECTION_FOLLOW`. Returns `{ setFollow, pending, error }`.
|
|
525
|
+
|
|
526
|
+
**No block scope, and no token on the wire.** The host calls the session-authed
|
|
527
|
+
`collection.follow` / `collection.unfollow` procedures, which self-bind to the
|
|
528
|
+
viewer server-side — `collectionId` is the only thing a block influences.
|
|
529
|
+
|
|
530
|
+
🔴 **Every call opens a host-chrome consent confirm naming the collection**, and
|
|
531
|
+
that click is the *only* consent this path has ever had: the HTTP predecessor's
|
|
532
|
+
`collections:write:self` scope is consent-exempt server-side and prompted nobody.
|
|
533
|
+
Moving to this bridge **tightens** the flow; what it gives up is the manifest
|
|
534
|
+
`scopes` declaration a moderator reads before install. The host resolves the
|
|
535
|
+
collection's name itself (there is no `name` field on the wire, deliberately) and
|
|
536
|
+
bounds that to **20 distinct ids per block instance** — past the cap it refuses
|
|
537
|
+
with `collection-unavailable`, the same code a collection the viewer cannot see
|
|
538
|
+
gets.
|
|
539
|
+
|
|
540
|
+
`setFollow` **rejects** with a `CollectionFollowError` on every non-success. Two
|
|
541
|
+
of those are not failures to render:
|
|
542
|
+
|
|
543
|
+
| | meaning | what to do |
|
|
544
|
+
|---|---|---|
|
|
545
|
+
| `err.declined` | the viewer dismissed the confirm — **no write occurred** | revert, say nothing |
|
|
546
|
+
| `err.signInRequired` | no session | route into `useRequestSignIn()` |
|
|
547
|
+
| `err.timedOut` | no reply arrived within the 10-min consent bound | 🔴 **check this BEFORE `.message`** — it also has no `.code`, and its message is an SDK-internal string. It does **not** mean no write occurred; re-read your state |
|
|
548
|
+
| `err.code` set otherwise | a host refusal (`invalid-request` / `review-mode` / `not-ready` / `collection-unavailable`) | show or ignore per case |
|
|
549
|
+
| `err.code === undefined` **and** `!err.timedOut` | a **server** message the host forwarded verbatim | show `err.message` |
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
const { setFollow, pending } = useCollectionFollow();
|
|
553
|
+
const { requestSignIn } = useRequestSignIn();
|
|
554
|
+
|
|
555
|
+
async function toggle() {
|
|
556
|
+
try {
|
|
557
|
+
const result = await setFollow({ collectionId, follow: !followed });
|
|
558
|
+
setFollowed(result.followed); // adopt the host's echo, not the guess
|
|
559
|
+
} catch (err) {
|
|
560
|
+
if (err instanceof CollectionFollowError) {
|
|
561
|
+
if (err.signInRequired) return requestSignIn();
|
|
562
|
+
if (err.declined) return; // the viewer said no — say nothing
|
|
563
|
+
if (err.timedOut) return showToast('Still working — check back in a moment.');
|
|
564
|
+
showToast(err.message); // a real server message, safe to render
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Most blocks want `<FollowButton>` from `@civitai/blocks-react/ui` instead, which
|
|
571
|
+
wires all of the above.
|
|
572
|
+
|
|
521
573
|
### `useAppWorkflows(params?)`
|
|
522
574
|
|
|
523
575
|
The calling app's **own** generator subqueue — the tag-scoped list of generations
|
|
@@ -778,20 +830,48 @@ any request for a scope your dev token lacks produces one.
|
|
|
778
830
|
|
|
779
831
|
### `useDomainMaturity()`
|
|
780
832
|
|
|
781
|
-
Read the
|
|
782
|
-
|
|
783
|
-
|
|
833
|
+
Read the maturity ceiling in force for the current viewer, so a block can
|
|
834
|
+
hide/blur mature affordances. **Fail-closed SFW** until `BLOCK_INIT` lands or
|
|
835
|
+
against a host that projects no ceiling.
|
|
784
836
|
|
|
785
837
|
```tsx
|
|
786
838
|
const { isSfw, isLevelAllowed } = useDomainMaturity();
|
|
787
839
|
const showRSlider = isLevelAllowed(BrowsingLevel.R); // false on a SFW domain
|
|
788
840
|
```
|
|
789
841
|
|
|
842
|
+
The gates account for **two** things, and the distinction matters:
|
|
843
|
+
|
|
844
|
+
| field | answers |
|
|
845
|
+
| --- | --- |
|
|
846
|
+
| `maxBrowsingLevel` | what this **domain** permits anybody (identical for every viewer on it) |
|
|
847
|
+
| `effectiveBrowsingLevel` | what **this viewer** may be shown here — the domain ceiling narrowed by their own NSFW setting |
|
|
848
|
+
|
|
849
|
+
`isSfw` / `isLevelAllowed` gate on the second, so a viewer who turned NSFW off
|
|
850
|
+
sees SFW affordances even on a mature domain. `effectiveBrowsingLevel` is always
|
|
851
|
+
a subset of `maxBrowsingLevel`, so reading these can only ever show the viewer
|
|
852
|
+
**less** — never more. Compare the two when you want to explain *why* something
|
|
853
|
+
is hidden:
|
|
854
|
+
|
|
855
|
+
```tsx
|
|
856
|
+
const { maxBrowsingLevel, effectiveBrowsingLevel } = useDomainMaturity();
|
|
857
|
+
const hiddenByYourSettings = effectiveBrowsingLevel !== maxBrowsingLevel;
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
The hook's name is historic — it shipped when the domain ceiling was the only
|
|
861
|
+
signal. There is deliberately no separate viewer-maturity hook: two hooks would
|
|
862
|
+
mean two answers to "may I show this", and the one named for the domain would be
|
|
863
|
+
the wider of the pair.
|
|
864
|
+
|
|
865
|
+
Drive it locally with `createMockHost({ domain: 'red', viewerBrowsingLevel: BrowsingLevel.PG })`.
|
|
866
|
+
The mock clamps that option to its own ceiling exactly as the real host does, so
|
|
867
|
+
you cannot test against a viewer wider than the domain — production cannot
|
|
868
|
+
produce one either.
|
|
869
|
+
|
|
790
870
|
### `SfwGate`
|
|
791
871
|
|
|
792
|
-
Convenience component that renders `children` only when the
|
|
793
|
-
|
|
794
|
-
browsing-level bit. Fail-closed SFW.
|
|
872
|
+
Convenience component that renders `children` only when the current viewer may be
|
|
873
|
+
shown them — no `level` prop gates on `isSfw`, a `level` prop gates on that
|
|
874
|
+
browsing-level bit. Both account for the domain **and** the viewer. Fail-closed SFW.
|
|
795
875
|
|
|
796
876
|
```tsx
|
|
797
877
|
function MatureSection() {
|
|
@@ -924,6 +1004,8 @@ export function App() {
|
|
|
924
1004
|
| `Collapse` | controlled disclosure (`open` + `onOpenChange`, `title`, `disabled`) for the "advanced params reveal". `aria-expanded` + `aria-controls`; content region `role="region"`, `hidden` when closed. |
|
|
925
1005
|
| `SegmentedControl` | controlled view/tab switcher (`value` + `onChange`, `data: {value,label,disabled}[]`, `fullWidth`, `size`). `role="tablist"` of `role="tab"` buttons; ArrowLeft/ArrowRight rove selection. |
|
|
926
1006
|
| `ReportButton` | two-step control that files a shared-board row for platform moderator review via `useSharedStorage().report()`. `noun` + `onReport` (+ `reported` for server truth). 🔴 Its visible copy is deliberately **not** overridable — see the component's JSDoc. |
|
|
1007
|
+
| `FollowButton` | follow/unfollow a collection over the host bridge. `collectionId` + `followed` (+ `onChange`, `collectionName`). Flips optimistically, then adopts the **host's echo**; reverts on failure. 🔴 It exists because three outcomes are easy to get wrong: `declined` reverts **silently** (the viewer dismissed the host's confirm — nothing was written), `sign-in-required` routes into `REQUEST_SIGN_IN` rather than an error line, and every other rejection reverts with a `role="alert"` note. |
|
|
1008
|
+
| `TipButton` | two-step Buzz tip via `useTip()`. `toUserId` + `amount` + `noun` (+ `entityType`/`entityId`, `tipped`, `remaining`, `disabledReason`, `onTipped`). 🔴 **The confirm is the component's, not host chrome** — `useTip` posts directly with the block token, so a one-press money spend is reachable without it. It mints **one idempotency key per logical tip and reuses it on retry**, so a retry after a *lost* response cannot become a second transfer. Pass `remaining` from ONE `useTipAllowance()` held by the view — it deliberately does not fetch, or a screen of cards becomes N HTTP reads. |
|
|
927
1009
|
| `ResourceCard` | a picked generation resource (`BlockResourceInfo`) as a grid tile (`variant="card"`) or a compact line (`variant="row"`). `interactive` is an explicit discriminant that requires `onSelect` (+ `selected`/`disabled`); `thumbnailUrl` is optional because **`BlockResourceInfo` carries no image field**, and a missing *or failed* image falls back to a frozen "No preview" frame. `actions` is the trailing flow slot on both variants; `overlay` is the decorative corner badge over the thumbnail and is **`card`-only — a type error on a `row`**. Both render as siblings of the hit area, never inside it. The name fallback, type label, placeholder copy, selected mark and accessible-name order are frozen, not props. |
|
|
928
1010
|
|
|
929
1011
|
Every component carries a `data-civitai-ui="<name>"` hook. Most also forward
|
|
@@ -985,6 +1067,7 @@ Runnable, minimal blocks — one per feature, each with its own README:
|
|
|
985
1067
|
|
|
986
1068
|
| `@civitai/blocks-react` | pairs with `@civitai/app-sdk` | adds |
|
|
987
1069
|
|---|---|---|
|
|
1070
|
+
| `0.48.x` | `^0.38.0` | `useCollectionFollow()` + `<FollowButton>` / `<TipButton>` (`SET_COLLECTION_FOLLOW`). 🔴 The `peerDependencies` floor stays the deliberately-wide `>=0.29.0 <1.0.0` (#206 — a per-minor floor forced a major on consumers), so **npm will not warn you**: pairing this with an SDK below `0.38.0` fails at `tsc` with `Cannot find name 'BlockCollectionFollowErrorCode'`, not at install. |
|
|
988
1071
|
| `0.36.x` | `^0.27.0` | auto-installs the SDK's opaque-origin web-storage shim (`@civitai/app-sdk/safe-storage`) on import |
|
|
989
1072
|
| `0.29.x` | `^0.24.0` | `useAppWorkflows()` — app generator subqueue read + cancel (`QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW`) |
|
|
990
1073
|
| `0.27.x`–`0.28.x` | `^0.23.0` | async-scan image upload; transport validators for all `SHARED_*` / `APP_STORAGE_*` / picker replies |
|
package/dist/hooks/SfwGate.d.ts
CHANGED
|
@@ -8,25 +8,32 @@ export interface SfwGateProps {
|
|
|
8
8
|
/**
|
|
9
9
|
* When set, gate on `isLevelAllowed(level)` (a single `BrowsingLevel` bit)
|
|
10
10
|
* instead of the coarse `isSfw`. Lets a block reveal a level-specific
|
|
11
|
-
* affordance (e.g. an R-rated toggle) only when
|
|
11
|
+
* affordance (e.g. an R-rated toggle) only when that level is permitted —
|
|
12
|
+
* by the domain AND by the viewer's own browsing-level setting.
|
|
12
13
|
*/
|
|
13
14
|
level?: number;
|
|
14
15
|
/** Rendered when the gate is closed. Defaults to `null` (render nothing). */
|
|
15
16
|
fallback?: ReactNode;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
|
-
* Convenience wrapper that renders `children` only when the
|
|
19
|
-
*
|
|
20
|
-
* affordances
|
|
19
|
+
* Convenience wrapper that renders `children` only when the current viewer may
|
|
20
|
+
* be shown them here, else `fallback` — so a block can hide/blur mature
|
|
21
|
+
* affordances without wiring {@link useDomainMaturity} by hand.
|
|
21
22
|
*
|
|
22
|
-
* Gating
|
|
23
|
-
*
|
|
23
|
+
* Gating (both delegate to the hook, so both account for the DOMAIN's ceiling
|
|
24
|
+
* AND the VIEWER's own browsing level — see `effectiveBrowsingLevel`):
|
|
25
|
+
* - no `level` prop → renders `children` when nothing mature may be shown
|
|
26
|
+
* (`isSfw`).
|
|
24
27
|
* - `level` prop set → renders `children` when that browsing-level bit is
|
|
25
|
-
*
|
|
28
|
+
* permitted (`isLevelAllowed(level)`).
|
|
29
|
+
*
|
|
30
|
+
* 🔴 A red-domain viewer who turned NSFW off closes this gate. That is the
|
|
31
|
+
* point: before the per-viewer ceiling existed, the gate opened for everyone on
|
|
32
|
+
* a mature domain regardless of their own setting.
|
|
26
33
|
*
|
|
27
34
|
* **Fail-closed SFW**: before `BLOCK_INIT` lands, and against a host that
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* projects no ceiling at all, the gate is treated as SFW — `children` show only
|
|
36
|
+
* for SFW content, mature content shows `fallback`.
|
|
30
37
|
*
|
|
31
38
|
* @example
|
|
32
39
|
* // Hide a mature-only carousel on a SFW domain:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SfwGate.d.ts","sourceRoot":"","sources":["../../src/hooks/SfwGate.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,SAAS,CAAC;IACpB
|
|
1
|
+
{"version":3,"file":"SfwGate.d.ts","sourceRoot":"","sources":["../../src/hooks/SfwGate.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAe,EAAE,EAAE,YAAY,GAAG,SAAS,CAIrF"}
|
package/dist/hooks/SfwGate.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { useDomainMaturity } from './useDomainMaturity.js';
|
|
2
2
|
/**
|
|
3
|
-
* Convenience wrapper that renders `children` only when the
|
|
4
|
-
*
|
|
5
|
-
* affordances
|
|
3
|
+
* Convenience wrapper that renders `children` only when the current viewer may
|
|
4
|
+
* be shown them here, else `fallback` — so a block can hide/blur mature
|
|
5
|
+
* affordances without wiring {@link useDomainMaturity} by hand.
|
|
6
6
|
*
|
|
7
|
-
* Gating
|
|
8
|
-
*
|
|
7
|
+
* Gating (both delegate to the hook, so both account for the DOMAIN's ceiling
|
|
8
|
+
* AND the VIEWER's own browsing level — see `effectiveBrowsingLevel`):
|
|
9
|
+
* - no `level` prop → renders `children` when nothing mature may be shown
|
|
10
|
+
* (`isSfw`).
|
|
9
11
|
* - `level` prop set → renders `children` when that browsing-level bit is
|
|
10
|
-
*
|
|
12
|
+
* permitted (`isLevelAllowed(level)`).
|
|
13
|
+
*
|
|
14
|
+
* 🔴 A red-domain viewer who turned NSFW off closes this gate. That is the
|
|
15
|
+
* point: before the per-viewer ceiling existed, the gate opened for everyone on
|
|
16
|
+
* a mature domain regardless of their own setting.
|
|
11
17
|
*
|
|
12
18
|
* **Fail-closed SFW**: before `BLOCK_INIT` lands, and against a host that
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* projects no ceiling at all, the gate is treated as SFW — `children` show only
|
|
20
|
+
* for SFW content, mature content shows `fallback`.
|
|
15
21
|
*
|
|
16
22
|
* @example
|
|
17
23
|
* // Hide a mature-only carousel on a SFW domain:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SfwGate.js","sourceRoot":"","sources":["../../src/hooks/SfwGate.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"SfwGate.js","sourceRoot":"","sources":["../../src/hooks/SfwGate.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAmB3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAgB;IACxE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,iBAAiB,EAAE,CAAC;IACtD,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { BlockCollectionFollowErrorCode, BlockCollectionFollowResult } from '@civitai/app-sdk/blocks';
|
|
2
|
+
export type { BlockCollectionFollowErrorCode, BlockCollectionFollowResult };
|
|
3
|
+
/**
|
|
4
|
+
* The closed set of HOST refusal codes, as a runtime Set.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 THIS EXISTS BECAUSE THE ERROR CHANNEL IS NOT AN ENUM. The host sends either
|
|
7
|
+
* one of these codes or a free-text server message, so "is this a code?" is a
|
|
8
|
+
* MEMBERSHIP question at runtime, not a type-level one — a `switch` over the
|
|
9
|
+
* union type would silently treat `"You do not have permission…"` as unmatched
|
|
10
|
+
* prose while a typo'd literal compiled fine. Derived from the array below so
|
|
11
|
+
* the two cannot drift.
|
|
12
|
+
*/
|
|
13
|
+
export declare const COLLECTION_FOLLOW_ERROR_CODES: readonly ["invalid-request", "sign-in-required", "review-mode", "not-ready", "declined", "collection-unavailable"];
|
|
14
|
+
/**
|
|
15
|
+
* `true` when `error` is one of the host's CLOSED refusal codes rather than a
|
|
16
|
+
* free-text server message.
|
|
17
|
+
*
|
|
18
|
+
* Use it before comparing against a code — see
|
|
19
|
+
* {@link COLLECTION_FOLLOW_ERROR_CODES} for why equality alone is not enough.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isCollectionFollowErrorCode(error: string): error is BlockCollectionFollowErrorCode;
|
|
22
|
+
/**
|
|
23
|
+
* A collection follow/unfollow failure.
|
|
24
|
+
*
|
|
25
|
+
* `.code` is the host's refusal code when the host refused, and `undefined`
|
|
26
|
+
* otherwise.
|
|
27
|
+
*
|
|
28
|
+
* 🔴 `.code === undefined` IS NOT BY ITSELF "A SERVER MESSAGE WORTH SHOWING" —
|
|
29
|
+
* that reading is why {@link CollectionFollowError.timedOut} exists, and this
|
|
30
|
+
* paragraph asserted it until an audit caught it. TWO different failures land
|
|
31
|
+
* here: a server message the host forwarded verbatim, and a TRANSPORT TIMEOUT
|
|
32
|
+
* whose `.message` is an SDK-internal string. **Check `.timedOut` first**;
|
|
33
|
+
* `.code === undefined && !timedOut` is the branch whose `.message` is meant to
|
|
34
|
+
* be rendered.
|
|
35
|
+
*/
|
|
36
|
+
export declare class CollectionFollowError extends Error {
|
|
37
|
+
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
38
|
+
readonly code?: BlockCollectionFollowErrorCode;
|
|
39
|
+
/**
|
|
40
|
+
* The SDK transport gave up waiting — no reply ever arrived.
|
|
41
|
+
*
|
|
42
|
+
* 🔴 CHECK THIS BEFORE SHOWING `.message`. A timeout also has
|
|
43
|
+
* `code === undefined`, so "no code ⇒ a server message worth rendering" is
|
|
44
|
+
* false, and acting on it puts an SDK-internal string
|
|
45
|
+
* (`IframeTransport: request "SET_COLLECTION_FOLLOW" timed out after 600000ms`)
|
|
46
|
+
* in front of a viewer. 🔴 It also does NOT mean no write occurred: the host
|
|
47
|
+
* may have completed the follow and failed only to deliver the reply, so do
|
|
48
|
+
* not present a timeout as "nothing happened" — re-read your own state.
|
|
49
|
+
*/
|
|
50
|
+
readonly timedOut: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The viewer DISMISSED the host's consent confirm, so NO WRITE OCCURRED.
|
|
53
|
+
*
|
|
54
|
+
* 🔴 Not an error condition to shout about, and it is trustworthy in the one
|
|
55
|
+
* direction that matters: the host takes its consent latch synchronously
|
|
56
|
+
* before the write, so `declined` can never be reported for a follow that
|
|
57
|
+
* landed. Revert optimistic state and render nothing.
|
|
58
|
+
*/
|
|
59
|
+
readonly declined: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* There is no session. Route this into `useRequestSignIn()` rather than
|
|
62
|
+
* showing an error — the viewer's next step is signing in, not retrying.
|
|
63
|
+
*/
|
|
64
|
+
readonly signInRequired: boolean;
|
|
65
|
+
constructor(error: string, opts?: {
|
|
66
|
+
timedOut?: boolean;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** What {@link useCollectionFollow} returns. */
|
|
70
|
+
export interface UseCollectionFollow {
|
|
71
|
+
/**
|
|
72
|
+
* Ask the host to follow (`follow: true`) or unfollow (`false`) `collectionId`
|
|
73
|
+
* for the viewer, and resolve with the host's echo of what it wrote.
|
|
74
|
+
*
|
|
75
|
+
* REJECTS with a {@link CollectionFollowError} on every non-success — including
|
|
76
|
+
* `declined`, which means the viewer dismissed the confirm and nothing was
|
|
77
|
+
* written. Check `.declined` before rendering a failure.
|
|
78
|
+
*/
|
|
79
|
+
setFollow: (args: {
|
|
80
|
+
collectionId: number;
|
|
81
|
+
follow: boolean;
|
|
82
|
+
}) => Promise<BlockCollectionFollowResult>;
|
|
83
|
+
/** `true` while a follow request is in flight (including the viewer's confirm). */
|
|
84
|
+
pending: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The last request's failure, or `null`. Cleared at the start of the next
|
|
87
|
+
* `setFollow`. A dismissal lands here too — read `.declined`.
|
|
88
|
+
*/
|
|
89
|
+
error: CollectionFollowError | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Follow / unfollow a collection for the viewer through the host-mediated
|
|
93
|
+
* `SET_COLLECTION_FOLLOW` → `COLLECTION_FOLLOW_RESULT` bridge.
|
|
94
|
+
*
|
|
95
|
+
* TOKEN-INDEPENDENT — this needs NO block scope and sends no token. The host
|
|
96
|
+
* calls the session-authed `collection.follow` / `collection.unfollow`
|
|
97
|
+
* procedures, which self-bind to the viewer server-side, so `collectionId` is
|
|
98
|
+
* the only thing a block influences.
|
|
99
|
+
*
|
|
100
|
+
* 🔴 EVERY CALL OPENS A HOST-CHROME CONSENT CONFIRM NAMING THE COLLECTION, and
|
|
101
|
+
* that click is the ONLY consent this path has ever had. The HTTP predecessor's
|
|
102
|
+
* `collections:write:self` scope was consent-exempt server-side and prompted
|
|
103
|
+
* nobody, so moving to this bridge TIGHTENS the path — it does not loosen it.
|
|
104
|
+
* What it costs is the manifest `scopes` declaration a moderator reads before
|
|
105
|
+
* install. Do not add a "skip the confirm" option; there is nothing behind it.
|
|
106
|
+
*
|
|
107
|
+
* Because the reply waits on a person, the request carries
|
|
108
|
+
* {@link HUMAN_INTERACTION_TIMEOUT_MS} (10 min) rather than the ~30s protocol
|
|
109
|
+
* default — it resolves the instant the viewer acts, and the ceiling only bounds
|
|
110
|
+
* an abandoned dialog.
|
|
111
|
+
*
|
|
112
|
+
* 🔴 THE HOST RESOLVES THE COLLECTION'S NAME ITSELF and bounds that to 20
|
|
113
|
+
* DISTINCT ids per block instance. Past the cap it refuses with
|
|
114
|
+
* `collection-unavailable` — the SAME code a collection the viewer cannot see
|
|
115
|
+
* gets — so a block driving many ids degrades into "we cannot act on this",
|
|
116
|
+
* never into an enumeration oracle. Repeats of an already-admitted id are free
|
|
117
|
+
* forever, so re-following a collection the viewer has already been asked about
|
|
118
|
+
* keeps working.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const { setFollow, pending } = useCollectionFollow();
|
|
122
|
+
* const { requestSignIn } = useRequestSignIn();
|
|
123
|
+
*
|
|
124
|
+
* const toggle = async () => {
|
|
125
|
+
* setFollowed((f) => !f); // optimistic
|
|
126
|
+
* try {
|
|
127
|
+
* const r = await setFollow({ collectionId, follow: !followed });
|
|
128
|
+
* setFollowed(r.followed); // adopt the host's echo, not the guess
|
|
129
|
+
* } catch (e) {
|
|
130
|
+
* setFollowed(followed); // roll back
|
|
131
|
+
* if (e instanceof CollectionFollowError) {
|
|
132
|
+
* if (e.signInRequired) return requestSignIn();
|
|
133
|
+
* if (e.declined) return; // the viewer said no — say nothing
|
|
134
|
+
* if (e.timedOut) return showToast('Still working — check back in a moment.');
|
|
135
|
+
* showToast(e.message); // a real server message, safe to render
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* };
|
|
139
|
+
*/
|
|
140
|
+
export declare function useCollectionFollow(): UseCollectionFollow;
|
|
141
|
+
//# sourceMappingURL=useCollectionFollow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollectionFollow.d.ts","sourceRoot":"","sources":["../../src/hooks/useCollectionFollow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,2BAA2B,EAC5B,MAAM,yBAAyB,CAAC;AAMjC,YAAY,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,CAAC;AAE5E;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,oHAOoB,CAAC;AAI/D;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,8BAA8B,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,8BAA8B,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;gBAErB,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE;CAQzD;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,OAAO,CAAC;KACjB,KAAK,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAC3C,mFAAmF;IACnF,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,KAAK,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,mBAAmB,IAAI,mBAAmB,CA2EzD"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../internal/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../internal/singleton.js';
|
|
4
|
+
import { RequestTimeoutError, sendTypedRequest } from '../internal/transport.js';
|
|
5
|
+
/**
|
|
6
|
+
* The closed set of HOST refusal codes, as a runtime Set.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 THIS EXISTS BECAUSE THE ERROR CHANNEL IS NOT AN ENUM. The host sends either
|
|
9
|
+
* one of these codes or a free-text server message, so "is this a code?" is a
|
|
10
|
+
* MEMBERSHIP question at runtime, not a type-level one — a `switch` over the
|
|
11
|
+
* union type would silently treat `"You do not have permission…"` as unmatched
|
|
12
|
+
* prose while a typo'd literal compiled fine. Derived from the array below so
|
|
13
|
+
* the two cannot drift.
|
|
14
|
+
*/
|
|
15
|
+
export const COLLECTION_FOLLOW_ERROR_CODES = [
|
|
16
|
+
'invalid-request',
|
|
17
|
+
'sign-in-required',
|
|
18
|
+
'review-mode',
|
|
19
|
+
'not-ready',
|
|
20
|
+
'declined',
|
|
21
|
+
'collection-unavailable',
|
|
22
|
+
];
|
|
23
|
+
const CODE_SET = new Set(COLLECTION_FOLLOW_ERROR_CODES);
|
|
24
|
+
/**
|
|
25
|
+
* `true` when `error` is one of the host's CLOSED refusal codes rather than a
|
|
26
|
+
* free-text server message.
|
|
27
|
+
*
|
|
28
|
+
* Use it before comparing against a code — see
|
|
29
|
+
* {@link COLLECTION_FOLLOW_ERROR_CODES} for why equality alone is not enough.
|
|
30
|
+
*/
|
|
31
|
+
export function isCollectionFollowErrorCode(error) {
|
|
32
|
+
return CODE_SET.has(error);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A collection follow/unfollow failure.
|
|
36
|
+
*
|
|
37
|
+
* `.code` is the host's refusal code when the host refused, and `undefined`
|
|
38
|
+
* otherwise.
|
|
39
|
+
*
|
|
40
|
+
* 🔴 `.code === undefined` IS NOT BY ITSELF "A SERVER MESSAGE WORTH SHOWING" —
|
|
41
|
+
* that reading is why {@link CollectionFollowError.timedOut} exists, and this
|
|
42
|
+
* paragraph asserted it until an audit caught it. TWO different failures land
|
|
43
|
+
* here: a server message the host forwarded verbatim, and a TRANSPORT TIMEOUT
|
|
44
|
+
* whose `.message` is an SDK-internal string. **Check `.timedOut` first**;
|
|
45
|
+
* `.code === undefined && !timedOut` is the branch whose `.message` is meant to
|
|
46
|
+
* be rendered.
|
|
47
|
+
*/
|
|
48
|
+
export class CollectionFollowError extends Error {
|
|
49
|
+
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
50
|
+
code;
|
|
51
|
+
/**
|
|
52
|
+
* The SDK transport gave up waiting — no reply ever arrived.
|
|
53
|
+
*
|
|
54
|
+
* 🔴 CHECK THIS BEFORE SHOWING `.message`. A timeout also has
|
|
55
|
+
* `code === undefined`, so "no code ⇒ a server message worth rendering" is
|
|
56
|
+
* false, and acting on it puts an SDK-internal string
|
|
57
|
+
* (`IframeTransport: request "SET_COLLECTION_FOLLOW" timed out after 600000ms`)
|
|
58
|
+
* in front of a viewer. 🔴 It also does NOT mean no write occurred: the host
|
|
59
|
+
* may have completed the follow and failed only to deliver the reply, so do
|
|
60
|
+
* not present a timeout as "nothing happened" — re-read your own state.
|
|
61
|
+
*/
|
|
62
|
+
timedOut;
|
|
63
|
+
/**
|
|
64
|
+
* The viewer DISMISSED the host's consent confirm, so NO WRITE OCCURRED.
|
|
65
|
+
*
|
|
66
|
+
* 🔴 Not an error condition to shout about, and it is trustworthy in the one
|
|
67
|
+
* direction that matters: the host takes its consent latch synchronously
|
|
68
|
+
* before the write, so `declined` can never be reported for a follow that
|
|
69
|
+
* landed. Revert optimistic state and render nothing.
|
|
70
|
+
*/
|
|
71
|
+
declined;
|
|
72
|
+
/**
|
|
73
|
+
* There is no session. Route this into `useRequestSignIn()` rather than
|
|
74
|
+
* showing an error — the viewer's next step is signing in, not retrying.
|
|
75
|
+
*/
|
|
76
|
+
signInRequired;
|
|
77
|
+
constructor(error, opts) {
|
|
78
|
+
super(error);
|
|
79
|
+
this.name = 'CollectionFollowError';
|
|
80
|
+
this.timedOut = opts?.timedOut === true;
|
|
81
|
+
if (isCollectionFollowErrorCode(error))
|
|
82
|
+
this.code = error;
|
|
83
|
+
this.declined = error === 'declined';
|
|
84
|
+
this.signInRequired = error === 'sign-in-required';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Follow / unfollow a collection for the viewer through the host-mediated
|
|
89
|
+
* `SET_COLLECTION_FOLLOW` → `COLLECTION_FOLLOW_RESULT` bridge.
|
|
90
|
+
*
|
|
91
|
+
* TOKEN-INDEPENDENT — this needs NO block scope and sends no token. The host
|
|
92
|
+
* calls the session-authed `collection.follow` / `collection.unfollow`
|
|
93
|
+
* procedures, which self-bind to the viewer server-side, so `collectionId` is
|
|
94
|
+
* the only thing a block influences.
|
|
95
|
+
*
|
|
96
|
+
* 🔴 EVERY CALL OPENS A HOST-CHROME CONSENT CONFIRM NAMING THE COLLECTION, and
|
|
97
|
+
* that click is the ONLY consent this path has ever had. The HTTP predecessor's
|
|
98
|
+
* `collections:write:self` scope was consent-exempt server-side and prompted
|
|
99
|
+
* nobody, so moving to this bridge TIGHTENS the path — it does not loosen it.
|
|
100
|
+
* What it costs is the manifest `scopes` declaration a moderator reads before
|
|
101
|
+
* install. Do not add a "skip the confirm" option; there is nothing behind it.
|
|
102
|
+
*
|
|
103
|
+
* Because the reply waits on a person, the request carries
|
|
104
|
+
* {@link HUMAN_INTERACTION_TIMEOUT_MS} (10 min) rather than the ~30s protocol
|
|
105
|
+
* default — it resolves the instant the viewer acts, and the ceiling only bounds
|
|
106
|
+
* an abandoned dialog.
|
|
107
|
+
*
|
|
108
|
+
* 🔴 THE HOST RESOLVES THE COLLECTION'S NAME ITSELF and bounds that to 20
|
|
109
|
+
* DISTINCT ids per block instance. Past the cap it refuses with
|
|
110
|
+
* `collection-unavailable` — the SAME code a collection the viewer cannot see
|
|
111
|
+
* gets — so a block driving many ids degrades into "we cannot act on this",
|
|
112
|
+
* never into an enumeration oracle. Repeats of an already-admitted id are free
|
|
113
|
+
* forever, so re-following a collection the viewer has already been asked about
|
|
114
|
+
* keeps working.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const { setFollow, pending } = useCollectionFollow();
|
|
118
|
+
* const { requestSignIn } = useRequestSignIn();
|
|
119
|
+
*
|
|
120
|
+
* const toggle = async () => {
|
|
121
|
+
* setFollowed((f) => !f); // optimistic
|
|
122
|
+
* try {
|
|
123
|
+
* const r = await setFollow({ collectionId, follow: !followed });
|
|
124
|
+
* setFollowed(r.followed); // adopt the host's echo, not the guess
|
|
125
|
+
* } catch (e) {
|
|
126
|
+
* setFollowed(followed); // roll back
|
|
127
|
+
* if (e instanceof CollectionFollowError) {
|
|
128
|
+
* if (e.signInRequired) return requestSignIn();
|
|
129
|
+
* if (e.declined) return; // the viewer said no — say nothing
|
|
130
|
+
* if (e.timedOut) return showToast('Still working — check back in a moment.');
|
|
131
|
+
* showToast(e.message); // a real server message, safe to render
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
* };
|
|
135
|
+
*/
|
|
136
|
+
export function useCollectionFollow() {
|
|
137
|
+
const [pending, setPending] = useState(false);
|
|
138
|
+
const [error, setError] = useState(null);
|
|
139
|
+
// Same guard as `useTip`/`useWildcardPack`: this request can outlive the
|
|
140
|
+
// component by up to ten minutes (the viewer may leave the confirm open), so
|
|
141
|
+
// it is the LONGEST-lived state write in the package. Writing after unmount
|
|
142
|
+
// is not merely a warning here — a `pending` toggled on an unmounted control
|
|
143
|
+
// is state nobody can clear.
|
|
144
|
+
const mountedRef = useRef(true);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
mountedRef.current = true;
|
|
147
|
+
return () => {
|
|
148
|
+
mountedRef.current = false;
|
|
149
|
+
};
|
|
150
|
+
}, []);
|
|
151
|
+
const setFollow = useCallback(async (args) => {
|
|
152
|
+
if (mountedRef.current) {
|
|
153
|
+
setPending(true);
|
|
154
|
+
setError(null);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const reply = await sendTypedRequest(getTransport(), {
|
|
158
|
+
type: 'SET_COLLECTION_FOLLOW',
|
|
159
|
+
payload: { collectionId: args.collectionId, follow: args.follow },
|
|
160
|
+
}, 'COLLECTION_FOLLOW_RESULT',
|
|
161
|
+
// See the `'human'` bucketing in `internal/requestTimeouts.ts`: the
|
|
162
|
+
// host answers only when the viewer clicks or dismisses its confirm.
|
|
163
|
+
{ timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
164
|
+
if (reply.error || !reply.result) {
|
|
165
|
+
// 🔴 `||`, NOT `??` — the opposite of `useWildcardPack`, and for the
|
|
166
|
+
// opposite reason. `isValidCollectionFollowResult` gates `error` on
|
|
167
|
+
// SHAPE only (it has to: the channel carries free-text server
|
|
168
|
+
// messages), so a host `error: ''` is a VALID reply that reaches here.
|
|
169
|
+
// `??` would then throw an Error with an EMPTY message for an account
|
|
170
|
+
// write, which renders as a blank failure. Fall through to a code that
|
|
171
|
+
// at least names a real outcome.
|
|
172
|
+
throw new CollectionFollowError(reply.error || 'collection-unavailable');
|
|
173
|
+
}
|
|
174
|
+
return reply.result;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// A transport timeout arrives as a plain Error; wrap it so callers have
|
|
178
|
+
// ONE error type to test, with `.code` left undefined (it is not a host
|
|
179
|
+
// refusal). Re-wrapping our own error would lose `.code`, so pass it
|
|
180
|
+
// through.
|
|
181
|
+
const wrapped = err instanceof CollectionFollowError
|
|
182
|
+
? err
|
|
183
|
+
: new CollectionFollowError(err instanceof Error ? err.message : String(err), {
|
|
184
|
+
// Structural, not a message match — see `RequestTimeoutError`.
|
|
185
|
+
timedOut: err instanceof RequestTimeoutError,
|
|
186
|
+
});
|
|
187
|
+
if (mountedRef.current)
|
|
188
|
+
setError(wrapped);
|
|
189
|
+
// 🔴 THROWN UNCONDITIONALLY, even when unmounted. The caller's `await`
|
|
190
|
+
// is not the component — an app that persists the result (a toast queue,
|
|
191
|
+
// a store) must still learn the write failed, and swallowing it here
|
|
192
|
+
// would make an unmount look like a success.
|
|
193
|
+
throw wrapped;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (mountedRef.current)
|
|
197
|
+
setPending(false);
|
|
198
|
+
}
|
|
199
|
+
}, []);
|
|
200
|
+
return { setFollow, pending, error };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=useCollectionFollow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollectionFollow.js","sourceRoot":"","sources":["../../src/hooks/useCollectionFollow.ts"],"names":[],"mappings":"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"}
|