@postrun/react 0.2.0 → 1.1.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 +82 -5
- package/dist/index.cjs +419 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -82
- package/dist/index.d.ts +266 -82
- package/dist/index.js +417 -110
- 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';
|
|
@@ -194,29 +196,281 @@ function useDeleteProfile() {
|
|
|
194
196
|
queryClient
|
|
195
197
|
);
|
|
196
198
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
298
|
}
|
|
202
299
|
|
|
203
300
|
// src/connections.ts
|
|
204
|
-
|
|
301
|
+
var POLL_INTERVAL_MS = 1500;
|
|
302
|
+
var POLL_TIMEOUT_MS = 15e3;
|
|
303
|
+
function useConnect({
|
|
304
|
+
profileId,
|
|
305
|
+
platform,
|
|
306
|
+
onConnected,
|
|
307
|
+
onError,
|
|
308
|
+
onCancelled,
|
|
309
|
+
prepareOnMount = true
|
|
310
|
+
}) {
|
|
205
311
|
const { client, queryClient } = usePostrun();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
312
|
+
const [state, setState] = useState({ phase: "preparing" });
|
|
313
|
+
const [remintNonce, setRemintNonce] = useState(0);
|
|
314
|
+
const sessionRef = useRef(null);
|
|
315
|
+
const pickRef = useRef(null);
|
|
316
|
+
const inFlightRef = useRef(false);
|
|
317
|
+
const flowGenRef = useRef(0);
|
|
318
|
+
const preparingRef = useRef(false);
|
|
319
|
+
const prepareGenRef = useRef(0);
|
|
320
|
+
const onConnectedRef = useRef(onConnected);
|
|
321
|
+
const onErrorRef = useRef(onError);
|
|
322
|
+
const onCancelledRef = useRef(onCancelled);
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
onConnectedRef.current = onConnected;
|
|
325
|
+
onErrorRef.current = onError;
|
|
326
|
+
onCancelledRef.current = onCancelled;
|
|
327
|
+
});
|
|
328
|
+
const abandonFlow = useCallback(() => {
|
|
329
|
+
flowGenRef.current += 1;
|
|
330
|
+
inFlightRef.current = false;
|
|
331
|
+
const pick = pickRef.current;
|
|
332
|
+
pickRef.current = null;
|
|
333
|
+
pick?.reject(new Error("connect flow abandoned"));
|
|
334
|
+
}, []);
|
|
335
|
+
const prepare = useCallback(() => {
|
|
336
|
+
if (sessionRef.current || preparingRef.current) return;
|
|
337
|
+
preparingRef.current = true;
|
|
338
|
+
const gen = prepareGenRef.current;
|
|
339
|
+
setState({ phase: "preparing" });
|
|
340
|
+
const failPrepare = () => {
|
|
341
|
+
preparingRef.current = false;
|
|
342
|
+
setState({ phase: "error", reason: "prepare_failed" });
|
|
343
|
+
onErrorRef.current?.("prepare_failed");
|
|
344
|
+
};
|
|
345
|
+
connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
|
|
346
|
+
if (prepareGenRef.current !== gen) return;
|
|
347
|
+
if (!data) {
|
|
348
|
+
failPrepare();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
preparingRef.current = false;
|
|
352
|
+
sessionRef.current = {
|
|
353
|
+
token: data.connect_session_token,
|
|
354
|
+
providerConfigKey: data.provider_config_key,
|
|
355
|
+
host: data.nango_host
|
|
356
|
+
};
|
|
357
|
+
setState({ phase: "idle" });
|
|
358
|
+
}).catch(() => {
|
|
359
|
+
if (prepareGenRef.current !== gen) return;
|
|
360
|
+
failPrepare();
|
|
361
|
+
});
|
|
362
|
+
}, [client, profileId, platform]);
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
prepareGenRef.current += 1;
|
|
365
|
+
preparingRef.current = false;
|
|
366
|
+
sessionRef.current = null;
|
|
367
|
+
setState({ phase: "preparing" });
|
|
368
|
+
if (prepareOnMount) prepare();
|
|
369
|
+
return () => {
|
|
370
|
+
prepareGenRef.current += 1;
|
|
371
|
+
abandonFlow();
|
|
372
|
+
};
|
|
373
|
+
}, [profileId, platform, remintNonce, prepareOnMount, prepare, abandonFlow]);
|
|
374
|
+
const start = useCallback(() => {
|
|
375
|
+
const session = sessionRef.current;
|
|
376
|
+
if (!session) {
|
|
377
|
+
prepare();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (inFlightRef.current) return;
|
|
381
|
+
inFlightRef.current = true;
|
|
382
|
+
const gen = flowGenRef.current;
|
|
383
|
+
const isCurrent = () => flowGenRef.current === gen;
|
|
384
|
+
setState({ phase: "connecting" });
|
|
385
|
+
void runEmbeddedConnect({
|
|
386
|
+
// Nango lives INSIDE `authorize` so a SYNCHRONOUS throw (invalid host /
|
|
387
|
+
// missing token — the Nango SDK throws `AuthError` synchronously) becomes a
|
|
388
|
+
// promise rejection that `grant()` maps to `auth_failed`, never an uncaught
|
|
389
|
+
// throw escaping the click and wedging `inFlightRef`. Gesture timing still
|
|
390
|
+
// holds: `authorize()` is invoked SYNCHRONOUSLY down the
|
|
391
|
+
// start → runEmbeddedConnect → grant chain (each `await`'s operand is
|
|
392
|
+
// evaluated before it suspends), so `nango.auth()`'s `window.open` fires
|
|
393
|
+
// inside the user gesture, with no `await` before it.
|
|
394
|
+
authorize: async () => {
|
|
395
|
+
const nango = new Nango({
|
|
396
|
+
host: session.host,
|
|
397
|
+
connectSessionToken: session.token
|
|
398
|
+
});
|
|
399
|
+
const result = await nango.auth(session.providerConfigKey, {
|
|
400
|
+
detectClosedAuthWindow: true
|
|
401
|
+
});
|
|
402
|
+
return result.connectionId;
|
|
403
|
+
},
|
|
404
|
+
chooseAccount: (accounts) => new Promise((resolve, reject) => {
|
|
405
|
+
if (!isCurrent()) {
|
|
406
|
+
reject(new Error("connect flow abandoned"));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
pickRef.current = { resolve, reject };
|
|
410
|
+
setState({ phase: "picking", accounts });
|
|
411
|
+
}),
|
|
412
|
+
listByNangoConnectionId: async (nangoConnectionId) => {
|
|
413
|
+
const { data } = await connectionsListByProfile({
|
|
210
414
|
client,
|
|
211
415
|
path: { id: profileId },
|
|
212
|
-
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
|
|
416
|
+
query: { nango_connection_id: nangoConnectionId }
|
|
417
|
+
});
|
|
418
|
+
return data?.data ?? [];
|
|
419
|
+
},
|
|
420
|
+
discoverAccounts: async (connectionId) => {
|
|
421
|
+
const { data } = await connectionsListAccounts({
|
|
422
|
+
client,
|
|
423
|
+
path: { id: connectionId }
|
|
424
|
+
});
|
|
425
|
+
return data?.data ?? [];
|
|
426
|
+
},
|
|
427
|
+
selectAccount: async (connectionId, externalAccountId) => {
|
|
428
|
+
const { data } = await connectionsSelect({
|
|
429
|
+
client,
|
|
430
|
+
path: { id: connectionId },
|
|
431
|
+
body: { external_account_id: externalAccountId }
|
|
432
|
+
});
|
|
433
|
+
if (!data) throw new Error("select returned no connection");
|
|
434
|
+
return data;
|
|
435
|
+
},
|
|
436
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
437
|
+
pollTimeoutMs: POLL_TIMEOUT_MS
|
|
438
|
+
}).then((outcome) => {
|
|
439
|
+
if (!isCurrent()) return;
|
|
440
|
+
inFlightRef.current = false;
|
|
441
|
+
pickRef.current = null;
|
|
442
|
+
switch (outcome.status) {
|
|
443
|
+
case "active":
|
|
444
|
+
setState({ phase: "active", connection: outcome.connection });
|
|
445
|
+
void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
|
|
446
|
+
onConnectedRef.current?.(outcome.connection);
|
|
447
|
+
return;
|
|
448
|
+
case "connected_pending":
|
|
449
|
+
setState({ phase: "connected_pending" });
|
|
450
|
+
void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
|
|
451
|
+
return;
|
|
452
|
+
case "cancelled":
|
|
453
|
+
setState({ phase: "cancelled" });
|
|
454
|
+
onCancelledRef.current?.();
|
|
455
|
+
return;
|
|
456
|
+
case "error":
|
|
457
|
+
setState({ phase: "error", reason: outcome.reason });
|
|
458
|
+
onErrorRef.current?.(outcome.reason);
|
|
459
|
+
return;
|
|
216
460
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
)
|
|
461
|
+
});
|
|
462
|
+
}, [client, profileId, prepare, queryClient]);
|
|
463
|
+
const select = useCallback((externalAccountId) => {
|
|
464
|
+
const pick = pickRef.current;
|
|
465
|
+
if (!pick) return;
|
|
466
|
+
pickRef.current = null;
|
|
467
|
+
setState({ phase: "connecting" });
|
|
468
|
+
pick.resolve(externalAccountId);
|
|
469
|
+
}, []);
|
|
470
|
+
const reset = useCallback(() => {
|
|
471
|
+
setRemintNonce((n) => n + 1);
|
|
472
|
+
}, []);
|
|
473
|
+
return { state, start, prepare, select, reset };
|
|
220
474
|
}
|
|
221
475
|
function useConnections(profileId, filter) {
|
|
222
476
|
const { client, queryClient } = usePostrun();
|
|
@@ -282,6 +536,27 @@ function useDisconnect() {
|
|
|
282
536
|
queryClient
|
|
283
537
|
);
|
|
284
538
|
}
|
|
539
|
+
|
|
540
|
+
// src/Connect.tsx
|
|
541
|
+
function Connect({
|
|
542
|
+
profileId,
|
|
543
|
+
platform,
|
|
544
|
+
onConnected,
|
|
545
|
+
onError,
|
|
546
|
+
onCancelled,
|
|
547
|
+
prepareOnMount,
|
|
548
|
+
children
|
|
549
|
+
}) {
|
|
550
|
+
const api = useConnect({
|
|
551
|
+
profileId,
|
|
552
|
+
platform,
|
|
553
|
+
onConnected,
|
|
554
|
+
onError,
|
|
555
|
+
onCancelled,
|
|
556
|
+
prepareOnMount
|
|
557
|
+
});
|
|
558
|
+
return children(api);
|
|
559
|
+
}
|
|
285
560
|
var UploadError = class extends Error {
|
|
286
561
|
status;
|
|
287
562
|
constructor(status, message) {
|
|
@@ -322,17 +597,7 @@ async function uploadBytes(target, file, options = {}) {
|
|
|
322
597
|
}
|
|
323
598
|
|
|
324
599
|
// src/media.ts
|
|
325
|
-
|
|
326
|
-
function inferKind(contentType) {
|
|
327
|
-
if (contentType === "image/gif") return "gif";
|
|
328
|
-
if (contentType.startsWith("image/")) return "image";
|
|
329
|
-
if (contentType.startsWith("video/")) return "video";
|
|
330
|
-
if (DOCUMENT_MIME.test(contentType)) return "document";
|
|
331
|
-
throw new Error(
|
|
332
|
-
`Could not infer media kind from "${contentType}". Pass { kind } explicitly.`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
async function pollUntilSettled(client, id, signal) {
|
|
600
|
+
async function pollUntilSettled(client, id, signal, onTick) {
|
|
336
601
|
let latest;
|
|
337
602
|
await pWaitFor(
|
|
338
603
|
async () => {
|
|
@@ -340,6 +605,7 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
340
605
|
throw new DOMException("Upload aborted", "AbortError");
|
|
341
606
|
}
|
|
342
607
|
latest = (await mediaGet({ client, path: { id } })).data;
|
|
608
|
+
onTick?.(latest);
|
|
343
609
|
return latest.status === "ready" || latest.status === "failed";
|
|
344
610
|
},
|
|
345
611
|
{ interval: 1500, timeout: 3e5 }
|
|
@@ -349,93 +615,134 @@ async function pollUntilSettled(client, id, signal) {
|
|
|
349
615
|
}
|
|
350
616
|
return latest;
|
|
351
617
|
}
|
|
352
|
-
function
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
body: {
|
|
379
|
-
profile_id: options.profileId,
|
|
380
|
-
kind,
|
|
381
|
-
content_type: contentType,
|
|
382
|
-
targets: options.targets,
|
|
383
|
-
raw: options.raw,
|
|
384
|
-
alt_text: options.altText,
|
|
385
|
-
external_id: options.externalId,
|
|
386
|
-
metadata: options.metadata
|
|
618
|
+
async function runUpload(client, file, options, signal, callbacks) {
|
|
619
|
+
const created = (await mediaCreate({
|
|
620
|
+
client,
|
|
621
|
+
body: {
|
|
622
|
+
profile_id: options.profileId,
|
|
623
|
+
kind: options.kind,
|
|
624
|
+
content_type: options.contentType,
|
|
625
|
+
targets: options.targets,
|
|
626
|
+
raw: options.raw,
|
|
627
|
+
alt_text: options.altText,
|
|
628
|
+
external_id: options.externalId,
|
|
629
|
+
metadata: options.metadata
|
|
630
|
+
}
|
|
631
|
+
})).data;
|
|
632
|
+
if (created.upload) {
|
|
633
|
+
const target = created.upload;
|
|
634
|
+
await pRetry(
|
|
635
|
+
async () => {
|
|
636
|
+
try {
|
|
637
|
+
await uploadBytes(target, file, {
|
|
638
|
+
onProgress: callbacks.onProgress,
|
|
639
|
+
signal
|
|
640
|
+
});
|
|
641
|
+
} catch (uploadError) {
|
|
642
|
+
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
643
|
+
throw new AbortError(uploadError);
|
|
387
644
|
}
|
|
388
|
-
|
|
389
|
-
if (created.upload) {
|
|
390
|
-
const target = created.upload;
|
|
391
|
-
await pRetry(
|
|
392
|
-
async () => {
|
|
393
|
-
try {
|
|
394
|
-
await uploadBytes(target, file, {
|
|
395
|
-
onProgress: setProgress,
|
|
396
|
-
signal: controller.signal
|
|
397
|
-
});
|
|
398
|
-
} catch (uploadError) {
|
|
399
|
-
if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
|
|
400
|
-
throw new AbortError(uploadError);
|
|
401
|
-
}
|
|
402
|
-
throw uploadError;
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
{ retries: 3, signal: controller.signal }
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
setStatus("processing");
|
|
409
|
-
const settled = await pollUntilSettled(
|
|
410
|
-
client,
|
|
411
|
-
created.id,
|
|
412
|
-
controller.signal
|
|
413
|
-
);
|
|
414
|
-
queryClient.setQueryData(mediaKeys.detail(created.id), settled);
|
|
415
|
-
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
416
|
-
setMedia(settled);
|
|
417
|
-
setStatus(settled.status === "failed" ? "failed" : "ready");
|
|
418
|
-
return settled;
|
|
419
|
-
} catch (caught) {
|
|
420
|
-
setError(caught);
|
|
421
|
-
setStatus("failed");
|
|
422
|
-
throw caught;
|
|
423
|
-
} finally {
|
|
424
|
-
if (abortRef.current === controller) {
|
|
425
|
-
abortRef.current = null;
|
|
645
|
+
throw uploadError;
|
|
426
646
|
}
|
|
647
|
+
},
|
|
648
|
+
{ retries: 3, signal }
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
callbacks.onProcessing();
|
|
652
|
+
return pollUntilSettled(client, created.id, signal, callbacks.onPoll);
|
|
653
|
+
}
|
|
654
|
+
function toFileArray(files) {
|
|
655
|
+
if (files instanceof File) return [files];
|
|
656
|
+
return Array.from(files);
|
|
657
|
+
}
|
|
658
|
+
function useMediaUpload(options) {
|
|
659
|
+
const { client, queryClient } = usePostrun();
|
|
660
|
+
const [items, setItems] = useState([]);
|
|
661
|
+
const controllers = useRef(/* @__PURE__ */ new Map());
|
|
662
|
+
const limitRef = useRef(null);
|
|
663
|
+
if (!limitRef.current) {
|
|
664
|
+
limitRef.current = pLimit(options?.concurrency ?? 3);
|
|
665
|
+
}
|
|
666
|
+
const patch = useCallback(
|
|
667
|
+
(id, changes) => {
|
|
668
|
+
setItems(
|
|
669
|
+
(current) => current.map(
|
|
670
|
+
(item) => item.id === id ? { ...item, ...changes } : item
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
[]
|
|
675
|
+
);
|
|
676
|
+
const add = useCallback(
|
|
677
|
+
(files, uploadOptions) => {
|
|
678
|
+
const queued = toFileArray(files).map((file) => ({
|
|
679
|
+
id: crypto.randomUUID(),
|
|
680
|
+
file,
|
|
681
|
+
status: "uploading",
|
|
682
|
+
progress: 0,
|
|
683
|
+
media: null,
|
|
684
|
+
error: null
|
|
685
|
+
}));
|
|
686
|
+
setItems((current) => [...current, ...queued]);
|
|
687
|
+
const limit = limitRef.current;
|
|
688
|
+
if (!limit) {
|
|
689
|
+
return Promise.resolve([]);
|
|
427
690
|
}
|
|
691
|
+
const settlements = queued.map((item) => {
|
|
692
|
+
const controller = new AbortController();
|
|
693
|
+
controllers.current.set(item.id, controller);
|
|
694
|
+
return limit(() => {
|
|
695
|
+
if (controller.signal.aborted) {
|
|
696
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
697
|
+
}
|
|
698
|
+
return runUpload(client, item.file, uploadOptions, controller.signal, {
|
|
699
|
+
onProgress: (progress) => patch(item.id, { progress }),
|
|
700
|
+
onProcessing: () => patch(item.id, { status: "processing" }),
|
|
701
|
+
// Live server progress (stage + percent) each poll tick.
|
|
702
|
+
onPoll: (media) => patch(item.id, { media })
|
|
703
|
+
});
|
|
704
|
+
}).then((settled) => {
|
|
705
|
+
patch(item.id, {
|
|
706
|
+
status: settled.status === "failed" ? "failed" : "ready",
|
|
707
|
+
media: settled,
|
|
708
|
+
progress: 1
|
|
709
|
+
});
|
|
710
|
+
queryClient.setQueryData(mediaKeys.detail(settled.id), settled);
|
|
711
|
+
void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
|
|
712
|
+
return settled;
|
|
713
|
+
}).catch((error) => {
|
|
714
|
+
if (controller.signal.aborted) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
patch(item.id, { status: "failed", error });
|
|
718
|
+
return null;
|
|
719
|
+
}).finally(() => {
|
|
720
|
+
controllers.current.delete(item.id);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
return Promise.all(settlements).then(
|
|
724
|
+
(results) => results.filter((result) => result !== null)
|
|
725
|
+
);
|
|
428
726
|
},
|
|
429
|
-
[client, queryClient]
|
|
727
|
+
[client, queryClient, patch]
|
|
430
728
|
);
|
|
431
|
-
const
|
|
729
|
+
const remove = useCallback((id) => {
|
|
730
|
+
controllers.current.get(id)?.abort();
|
|
731
|
+
controllers.current.delete(id);
|
|
732
|
+
setItems((current) => current.filter((item) => item.id !== id));
|
|
733
|
+
}, []);
|
|
432
734
|
const reset = useCallback(() => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
setError(null);
|
|
735
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
736
|
+
controllers.current.clear();
|
|
737
|
+
setItems([]);
|
|
437
738
|
}, []);
|
|
438
|
-
|
|
739
|
+
const ready = items.flatMap(
|
|
740
|
+
(item) => item.status === "ready" && item.media ? [item.media] : []
|
|
741
|
+
);
|
|
742
|
+
const isUploading = items.some(
|
|
743
|
+
(item) => item.status === "uploading" || item.status === "processing"
|
|
744
|
+
);
|
|
745
|
+
return { items, ready, isUploading, add, remove, reset };
|
|
439
746
|
}
|
|
440
747
|
function useMedia(id) {
|
|
441
748
|
const { client, queryClient } = usePostrun();
|
|
@@ -1317,6 +1624,6 @@ function LinkedInPostPreviewImpl({
|
|
|
1317
1624
|
}
|
|
1318
1625
|
var LinkedInPostPreview = memo(LinkedInPostPreviewImpl);
|
|
1319
1626
|
|
|
1320
|
-
export { 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 };
|
|
1627
|
+
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 };
|
|
1321
1628
|
//# sourceMappingURL=index.js.map
|
|
1322
1629
|
//# sourceMappingURL=index.js.map
|