@postrun/react 0.1.0 → 1.0.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 +59 -5
- package/dist/index.cjs +421 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -124
- package/dist/index.d.ts +483 -124
- package/dist/index.js +417 -118
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContext, memo, useMemo, useState, useEffect, Fragment as Fragment$1, useRef, createElement, useContext, useCallback } from 'react';
|
|
3
3
|
import { useInfiniteQuery, useQuery, useMutation, QueryClient } from '@tanstack/react-query';
|
|
4
|
-
import { createPostrunClient, profilesList, profilesGet, profilesCreate, profilesUpdate, profilesDelete, connectionsConnect, connectionsListByProfile, connectionsGet, connectionsListAccounts, connectionsSelect, connectionsDelete,
|
|
4
|
+
import { createPostrunClient, profilesList, profilesGet, profilesCreate, profilesUpdate, profilesDelete, connectionsConnect, connectionsListByProfile, connectionsGet, connectionsListAccounts, connectionsSelect, connectionsDelete, mediaGet, mediaList, mediaUpdate, mediaDelete, postsList, postsGet, postsCreate, buildCreatePost, isPostPlatform, postsUpdate, postsDelete, mediaCreate, PostrunError } from '@postrun/js';
|
|
5
|
+
import Nango, { AuthError } from '@nangohq/frontend';
|
|
6
|
+
import pWaitFor, { TimeoutError } from 'p-wait-for';
|
|
7
|
+
import pLimit from 'p-limit';
|
|
5
8
|
import pRetry, { AbortError } from 'p-retry';
|
|
6
|
-
import pWaitFor from 'p-wait-for';
|
|
7
9
|
import axios, { isAxiosError } from 'axios';
|
|
8
10
|
import { enrichTweet, TweetContainer, TweetHeader, TweetInReplyTo, TweetBody, TweetMedia, QuotedTweet } from 'react-tweet';
|
|
9
11
|
import { FiMessageCircle, FiRepeat, FiHeart, FiBarChart2, FiShare, FiGlobe, FiUsers, FiThumbsUp, FiMessageSquare, FiSend } from 'react-icons/fi';
|
|
@@ -89,7 +91,8 @@ var profileKeys = {
|
|
|
89
91
|
list: (query) => [...profileKeys.lists(), query ?? {}],
|
|
90
92
|
// Nested under lists() so a create/update/delete invalidating lists() also
|
|
91
93
|
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
92
|
-
// single Page vs accumulated pages) never collide on one key.
|
|
94
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
95
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
93
96
|
infinite: (query) => [...profileKeys.lists(), "infinite", query ?? {}],
|
|
94
97
|
details: () => [...profileKeys.all, "detail"],
|
|
95
98
|
detail: (id) => [...profileKeys.details(), id]
|
|
@@ -100,20 +103,28 @@ var postKeys = {
|
|
|
100
103
|
list: (query) => [...postKeys.lists(), query ?? {}],
|
|
101
104
|
// Nested under lists() so a create/update/delete invalidating lists() also
|
|
102
105
|
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
103
|
-
// single Page vs accumulated pages) never collide on one key.
|
|
106
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
107
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
104
108
|
infinite: (query) => [...postKeys.lists(), "infinite", query ?? {}],
|
|
105
109
|
details: () => [...postKeys.all, "detail"],
|
|
106
110
|
detail: (id) => [...postKeys.details(), id]
|
|
107
111
|
};
|
|
108
112
|
var mediaKeys = {
|
|
109
113
|
all: [ROOT, "media"],
|
|
114
|
+
lists: () => [...mediaKeys.all, "list"],
|
|
115
|
+
list: (query) => [...mediaKeys.lists(), query ?? {}],
|
|
116
|
+
// Nested under lists() so an upload/update/delete invalidating lists() also
|
|
117
|
+
// refreshes the infinite cache; distinct tail so the two cache shapes (a
|
|
118
|
+
// single Page vs accumulated pages) never collide on one key. The filter omits
|
|
119
|
+
// limit/offset — the infinite hook owns pagination, so they never key the cache.
|
|
120
|
+
infinite: (query) => [...mediaKeys.lists(), "infinite", query ?? {}],
|
|
110
121
|
details: () => [...mediaKeys.all, "detail"],
|
|
111
122
|
detail: (id) => [...mediaKeys.details(), id]
|
|
112
123
|
};
|
|
113
124
|
var connectionKeys = {
|
|
114
125
|
all: [ROOT, "connections"],
|
|
115
126
|
lists: () => [...connectionKeys.all, "list"],
|
|
116
|
-
list: (profileId) => [...connectionKeys.lists(), profileId],
|
|
127
|
+
list: (profileId, filter) => [...connectionKeys.lists(), profileId, filter ?? {}],
|
|
117
128
|
details: () => [...connectionKeys.all, "detail"],
|
|
118
129
|
detail: (id) => [...connectionKeys.details(), id],
|
|
119
130
|
accounts: (id) => [...connectionKeys.all, "accounts", id]
|
|
@@ -185,36 +196,256 @@ function useDeleteProfile() {
|
|
|
185
196
|
queryClient
|
|
186
197
|
);
|
|
187
198
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
var PENDING = { status: "connected_pending" };
|
|
200
|
+
var CANCELLED = { status: "cancelled" };
|
|
201
|
+
var active = (connection) => ({
|
|
202
|
+
status: "active",
|
|
203
|
+
connection
|
|
204
|
+
});
|
|
205
|
+
var failed = (reason) => ({
|
|
206
|
+
status: "error",
|
|
207
|
+
reason
|
|
208
|
+
});
|
|
209
|
+
function outcomeForAuthError(error) {
|
|
210
|
+
switch (error.type) {
|
|
211
|
+
case "window_closed":
|
|
212
|
+
return CANCELLED;
|
|
213
|
+
case "blocked_by_browser":
|
|
214
|
+
return failed("popup_blocked");
|
|
215
|
+
case "missing_auth_token":
|
|
216
|
+
case "invalid_host_url":
|
|
217
|
+
case "missing_credentials":
|
|
218
|
+
case "connection_test_failed":
|
|
219
|
+
case "missing_connect_session_token":
|
|
220
|
+
case "connection_validation_failed":
|
|
221
|
+
case "resource_capped":
|
|
222
|
+
case "unknown_error":
|
|
223
|
+
return failed("auth_failed");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function grant(authorize) {
|
|
227
|
+
try {
|
|
228
|
+
return { ok: true, connectionId: await authorize() };
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error instanceof AuthError) {
|
|
231
|
+
return { ok: false, outcome: outcomeForAuthError(error) };
|
|
232
|
+
}
|
|
233
|
+
return { ok: false, outcome: failed("auth_failed") };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function awaitGrantedConnection(deps, nangoConnectionId) {
|
|
237
|
+
try {
|
|
238
|
+
return await pWaitFor(
|
|
239
|
+
async () => {
|
|
240
|
+
const rows = await deps.listByNangoConnectionId(nangoConnectionId);
|
|
241
|
+
const match = rows[0];
|
|
242
|
+
return match ? pWaitFor.resolveWith(match) : false;
|
|
243
|
+
},
|
|
244
|
+
{ interval: deps.pollIntervalMs, timeout: deps.pollTimeoutMs }
|
|
245
|
+
);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof TimeoutError) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function bindPendingConnection(deps, connection) {
|
|
254
|
+
let accounts;
|
|
255
|
+
try {
|
|
256
|
+
accounts = await deps.discoverAccounts(connection.id);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof PostrunError && error.code === "not_implemented") {
|
|
259
|
+
return PENDING;
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
if (accounts.length === 0) {
|
|
264
|
+
return PENDING;
|
|
265
|
+
}
|
|
266
|
+
const chosen = await deps.chooseAccount(accounts);
|
|
267
|
+
try {
|
|
268
|
+
return active(await deps.selectAccount(connection.id, chosen));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof PostrunError) {
|
|
271
|
+
return error.code === "connection_reauth_required" ? failed("reauth_required") : failed("select_failed");
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function runEmbeddedConnect(deps) {
|
|
277
|
+
const granted = await grant(deps.authorize);
|
|
278
|
+
if (!granted.ok) {
|
|
279
|
+
return granted.outcome;
|
|
280
|
+
}
|
|
281
|
+
let connection;
|
|
282
|
+
try {
|
|
283
|
+
connection = await awaitGrantedConnection(deps, granted.connectionId);
|
|
284
|
+
} catch {
|
|
285
|
+
return failed("connection_not_found");
|
|
286
|
+
}
|
|
287
|
+
if (connection === null) {
|
|
288
|
+
return PENDING;
|
|
289
|
+
}
|
|
290
|
+
if (connection.external_account_id !== null) {
|
|
291
|
+
return active(connection);
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
return await bindPendingConnection(deps, connection);
|
|
295
|
+
} catch {
|
|
296
|
+
return failed("select_failed");
|
|
297
|
+
}
|
|
192
298
|
}
|
|
193
299
|
|
|
194
300
|
// src/connections.ts
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
301
|
+
var POLL_INTERVAL_MS = 1500;
|
|
302
|
+
var POLL_TIMEOUT_MS = 15e3;
|
|
303
|
+
function useConnect({
|
|
304
|
+
profileId,
|
|
305
|
+
platform,
|
|
306
|
+
onConnected
|
|
307
|
+
}) {
|
|
308
|
+
const { client } = usePostrun();
|
|
309
|
+
const [state, setState] = useState({ phase: "preparing" });
|
|
310
|
+
const [remintNonce, setRemintNonce] = useState(0);
|
|
311
|
+
const sessionRef = useRef(null);
|
|
312
|
+
const pickRef = useRef(null);
|
|
313
|
+
const inFlightRef = useRef(false);
|
|
314
|
+
const flowGenRef = useRef(0);
|
|
315
|
+
const onConnectedRef = useRef(onConnected);
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
onConnectedRef.current = onConnected;
|
|
318
|
+
}, [onConnected]);
|
|
319
|
+
const abandonFlow = useCallback(() => {
|
|
320
|
+
flowGenRef.current += 1;
|
|
321
|
+
inFlightRef.current = false;
|
|
322
|
+
const pick = pickRef.current;
|
|
323
|
+
pickRef.current = null;
|
|
324
|
+
pick?.reject(new Error("connect flow abandoned"));
|
|
325
|
+
}, []);
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
let abandoned = false;
|
|
328
|
+
setState({ phase: "preparing" });
|
|
329
|
+
sessionRef.current = null;
|
|
330
|
+
connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
|
|
331
|
+
if (abandoned || !data) return;
|
|
332
|
+
sessionRef.current = {
|
|
333
|
+
token: data.connect_session_token,
|
|
334
|
+
providerConfigKey: data.provider_config_key,
|
|
335
|
+
host: data.nango_host
|
|
336
|
+
};
|
|
337
|
+
setState({ phase: "idle" });
|
|
338
|
+
}).catch(() => {
|
|
339
|
+
if (!abandoned) setState({ phase: "error", reason: "auth_failed" });
|
|
340
|
+
});
|
|
341
|
+
return () => {
|
|
342
|
+
abandoned = true;
|
|
343
|
+
abandonFlow();
|
|
344
|
+
};
|
|
345
|
+
}, [client, profileId, platform, remintNonce, abandonFlow]);
|
|
346
|
+
const start = useCallback(() => {
|
|
347
|
+
const session = sessionRef.current;
|
|
348
|
+
if (!session || inFlightRef.current) return;
|
|
349
|
+
inFlightRef.current = true;
|
|
350
|
+
const gen = flowGenRef.current;
|
|
351
|
+
const isCurrent = () => flowGenRef.current === gen;
|
|
352
|
+
setState({ phase: "connecting" });
|
|
353
|
+
void runEmbeddedConnect({
|
|
354
|
+
// Nango lives INSIDE `authorize` so a SYNCHRONOUS throw (invalid host /
|
|
355
|
+
// missing token — the Nango SDK throws `AuthError` synchronously) becomes a
|
|
356
|
+
// promise rejection that `grant()` maps to `auth_failed`, never an uncaught
|
|
357
|
+
// throw escaping the click and wedging `inFlightRef`. Gesture timing still
|
|
358
|
+
// holds: `authorize()` is invoked SYNCHRONOUSLY down the
|
|
359
|
+
// start → runEmbeddedConnect → grant chain (each `await`'s operand is
|
|
360
|
+
// evaluated before it suspends), so `nango.auth()`'s `window.open` fires
|
|
361
|
+
// inside the user gesture, with no `await` before it.
|
|
362
|
+
authorize: async () => {
|
|
363
|
+
const nango = new Nango({
|
|
364
|
+
host: session.host,
|
|
365
|
+
connectSessionToken: session.token
|
|
366
|
+
});
|
|
367
|
+
const result = await nango.auth(session.providerConfigKey, {
|
|
368
|
+
detectClosedAuthWindow: true
|
|
369
|
+
});
|
|
370
|
+
return result.connectionId;
|
|
371
|
+
},
|
|
372
|
+
chooseAccount: (accounts) => new Promise((resolve, reject) => {
|
|
373
|
+
if (!isCurrent()) {
|
|
374
|
+
reject(new Error("connect flow abandoned"));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
pickRef.current = { resolve, reject };
|
|
378
|
+
setState({ phase: "picking", accounts });
|
|
379
|
+
}),
|
|
380
|
+
listByNangoConnectionId: async (nangoConnectionId) => {
|
|
381
|
+
const { data } = await connectionsListByProfile({
|
|
201
382
|
client,
|
|
202
383
|
path: { id: profileId },
|
|
203
|
-
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
|
|
384
|
+
query: { nango_connection_id: nangoConnectionId }
|
|
385
|
+
});
|
|
386
|
+
return data?.data ?? [];
|
|
387
|
+
},
|
|
388
|
+
discoverAccounts: async (connectionId) => {
|
|
389
|
+
const { data } = await connectionsListAccounts({
|
|
390
|
+
client,
|
|
391
|
+
path: { id: connectionId }
|
|
392
|
+
});
|
|
393
|
+
return data?.data ?? [];
|
|
394
|
+
},
|
|
395
|
+
selectAccount: async (connectionId, externalAccountId) => {
|
|
396
|
+
const { data } = await connectionsSelect({
|
|
397
|
+
client,
|
|
398
|
+
path: { id: connectionId },
|
|
399
|
+
body: { external_account_id: externalAccountId }
|
|
400
|
+
});
|
|
401
|
+
if (!data) throw new Error("select returned no connection");
|
|
402
|
+
return data;
|
|
403
|
+
},
|
|
404
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
405
|
+
pollTimeoutMs: POLL_TIMEOUT_MS
|
|
406
|
+
}).then((outcome) => {
|
|
407
|
+
if (!isCurrent()) return;
|
|
408
|
+
inFlightRef.current = false;
|
|
409
|
+
pickRef.current = null;
|
|
410
|
+
switch (outcome.status) {
|
|
411
|
+
case "active":
|
|
412
|
+
setState({ phase: "active", connection: outcome.connection });
|
|
413
|
+
onConnectedRef.current?.(outcome.connection);
|
|
414
|
+
return;
|
|
415
|
+
case "connected_pending":
|
|
416
|
+
setState({ phase: "connected_pending" });
|
|
417
|
+
return;
|
|
418
|
+
case "cancelled":
|
|
419
|
+
setState({ phase: "cancelled" });
|
|
420
|
+
return;
|
|
421
|
+
case "error":
|
|
422
|
+
setState({ phase: "error", reason: outcome.reason });
|
|
423
|
+
return;
|
|
207
424
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
)
|
|
425
|
+
});
|
|
426
|
+
}, [client, profileId]);
|
|
427
|
+
const select = useCallback((externalAccountId) => {
|
|
428
|
+
const pick = pickRef.current;
|
|
429
|
+
if (!pick) return;
|
|
430
|
+
pickRef.current = null;
|
|
431
|
+
setState({ phase: "connecting" });
|
|
432
|
+
pick.resolve(externalAccountId);
|
|
433
|
+
}, []);
|
|
434
|
+
const reset = useCallback(() => {
|
|
435
|
+
setRemintNonce((n) => n + 1);
|
|
436
|
+
}, []);
|
|
437
|
+
return { state, start, select, reset };
|
|
211
438
|
}
|
|
212
|
-
function useConnections(profileId) {
|
|
439
|
+
function useConnections(profileId, filter) {
|
|
213
440
|
const { client, queryClient } = usePostrun();
|
|
214
441
|
return useQuery(
|
|
215
442
|
{
|
|
216
|
-
queryKey: connectionKeys.list(profileId),
|
|
217
|
-
queryFn: async () => (await connectionsListByProfile({
|
|
443
|
+
queryKey: connectionKeys.list(profileId, filter),
|
|
444
|
+
queryFn: async () => (await connectionsListByProfile({
|
|
445
|
+
client,
|
|
446
|
+
path: { id: profileId },
|
|
447
|
+
query: filter
|
|
448
|
+
})).data,
|
|
218
449
|
enabled: Boolean(profileId)
|
|
219
450
|
},
|
|
220
451
|
queryClient
|
|
@@ -269,6 +500,17 @@ function useDisconnect() {
|
|
|
269
500
|
queryClient
|
|
270
501
|
);
|
|
271
502
|
}
|
|
503
|
+
|
|
504
|
+
// src/Connect.tsx
|
|
505
|
+
function Connect({
|
|
506
|
+
profileId,
|
|
507
|
+
platform,
|
|
508
|
+
onConnected,
|
|
509
|
+
children
|
|
510
|
+
}) {
|
|
511
|
+
const api = useConnect({ profileId, platform, onConnected });
|
|
512
|
+
return children(api);
|
|
513
|
+
}
|
|
272
514
|
var UploadError = class extends Error {
|
|
273
515
|
status;
|
|
274
516
|
constructor(status, message) {
|
|
@@ -309,17 +551,7 @@ async function uploadBytes(target, file, options = {}) {
|
|
|
309
551
|
}
|
|
310
552
|
|
|
311
553
|
// src/media.ts
|
|
312
|
-
|
|
313
|
-
function inferKind(contentType) {
|
|
314
|
-
if (contentType === "image/gif") return "gif";
|
|
315
|
-
if (contentType.startsWith("image/")) return "image";
|
|
316
|
-
if (contentType.startsWith("video/")) return "video";
|
|
317
|
-
if (DOCUMENT_MIME.test(contentType)) return "document";
|
|
318
|
-
throw new Error(
|
|
319
|
-
`Could not infer media kind from "${contentType}". Pass { kind } explicitly.`
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
async function pollUntilSettled(client, id, signal) {
|
|
554
|
+
async function pollUntilSettled(client, id, signal, onTick) {
|
|
323
555
|
let latest;
|
|
324
556
|
await pWaitFor(
|
|
325
557
|
async () => {
|
|
@@ -327,6 +559,7 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
327
559
|
throw new DOMException("Upload aborted", "AbortError");
|
|
328
560
|
}
|
|
329
561
|
latest = (await mediaGet({ client, path: { id } })).data;
|
|
562
|
+
onTick?.(latest);
|
|
330
563
|
return latest.status === "ready" || latest.status === "failed";
|
|
331
564
|
},
|
|
332
565
|
{ interval: 1500, timeout: 3e5 }
|
|
@@ -336,92 +569,134 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
336
569
|
}
|
|
337
570
|
return latest;
|
|
338
571
|
}
|
|
339
|
-
function
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
body: {
|
|
366
|
-
profile_id: options.profileId,
|
|
367
|
-
kind,
|
|
368
|
-
content_type: contentType,
|
|
369
|
-
targets: options.targets,
|
|
370
|
-
raw: options.raw,
|
|
371
|
-
alt_text: options.altText,
|
|
372
|
-
external_id: options.externalId,
|
|
373
|
-
metadata: options.metadata
|
|
572
|
+
async function runUpload(client, file, options, signal, callbacks) {
|
|
573
|
+
const created = (await mediaCreate({
|
|
574
|
+
client,
|
|
575
|
+
body: {
|
|
576
|
+
profile_id: options.profileId,
|
|
577
|
+
kind: options.kind,
|
|
578
|
+
content_type: options.contentType,
|
|
579
|
+
targets: options.targets,
|
|
580
|
+
raw: options.raw,
|
|
581
|
+
alt_text: options.altText,
|
|
582
|
+
external_id: options.externalId,
|
|
583
|
+
metadata: options.metadata
|
|
584
|
+
}
|
|
585
|
+
})).data;
|
|
586
|
+
if (created.upload) {
|
|
587
|
+
const target = created.upload;
|
|
588
|
+
await pRetry(
|
|
589
|
+
async () => {
|
|
590
|
+
try {
|
|
591
|
+
await uploadBytes(target, file, {
|
|
592
|
+
onProgress: callbacks.onProgress,
|
|
593
|
+
signal
|
|
594
|
+
});
|
|
595
|
+
} catch (uploadError) {
|
|
596
|
+
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
597
|
+
throw new AbortError(uploadError);
|
|
374
598
|
}
|
|
375
|
-
|
|
376
|
-
if (created.upload) {
|
|
377
|
-
const target = created.upload;
|
|
378
|
-
await pRetry(
|
|
379
|
-
async () => {
|
|
380
|
-
try {
|
|
381
|
-
await uploadBytes(target, file, {
|
|
382
|
-
onProgress: setProgress,
|
|
383
|
-
signal: controller.signal
|
|
384
|
-
});
|
|
385
|
-
} catch (uploadError) {
|
|
386
|
-
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
387
|
-
throw new AbortError(uploadError);
|
|
388
|
-
}
|
|
389
|
-
throw uploadError;
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
{ retries: 3, signal: controller.signal }
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
setStatus("processing");
|
|
396
|
-
const settled = await pollUntilSettled(
|
|
397
|
-
client,
|
|
398
|
-
created.id,
|
|
399
|
-
controller.signal
|
|
400
|
-
);
|
|
401
|
-
queryClient.setQueryData(mediaKeys.detail(created.id), settled);
|
|
402
|
-
setMedia(settled);
|
|
403
|
-
setStatus(settled.status === "failed" ? "failed" : "ready");
|
|
404
|
-
return settled;
|
|
405
|
-
} catch (caught) {
|
|
406
|
-
setError(caught);
|
|
407
|
-
setStatus("failed");
|
|
408
|
-
throw caught;
|
|
409
|
-
} finally {
|
|
410
|
-
if (abortRef.current === controller) {
|
|
411
|
-
abortRef.current = null;
|
|
599
|
+
throw uploadError;
|
|
412
600
|
}
|
|
601
|
+
},
|
|
602
|
+
{ retries: 3, signal }
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
callbacks.onProcessing();
|
|
606
|
+
return pollUntilSettled(client, created.id, signal, callbacks.onPoll);
|
|
607
|
+
}
|
|
608
|
+
function toFileArray(files) {
|
|
609
|
+
if (files instanceof File) return [files];
|
|
610
|
+
return Array.from(files);
|
|
611
|
+
}
|
|
612
|
+
function useMediaUpload(options) {
|
|
613
|
+
const { client, queryClient } = usePostrun();
|
|
614
|
+
const [items, setItems] = useState([]);
|
|
615
|
+
const controllers = useRef(/* @__PURE__ */ new Map());
|
|
616
|
+
const limitRef = useRef(null);
|
|
617
|
+
if (!limitRef.current) {
|
|
618
|
+
limitRef.current = pLimit(options?.concurrency ?? 3);
|
|
619
|
+
}
|
|
620
|
+
const patch = useCallback(
|
|
621
|
+
(id, changes) => {
|
|
622
|
+
setItems(
|
|
623
|
+
(current) => current.map(
|
|
624
|
+
(item) => item.id === id ? { ...item, ...changes } : item
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
},
|
|
628
|
+
[]
|
|
629
|
+
);
|
|
630
|
+
const add = useCallback(
|
|
631
|
+
(files, uploadOptions) => {
|
|
632
|
+
const queued = toFileArray(files).map((file) => ({
|
|
633
|
+
id: crypto.randomUUID(),
|
|
634
|
+
file,
|
|
635
|
+
status: "uploading",
|
|
636
|
+
progress: 0,
|
|
637
|
+
media: null,
|
|
638
|
+
error: null
|
|
639
|
+
}));
|
|
640
|
+
setItems((current) => [...current, ...queued]);
|
|
641
|
+
const limit = limitRef.current;
|
|
642
|
+
if (!limit) {
|
|
643
|
+
return Promise.resolve([]);
|
|
413
644
|
}
|
|
645
|
+
const settlements = queued.map((item) => {
|
|
646
|
+
const controller = new AbortController();
|
|
647
|
+
controllers.current.set(item.id, controller);
|
|
648
|
+
return limit(() => {
|
|
649
|
+
if (controller.signal.aborted) {
|
|
650
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
651
|
+
}
|
|
652
|
+
return runUpload(client, item.file, uploadOptions, controller.signal, {
|
|
653
|
+
onProgress: (progress) => patch(item.id, { progress }),
|
|
654
|
+
onProcessing: () => patch(item.id, { status: "processing" }),
|
|
655
|
+
// Live server progress (stage + percent) each poll tick.
|
|
656
|
+
onPoll: (media) => patch(item.id, { media })
|
|
657
|
+
});
|
|
658
|
+
}).then((settled) => {
|
|
659
|
+
patch(item.id, {
|
|
660
|
+
status: settled.status === "failed" ? "failed" : "ready",
|
|
661
|
+
media: settled,
|
|
662
|
+
progress: 1
|
|
663
|
+
});
|
|
664
|
+
queryClient.setQueryData(mediaKeys.detail(settled.id), settled);
|
|
665
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
666
|
+
return settled;
|
|
667
|
+
}).catch((error) => {
|
|
668
|
+
if (controller.signal.aborted) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
patch(item.id, { status: "failed", error });
|
|
672
|
+
return null;
|
|
673
|
+
}).finally(() => {
|
|
674
|
+
controllers.current.delete(item.id);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
return Promise.all(settlements).then(
|
|
678
|
+
(results) => results.filter((result) => result !== null)
|
|
679
|
+
);
|
|
414
680
|
},
|
|
415
|
-
[client, queryClient]
|
|
681
|
+
[client, queryClient, patch]
|
|
416
682
|
);
|
|
417
|
-
const
|
|
683
|
+
const remove = useCallback((id) => {
|
|
684
|
+
controllers.current.get(id)?.abort();
|
|
685
|
+
controllers.current.delete(id);
|
|
686
|
+
setItems((current) => current.filter((item) => item.id !== id));
|
|
687
|
+
}, []);
|
|
418
688
|
const reset = useCallback(() => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
setError(null);
|
|
689
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
690
|
+
controllers.current.clear();
|
|
691
|
+
setItems([]);
|
|
423
692
|
}, []);
|
|
424
|
-
|
|
693
|
+
const ready = items.flatMap(
|
|
694
|
+
(item) => item.status === "ready" && item.media ? [item.media] : []
|
|
695
|
+
);
|
|
696
|
+
const isUploading = items.some(
|
|
697
|
+
(item) => item.status === "uploading" || item.status === "processing"
|
|
698
|
+
);
|
|
699
|
+
return { items, ready, isUploading, add, remove, reset };
|
|
425
700
|
}
|
|
426
701
|
function useMedia(id) {
|
|
427
702
|
const { client, queryClient } = usePostrun();
|
|
@@ -438,12 +713,33 @@ function useMedia(id) {
|
|
|
438
713
|
queryClient
|
|
439
714
|
);
|
|
440
715
|
}
|
|
716
|
+
function useMediaList(query) {
|
|
717
|
+
const { client, queryClient } = usePostrun();
|
|
718
|
+
return useQuery(
|
|
719
|
+
{
|
|
720
|
+
queryKey: mediaKeys.list(query),
|
|
721
|
+
queryFn: async () => (await mediaList({ client, query })).data
|
|
722
|
+
},
|
|
723
|
+
queryClient
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
function useMediaInfinite(filters, options) {
|
|
727
|
+
const { client } = usePostrun();
|
|
728
|
+
return useInfiniteList({
|
|
729
|
+
queryKey: mediaKeys.infinite(filters),
|
|
730
|
+
limit: options?.pageSize,
|
|
731
|
+
fetchPage: async ({ limit, offset }) => (await mediaList({ client, query: { ...filters, limit, offset } })).data
|
|
732
|
+
});
|
|
733
|
+
}
|
|
441
734
|
function useUpdateMedia() {
|
|
442
735
|
const { client, queryClient } = usePostrun();
|
|
443
736
|
return useMutation(
|
|
444
737
|
{
|
|
445
738
|
mutationFn: async ({ id, ...body }) => (await mediaUpdate({ client, path: { id }, body })).data,
|
|
446
|
-
onSuccess: (result, { id }) =>
|
|
739
|
+
onSuccess: (result, { id }) => {
|
|
740
|
+
queryClient.setQueryData(mediaKeys.detail(id), result);
|
|
741
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
742
|
+
}
|
|
447
743
|
},
|
|
448
744
|
queryClient
|
|
449
745
|
);
|
|
@@ -453,7 +749,10 @@ function useDeleteMedia() {
|
|
|
453
749
|
return useMutation(
|
|
454
750
|
{
|
|
455
751
|
mutationFn: async (id) => (await mediaDelete({ client, path: { id } })).data,
|
|
456
|
-
onSuccess: (_result, id) =>
|
|
752
|
+
onSuccess: (_result, id) => {
|
|
753
|
+
queryClient.removeQueries({ queryKey: mediaKeys.detail(id) });
|
|
754
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
755
|
+
}
|
|
457
756
|
},
|
|
458
757
|
queryClient
|
|
459
758
|
);
|
|
@@ -1279,6 +1578,6 @@ function LinkedInPostPreviewImpl({
|
|
|
1279
1578
|
}
|
|
1280
1579
|
var LinkedInPostPreview = memo(LinkedInPostPreviewImpl);
|
|
1281
1580
|
|
|
1282
|
-
export { LinkedInPostPreview, PostrunProvider, UploadError, XPostPreview, connectionKeys, mediaKeys, postKeys, profileKeys, useCalendar, useConnect, useConnection, useConnections, useCreatePost, useCreateProfile, useDeleteMedia, useDeletePost, useDeleteProfile, useDisconnect, useDiscoverableAccounts, useInfiniteList, useMedia, useMediaUpload, usePost, usePostrun, usePosts, usePostsInfinite, useProfile, useProfiles, useProfilesInfinite, useSelectAccount, useUpdateMedia, useUpdatePost, useUpdateProfile };
|
|
1581
|
+
export { Connect, LinkedInPostPreview, PostrunProvider, UploadError, XPostPreview, connectionKeys, mediaKeys, postKeys, profileKeys, useCalendar, useConnect, useConnection, useConnections, useCreatePost, useCreateProfile, useDeleteMedia, useDeletePost, useDeleteProfile, useDisconnect, useDiscoverableAccounts, useInfiniteList, useMedia, useMediaInfinite, useMediaList, useMediaUpload, usePost, usePostrun, usePosts, usePostsInfinite, useProfile, useProfiles, useProfilesInfinite, useSelectAccount, useUpdateMedia, useUpdatePost, useUpdateProfile };
|
|
1283
1582
|
//# sourceMappingURL=index.js.map
|
|
1284
1583
|
//# sourceMappingURL=index.js.map
|