@civitai/blocks-react 0.48.0 → 0.50.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 +109 -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/useCreatePostFromApp.d.ts +144 -0
- package/dist/hooks/useCreatePostFromApp.d.ts.map +1 -0
- package/dist/hooks/useCreatePostFromApp.js +213 -0
- package/dist/hooks/useCreatePostFromApp.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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/liveHost.d.ts.map +1 -1
- package/dist/internal/liveHost.js +36 -0
- package/dist/internal/liveHost.js.map +1 -1
- package/dist/internal/mockHost.d.ts +48 -2
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +67 -0
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/requestTimeouts.d.ts.map +1 -1
- package/dist/internal/requestTimeouts.js +9 -0
- package/dist/internal/requestTimeouts.js.map +1 -1
- package/dist/internal/transport.d.ts +9 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +1 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts +23 -0
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +74 -0
- package/dist/internal/validate.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -570,6 +570,80 @@ async function toggle() {
|
|
|
570
570
|
Most blocks want `<FollowButton>` from `@civitai/blocks-react/ui` instead, which
|
|
571
571
|
wires all of the above.
|
|
572
572
|
|
|
573
|
+
### `useCreatePostFromApp()`
|
|
574
|
+
|
|
575
|
+
Publish a **real, published Post on the viewer's profile** from this app's own
|
|
576
|
+
outputs, host-mediated over `CREATE_POST_FROM_APP`. Returns
|
|
577
|
+
`{ createPost, pending, error }`.
|
|
578
|
+
|
|
579
|
+
The strictly-more-consequential sibling of `usePublishGenerationOutputs()`: that
|
|
580
|
+
one makes a bare `Image` row with no post, no feed presence, no reward and no
|
|
581
|
+
notification; this one makes **public, feed-visible, reward-earning content under
|
|
582
|
+
the viewer's byline**.
|
|
583
|
+
|
|
584
|
+
🔴 **Requires the `posts:write:self` scope**, which is **sensitive** and
|
|
585
|
+
**consent-gated**. Declare it in your manifest *with* a `scopeJustifications`
|
|
586
|
+
entry — the server rejects the manifest at submit without one — and expect the
|
|
587
|
+
viewer to be prompted to grant it before the first call succeeds.
|
|
588
|
+
|
|
589
|
+
🔴 **The grant is not the consent.** Every call opens a host-chrome confirm, and
|
|
590
|
+
what it shows is the **server's** resolution of your request, never your strings:
|
|
591
|
+
the tag names that will *actually* be applied, host-fetched model and version
|
|
592
|
+
names for a gallery attach, and real thumbnails. A block cannot show one post and
|
|
593
|
+
publish another.
|
|
594
|
+
|
|
595
|
+
🔴 **No arm of `sources` takes a URL.** Name a workflow from this app's own
|
|
596
|
+
subqueue plus indexes into its outputs, or `Image` ids from a previous
|
|
597
|
+
`usePublishGenerationOutputs()` publish. The server re-verifies both — ownership,
|
|
598
|
+
this app's provenance marker, and that the image is not already in a post.
|
|
599
|
+
|
|
600
|
+
⚠️ **Posting a published image removes it from this app's own grid.** The
|
|
601
|
+
app-scoped read behind `useGatedImages()` is conjoined with `postId IS NULL`, so
|
|
602
|
+
an image that joins a post stops resolving there. An app cannot both keep an
|
|
603
|
+
image in its shared grid and let the viewer post it — design around it.
|
|
604
|
+
|
|
605
|
+
Text is advisory: the server bounds `title`/`detail`, screens them, refuses a
|
|
606
|
+
`detail` containing a link, and resolves `tags` against **existing** tags only (a
|
|
607
|
+
name matching no tag is dropped, never minted, and is shown to the viewer on the
|
|
608
|
+
confirm).
|
|
609
|
+
|
|
610
|
+
`createPost` **rejects** with a `CreatePostError` on every non-success:
|
|
611
|
+
|
|
612
|
+
| | meaning | what to do |
|
|
613
|
+
|---|---|---|
|
|
614
|
+
| `err.declined` | the viewer dismissed the confirm — **no post was created** | revert, say nothing |
|
|
615
|
+
| `err.signInRequired` | no session | route into `useRequestSignIn()` |
|
|
616
|
+
| `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 nothing happened; tell the viewer to check their profile and never retry automatically |
|
|
617
|
+
| `err.code` set otherwise | a host refusal (`review-mode` / `block is not ready` / `no images to post` / `no block token`) | show or ignore per case |
|
|
618
|
+
| `err.code === undefined` **and** `!err.timedOut` | a **server** message the host forwarded verbatim (rate limit, blocked title, refused gallery attach) | show `err.message` |
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
const { createPost, pending } = useCreatePostFromApp();
|
|
622
|
+
const { requestSignIn } = useRequestSignIn();
|
|
623
|
+
|
|
624
|
+
async function share() {
|
|
625
|
+
try {
|
|
626
|
+
const post = await createPost({
|
|
627
|
+
sources: [{ kind: 'workflow', workflowId: w.workflowId, imageIndexes: [0, 2] }],
|
|
628
|
+
title: 'Made with Sticker Studio',
|
|
629
|
+
});
|
|
630
|
+
showToast(`Posted! ${post.url}`);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (err instanceof CreatePostError) {
|
|
633
|
+
if (err.signInRequired) return requestSignIn();
|
|
634
|
+
if (err.declined) return; // the viewer said no — say nothing
|
|
635
|
+
if (err.timedOut) return showToast('Still working — check your profile.');
|
|
636
|
+
showToast(err.message); // a real server message, safe to render
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
In `dev:mock` the `createPostResult` / `createPostError` scenario knobs drive
|
|
643
|
+
both arms (including `declined`). **`dev:live` refuses this bridge on purpose** —
|
|
644
|
+
it has no civitai chrome to render the server-resolved confirm in, and driving
|
|
645
|
+
the write without it would let dev prove out a flow production does not have.
|
|
646
|
+
|
|
573
647
|
### `useAppWorkflows(params?)`
|
|
574
648
|
|
|
575
649
|
The calling app's **own** generator subqueue — the tag-scoped list of generations
|
|
@@ -830,20 +904,48 @@ any request for a scope your dev token lacks produces one.
|
|
|
830
904
|
|
|
831
905
|
### `useDomainMaturity()`
|
|
832
906
|
|
|
833
|
-
Read the
|
|
834
|
-
|
|
835
|
-
|
|
907
|
+
Read the maturity ceiling in force for the current viewer, so a block can
|
|
908
|
+
hide/blur mature affordances. **Fail-closed SFW** until `BLOCK_INIT` lands or
|
|
909
|
+
against a host that projects no ceiling.
|
|
836
910
|
|
|
837
911
|
```tsx
|
|
838
912
|
const { isSfw, isLevelAllowed } = useDomainMaturity();
|
|
839
913
|
const showRSlider = isLevelAllowed(BrowsingLevel.R); // false on a SFW domain
|
|
840
914
|
```
|
|
841
915
|
|
|
916
|
+
The gates account for **two** things, and the distinction matters:
|
|
917
|
+
|
|
918
|
+
| field | answers |
|
|
919
|
+
| --- | --- |
|
|
920
|
+
| `maxBrowsingLevel` | what this **domain** permits anybody (identical for every viewer on it) |
|
|
921
|
+
| `effectiveBrowsingLevel` | what **this viewer** may be shown here — the domain ceiling narrowed by their own NSFW setting |
|
|
922
|
+
|
|
923
|
+
`isSfw` / `isLevelAllowed` gate on the second, so a viewer who turned NSFW off
|
|
924
|
+
sees SFW affordances even on a mature domain. `effectiveBrowsingLevel` is always
|
|
925
|
+
a subset of `maxBrowsingLevel`, so reading these can only ever show the viewer
|
|
926
|
+
**less** — never more. Compare the two when you want to explain *why* something
|
|
927
|
+
is hidden:
|
|
928
|
+
|
|
929
|
+
```tsx
|
|
930
|
+
const { maxBrowsingLevel, effectiveBrowsingLevel } = useDomainMaturity();
|
|
931
|
+
const hiddenByYourSettings = effectiveBrowsingLevel !== maxBrowsingLevel;
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
The hook's name is historic — it shipped when the domain ceiling was the only
|
|
935
|
+
signal. There is deliberately no separate viewer-maturity hook: two hooks would
|
|
936
|
+
mean two answers to "may I show this", and the one named for the domain would be
|
|
937
|
+
the wider of the pair.
|
|
938
|
+
|
|
939
|
+
Drive it locally with `createMockHost({ domain: 'red', viewerBrowsingLevel: BrowsingLevel.PG })`.
|
|
940
|
+
The mock clamps that option to its own ceiling exactly as the real host does, so
|
|
941
|
+
you cannot test against a viewer wider than the domain — production cannot
|
|
942
|
+
produce one either.
|
|
943
|
+
|
|
842
944
|
### `SfwGate`
|
|
843
945
|
|
|
844
|
-
Convenience component that renders `children` only when the
|
|
845
|
-
|
|
846
|
-
browsing-level bit. Fail-closed SFW.
|
|
946
|
+
Convenience component that renders `children` only when the current viewer may be
|
|
947
|
+
shown them — no `level` prop gates on `isSfw`, a `level` prop gates on that
|
|
948
|
+
browsing-level bit. Both account for the domain **and** the viewer. Fail-closed SFW.
|
|
847
949
|
|
|
848
950
|
```tsx
|
|
849
951
|
function MatureSection() {
|
|
@@ -1039,6 +1141,7 @@ Runnable, minimal blocks — one per feature, each with its own README:
|
|
|
1039
1141
|
|
|
1040
1142
|
| `@civitai/blocks-react` | pairs with `@civitai/app-sdk` | adds |
|
|
1041
1143
|
|---|---|---|
|
|
1144
|
+
| `0.50.x` | `^0.40.0` | `useCreatePostFromApp()` (`CREATE_POST_FROM_APP`) + the `posts:write:self` scope. 🔴 Same wide `peerDependencies` floor as the row below, so **npm will not warn you**: pairing this with an SDK below `0.40.0` fails at `tsc` with `Cannot find name 'BlockCreatePostHostError'`, not at install. |
|
|
1042
1145
|
| `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. |
|
|
1043
1146
|
| `0.36.x` | `^0.27.0` | auto-installs the SDK's opaque-origin web-storage shim (`@civitai/app-sdk/safe-storage`) on import |
|
|
1044
1147
|
| `0.29.x` | `^0.24.0` | `useAppWorkflows()` — app generator subqueue read + cancel (`QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW`) |
|
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,144 @@
|
|
|
1
|
+
import type { BlockCreatePostHostError, BlockCreatePostRequest, BlockCreatePostResult, BlockPostSource } from '@civitai/app-sdk/blocks';
|
|
2
|
+
export type { BlockCreatePostHostError, BlockCreatePostRequest, BlockCreatePostResult, BlockPostSource, };
|
|
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 (a rate limit, a blocked
|
|
8
|
+
* title, a refused gallery attach), so "is this a code?" is a MEMBERSHIP
|
|
9
|
+
* question at runtime, not a type-level one — a `switch` over the union type
|
|
10
|
+
* would silently treat `"You do not have permission…"` as unmatched prose while
|
|
11
|
+
* a typo'd literal compiled fine. Derived from the array below so the two cannot
|
|
12
|
+
* drift, and mirrored from civitai/civitai's `CREATE_POST_HOST_ERRORS`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CREATE_POST_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to post", "no images to post", "no block token", "declined"];
|
|
15
|
+
/**
|
|
16
|
+
* `true` when `error` is one of the host's CLOSED refusal codes rather than a
|
|
17
|
+
* free-text server message.
|
|
18
|
+
*
|
|
19
|
+
* Use it before comparing against a code — see {@link CREATE_POST_ERROR_CODES}
|
|
20
|
+
* for why equality alone is not enough.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isCreatePostErrorCode(error: string): error is BlockCreatePostHostError;
|
|
23
|
+
/**
|
|
24
|
+
* A post-creation failure.
|
|
25
|
+
*
|
|
26
|
+
* `.code` is the host's refusal code when the host refused, and `undefined`
|
|
27
|
+
* otherwise.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 `.code === undefined` IS NOT BY ITSELF "A SERVER MESSAGE WORTH SHOWING".
|
|
30
|
+
* TWO different failures land there: a server message the host forwarded
|
|
31
|
+
* verbatim, and a TRANSPORT TIMEOUT whose `.message` is an SDK-internal string.
|
|
32
|
+
* **Check `.timedOut` first**; `.code === undefined && !timedOut` is the branch
|
|
33
|
+
* whose `.message` is meant to be rendered.
|
|
34
|
+
*/
|
|
35
|
+
export declare class CreatePostError extends Error {
|
|
36
|
+
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
37
|
+
readonly code?: BlockCreatePostHostError;
|
|
38
|
+
/**
|
|
39
|
+
* The SDK transport gave up waiting — no reply ever arrived.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 CHECK THIS BEFORE SHOWING `.message`, and 🔴 DO NOT PRESENT IT AS
|
|
42
|
+
* "NOTHING HAPPENED". A timeout also has `code === undefined`, so "no code ⇒ a
|
|
43
|
+
* server message worth rendering" is false and acting on it puts an
|
|
44
|
+
* SDK-internal string in front of a viewer. More importantly the write may
|
|
45
|
+
* have LANDED and only the reply failed to arrive — and here the write is a
|
|
46
|
+
* PUBLIC POST under the viewer's name. Tell the viewer to check their profile;
|
|
47
|
+
* never retry automatically, which is how a duplicate post happens.
|
|
48
|
+
*/
|
|
49
|
+
readonly timedOut: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The viewer DISMISSED the host's consent confirm, so NO POST WAS CREATED.
|
|
52
|
+
*
|
|
53
|
+
* 🔴 Not an error condition to shout about, and it is trustworthy in the one
|
|
54
|
+
* direction that matters: the host takes its consent latch SYNCHRONOUSLY
|
|
55
|
+
* before the write, so `declined` can never be reported for a post that
|
|
56
|
+
* landed. Revert optimistic state and render nothing.
|
|
57
|
+
*/
|
|
58
|
+
readonly declined: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* There is no session. Route this into `useRequestSignIn()` rather than
|
|
61
|
+
* showing an error — the viewer's next step is signing in, not retrying.
|
|
62
|
+
*/
|
|
63
|
+
readonly signInRequired: boolean;
|
|
64
|
+
constructor(error: string, opts?: {
|
|
65
|
+
timedOut?: boolean;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** What {@link useCreatePostFromApp} returns. */
|
|
69
|
+
export interface UseCreatePostFromApp {
|
|
70
|
+
/**
|
|
71
|
+
* Ask the host to publish a REAL Post on the viewer's profile from this app's
|
|
72
|
+
* OWN outputs, and resolve with the created post.
|
|
73
|
+
*
|
|
74
|
+
* REJECTS with a {@link CreatePostError} on every non-success — including
|
|
75
|
+
* `declined`, which means the viewer dismissed the confirm and NO POST EXISTS.
|
|
76
|
+
* Check `.declined` before rendering a failure.
|
|
77
|
+
*/
|
|
78
|
+
createPost: (args: BlockCreatePostRequest) => Promise<BlockCreatePostResult>;
|
|
79
|
+
/** `true` while a request is in flight (including the viewer's confirm). */
|
|
80
|
+
pending: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The last request's failure, or `null`. Cleared at the start of the next
|
|
83
|
+
* `createPost`. A dismissal lands here too — read `.declined`.
|
|
84
|
+
*/
|
|
85
|
+
error: CreatePostError | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Publish a REAL, PUBLISHED Post on the VIEWER'S profile from the calling app's
|
|
89
|
+
* OWN outputs, through the host-mediated `CREATE_POST_FROM_APP` →
|
|
90
|
+
* `CREATE_POST_RESULT` bridge.
|
|
91
|
+
*
|
|
92
|
+
* The strictly-more-consequential sibling of `usePublishGenerationOutputs()`:
|
|
93
|
+
* that one makes a bare `Image` row with no post, no feed presence, no reward
|
|
94
|
+
* and no notification; this one makes public, feed-visible, reward-earning
|
|
95
|
+
* content under the viewer's byline.
|
|
96
|
+
*
|
|
97
|
+
* 🔴 REQUIRES THE `posts:write:self` SCOPE, which is SENSITIVE and
|
|
98
|
+
* CONSENT-GATED. Declare it in your manifest WITH a `scopeJustifications` entry
|
|
99
|
+
* — the server rejects the manifest at submit without one — and expect the
|
|
100
|
+
* viewer to be prompted to grant it before the first call succeeds.
|
|
101
|
+
*
|
|
102
|
+
* 🔴 THE SCOPE GRANT IS NOT THE CONSENT. Every call additionally opens a
|
|
103
|
+
* host-chrome confirm, and what that confirm shows is the SERVER'S resolution of
|
|
104
|
+
* your request, never your strings: the tag names that will ACTUALLY be applied,
|
|
105
|
+
* host-fetched model and version names for a gallery attach, and real
|
|
106
|
+
* thumbnails. A block cannot show one post and publish another.
|
|
107
|
+
*
|
|
108
|
+
* 🔴 NO ARM OF `sources` TAKES A URL. Name a workflow from this app's own
|
|
109
|
+
* subqueue plus indexes into its outputs, or `Image` ids from a previous
|
|
110
|
+
* `usePublishGenerationOutputs()` publish. The server re-verifies both.
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ POSTING A PUBLISHED IMAGE REMOVES IT FROM THIS APP'S OWN GRID. The
|
|
113
|
+
* app-scoped read behind `useGatedImages()` is conjoined with `postId IS NULL`,
|
|
114
|
+
* so an image that joins a post stops resolving there. An app cannot both keep
|
|
115
|
+
* an image in its shared grid and let the viewer post it.
|
|
116
|
+
*
|
|
117
|
+
* Because the reply waits on a person, the request carries
|
|
118
|
+
* {@link HUMAN_INTERACTION_TIMEOUT_MS} (10 min) rather than the ~30s protocol
|
|
119
|
+
* default — it resolves the instant the viewer acts, and the ceiling only bounds
|
|
120
|
+
* an abandoned dialog.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const { createPost, pending } = useCreatePostFromApp();
|
|
124
|
+
* const { requestSignIn } = useRequestSignIn();
|
|
125
|
+
*
|
|
126
|
+
* const share = async () => {
|
|
127
|
+
* try {
|
|
128
|
+
* const post = await createPost({
|
|
129
|
+
* sources: [{ kind: 'workflow', workflowId: w.workflowId, imageIndexes: [0, 2] }],
|
|
130
|
+
* title: 'Made with Sticker Studio',
|
|
131
|
+
* });
|
|
132
|
+
* showToast(`Posted! ${post.url}`);
|
|
133
|
+
* } catch (e) {
|
|
134
|
+
* if (e instanceof CreatePostError) {
|
|
135
|
+
* if (e.signInRequired) return requestSignIn();
|
|
136
|
+
* if (e.declined) return; // the viewer said no — say nothing
|
|
137
|
+
* if (e.timedOut) return showToast('Still working — check your profile.');
|
|
138
|
+
* showToast(e.message); // a real server message, safe to render
|
|
139
|
+
* }
|
|
140
|
+
* }
|
|
141
|
+
* };
|
|
142
|
+
*/
|
|
143
|
+
export declare function useCreatePostFromApp(): UseCreatePostFromApp;
|
|
144
|
+
//# sourceMappingURL=useCreatePostFromApp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCreatePostFromApp.d.ts","sourceRoot":"","sources":["../../src/hooks/useCreatePostFromApp.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,yBAAyB,CAAC;AAMjC,YAAY,EACV,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,GAChB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,sHAOoB,CAAC;AAIzD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,wBAAwB,CAEtF;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,wBAAwB,CAAC;IACzC;;;;;;;;;;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,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7E,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,oBAAoB,IAAI,oBAAoB,CA6E3D"}
|
|
@@ -0,0 +1,213 @@
|
|
|
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 (a rate limit, a blocked
|
|
10
|
+
* title, a refused gallery attach), so "is this a code?" is a MEMBERSHIP
|
|
11
|
+
* question at runtime, not a type-level one — a `switch` over the union type
|
|
12
|
+
* would silently treat `"You do not have permission…"` as unmatched prose while
|
|
13
|
+
* a typo'd literal compiled fine. Derived from the array below so the two cannot
|
|
14
|
+
* drift, and mirrored from civitai/civitai's `CREATE_POST_HOST_ERRORS`.
|
|
15
|
+
*/
|
|
16
|
+
export const CREATE_POST_ERROR_CODES = [
|
|
17
|
+
'review-mode',
|
|
18
|
+
'block is not ready',
|
|
19
|
+
'sign in to post',
|
|
20
|
+
'no images to post',
|
|
21
|
+
'no block token',
|
|
22
|
+
'declined',
|
|
23
|
+
];
|
|
24
|
+
const CODE_SET = new Set(CREATE_POST_ERROR_CODES);
|
|
25
|
+
/**
|
|
26
|
+
* `true` when `error` is one of the host's CLOSED refusal codes rather than a
|
|
27
|
+
* free-text server message.
|
|
28
|
+
*
|
|
29
|
+
* Use it before comparing against a code — see {@link CREATE_POST_ERROR_CODES}
|
|
30
|
+
* for why equality alone is not enough.
|
|
31
|
+
*/
|
|
32
|
+
export function isCreatePostErrorCode(error) {
|
|
33
|
+
return CODE_SET.has(error);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A post-creation failure.
|
|
37
|
+
*
|
|
38
|
+
* `.code` is the host's refusal code when the host refused, and `undefined`
|
|
39
|
+
* otherwise.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 `.code === undefined` IS NOT BY ITSELF "A SERVER MESSAGE WORTH SHOWING".
|
|
42
|
+
* TWO different failures land there: a server message the host forwarded
|
|
43
|
+
* verbatim, and a TRANSPORT TIMEOUT whose `.message` is an SDK-internal string.
|
|
44
|
+
* **Check `.timedOut` first**; `.code === undefined && !timedOut` is the branch
|
|
45
|
+
* whose `.message` is meant to be rendered.
|
|
46
|
+
*/
|
|
47
|
+
export class CreatePostError extends Error {
|
|
48
|
+
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
49
|
+
code;
|
|
50
|
+
/**
|
|
51
|
+
* The SDK transport gave up waiting — no reply ever arrived.
|
|
52
|
+
*
|
|
53
|
+
* 🔴 CHECK THIS BEFORE SHOWING `.message`, and 🔴 DO NOT PRESENT IT AS
|
|
54
|
+
* "NOTHING HAPPENED". A timeout also has `code === undefined`, so "no code ⇒ a
|
|
55
|
+
* server message worth rendering" is false and acting on it puts an
|
|
56
|
+
* SDK-internal string in front of a viewer. More importantly the write may
|
|
57
|
+
* have LANDED and only the reply failed to arrive — and here the write is a
|
|
58
|
+
* PUBLIC POST under the viewer's name. Tell the viewer to check their profile;
|
|
59
|
+
* never retry automatically, which is how a duplicate post happens.
|
|
60
|
+
*/
|
|
61
|
+
timedOut;
|
|
62
|
+
/**
|
|
63
|
+
* The viewer DISMISSED the host's consent confirm, so NO POST WAS CREATED.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 Not an error condition to shout about, and it is trustworthy in the one
|
|
66
|
+
* direction that matters: the host takes its consent latch SYNCHRONOUSLY
|
|
67
|
+
* before the write, so `declined` can never be reported for a post that
|
|
68
|
+
* landed. Revert optimistic state and render nothing.
|
|
69
|
+
*/
|
|
70
|
+
declined;
|
|
71
|
+
/**
|
|
72
|
+
* There is no session. Route this into `useRequestSignIn()` rather than
|
|
73
|
+
* showing an error — the viewer's next step is signing in, not retrying.
|
|
74
|
+
*/
|
|
75
|
+
signInRequired;
|
|
76
|
+
constructor(error, opts) {
|
|
77
|
+
super(error);
|
|
78
|
+
this.name = 'CreatePostError';
|
|
79
|
+
this.timedOut = opts?.timedOut === true;
|
|
80
|
+
if (isCreatePostErrorCode(error))
|
|
81
|
+
this.code = error;
|
|
82
|
+
this.declined = error === 'declined';
|
|
83
|
+
this.signInRequired = error === 'sign in to post';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Publish a REAL, PUBLISHED Post on the VIEWER'S profile from the calling app's
|
|
88
|
+
* OWN outputs, through the host-mediated `CREATE_POST_FROM_APP` →
|
|
89
|
+
* `CREATE_POST_RESULT` bridge.
|
|
90
|
+
*
|
|
91
|
+
* The strictly-more-consequential sibling of `usePublishGenerationOutputs()`:
|
|
92
|
+
* that one makes a bare `Image` row with no post, no feed presence, no reward
|
|
93
|
+
* and no notification; this one makes public, feed-visible, reward-earning
|
|
94
|
+
* content under the viewer's byline.
|
|
95
|
+
*
|
|
96
|
+
* 🔴 REQUIRES THE `posts:write:self` SCOPE, which is SENSITIVE and
|
|
97
|
+
* CONSENT-GATED. Declare it in your manifest WITH a `scopeJustifications` entry
|
|
98
|
+
* — the server rejects the manifest at submit without one — and expect the
|
|
99
|
+
* viewer to be prompted to grant it before the first call succeeds.
|
|
100
|
+
*
|
|
101
|
+
* 🔴 THE SCOPE GRANT IS NOT THE CONSENT. Every call additionally opens a
|
|
102
|
+
* host-chrome confirm, and what that confirm shows is the SERVER'S resolution of
|
|
103
|
+
* your request, never your strings: the tag names that will ACTUALLY be applied,
|
|
104
|
+
* host-fetched model and version names for a gallery attach, and real
|
|
105
|
+
* thumbnails. A block cannot show one post and publish another.
|
|
106
|
+
*
|
|
107
|
+
* 🔴 NO ARM OF `sources` TAKES A URL. Name a workflow from this app's own
|
|
108
|
+
* subqueue plus indexes into its outputs, or `Image` ids from a previous
|
|
109
|
+
* `usePublishGenerationOutputs()` publish. The server re-verifies both.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ POSTING A PUBLISHED IMAGE REMOVES IT FROM THIS APP'S OWN GRID. The
|
|
112
|
+
* app-scoped read behind `useGatedImages()` is conjoined with `postId IS NULL`,
|
|
113
|
+
* so an image that joins a post stops resolving there. An app cannot both keep
|
|
114
|
+
* an image in its shared grid and let the viewer post it.
|
|
115
|
+
*
|
|
116
|
+
* Because the reply waits on a person, the request carries
|
|
117
|
+
* {@link HUMAN_INTERACTION_TIMEOUT_MS} (10 min) rather than the ~30s protocol
|
|
118
|
+
* default — it resolves the instant the viewer acts, and the ceiling only bounds
|
|
119
|
+
* an abandoned dialog.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const { createPost, pending } = useCreatePostFromApp();
|
|
123
|
+
* const { requestSignIn } = useRequestSignIn();
|
|
124
|
+
*
|
|
125
|
+
* const share = async () => {
|
|
126
|
+
* try {
|
|
127
|
+
* const post = await createPost({
|
|
128
|
+
* sources: [{ kind: 'workflow', workflowId: w.workflowId, imageIndexes: [0, 2] }],
|
|
129
|
+
* title: 'Made with Sticker Studio',
|
|
130
|
+
* });
|
|
131
|
+
* showToast(`Posted! ${post.url}`);
|
|
132
|
+
* } catch (e) {
|
|
133
|
+
* if (e instanceof CreatePostError) {
|
|
134
|
+
* if (e.signInRequired) return requestSignIn();
|
|
135
|
+
* if (e.declined) return; // the viewer said no — say nothing
|
|
136
|
+
* if (e.timedOut) return showToast('Still working — check your profile.');
|
|
137
|
+
* showToast(e.message); // a real server message, safe to render
|
|
138
|
+
* }
|
|
139
|
+
* }
|
|
140
|
+
* };
|
|
141
|
+
*/
|
|
142
|
+
export function useCreatePostFromApp() {
|
|
143
|
+
const [pending, setPending] = useState(false);
|
|
144
|
+
const [error, setError] = useState(null);
|
|
145
|
+
// Same guard as `useCollectionFollow`: this request can outlive the component
|
|
146
|
+
// by up to ten minutes (the viewer may leave the confirm open), so a `pending`
|
|
147
|
+
// toggled on an unmounted control is state nobody can clear.
|
|
148
|
+
const mountedRef = useRef(true);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
mountedRef.current = true;
|
|
151
|
+
return () => {
|
|
152
|
+
mountedRef.current = false;
|
|
153
|
+
};
|
|
154
|
+
}, []);
|
|
155
|
+
const createPost = useCallback(async (args) => {
|
|
156
|
+
if (mountedRef.current) {
|
|
157
|
+
setPending(true);
|
|
158
|
+
setError(null);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const reply = await sendTypedRequest(getTransport(), {
|
|
162
|
+
type: 'CREATE_POST_FROM_APP',
|
|
163
|
+
payload: {
|
|
164
|
+
sources: args.sources,
|
|
165
|
+
...(args.title !== undefined ? { title: args.title } : {}),
|
|
166
|
+
...(args.detail !== undefined ? { detail: args.detail } : {}),
|
|
167
|
+
...(args.tags !== undefined ? { tags: args.tags } : {}),
|
|
168
|
+
...(args.modelVersionId !== undefined
|
|
169
|
+
? { modelVersionId: args.modelVersionId }
|
|
170
|
+
: {}),
|
|
171
|
+
},
|
|
172
|
+
}, 'CREATE_POST_RESULT',
|
|
173
|
+
// See the `'human'` bucketing in `internal/requestTimeouts.ts`: the
|
|
174
|
+
// host answers only when the viewer clicks or dismisses its confirm.
|
|
175
|
+
{ timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
176
|
+
if (reply.error || !reply.result) {
|
|
177
|
+
// 🔴 `||`, NOT `??`. `isValidCreatePostResult` gates `error` on SHAPE
|
|
178
|
+
// only (it has to: the channel carries free-text server messages), so
|
|
179
|
+
// a host `error: ''` is a VALID reply that reaches here. `??` would
|
|
180
|
+
// then throw an Error with an EMPTY message for a public post, which
|
|
181
|
+
// renders as a blank failure. Fall through to a code that at least
|
|
182
|
+
// names a real outcome.
|
|
183
|
+
throw new CreatePostError(reply.error || 'no images to post');
|
|
184
|
+
}
|
|
185
|
+
return reply.result;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
// A transport timeout arrives as a plain Error; wrap it so callers have
|
|
189
|
+
// ONE error type to test, with `.code` left undefined (it is not a host
|
|
190
|
+
// refusal). Re-wrapping our own error would lose `.code`, so pass it
|
|
191
|
+
// through.
|
|
192
|
+
const wrapped = err instanceof CreatePostError
|
|
193
|
+
? err
|
|
194
|
+
: new CreatePostError(err instanceof Error ? err.message : String(err), {
|
|
195
|
+
// Structural, not a message match — see `RequestTimeoutError`.
|
|
196
|
+
timedOut: err instanceof RequestTimeoutError,
|
|
197
|
+
});
|
|
198
|
+
if (mountedRef.current)
|
|
199
|
+
setError(wrapped);
|
|
200
|
+
// 🔴 THROWN UNCONDITIONALLY, even when unmounted. The caller's `await`
|
|
201
|
+
// is not the component — an app that persists the result must still
|
|
202
|
+
// learn the write failed, and swallowing it here would make an unmount
|
|
203
|
+
// look like a success.
|
|
204
|
+
throw wrapped;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
if (mountedRef.current)
|
|
208
|
+
setPending(false);
|
|
209
|
+
}
|
|
210
|
+
}, []);
|
|
211
|
+
return { createPost, pending, error };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=useCreatePostFromApp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCreatePostFromApp.js","sourceRoot":"","sources":["../../src/hooks/useCreatePostFromApp.ts"],"names":[],"mappings":"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"}
|