@scalemule/nextjs 0.1.8 → 0.1.10
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 +61 -1
- package/dist/index.d.mts +71 -33
- package/dist/index.d.ts +71 -33
- package/dist/index.js +115 -22
- package/dist/index.mjs +115 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,7 +132,67 @@ function LoginPage() {
|
|
|
132
132
|
}
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
### `
|
|
135
|
+
### `useMedia()` — recommended for image / video / audio uploads
|
|
136
|
+
|
|
137
|
+
One hook covers every MIME type, defaults to private, and auto-routes images
|
|
138
|
+
through the photo pipeline (optimization, transform breakpoints) and videos
|
|
139
|
+
through the video pipeline (HLS transcoding). Drop the result `file_id` into
|
|
140
|
+
a chat message or render it with `<ScaleMuleMedia />` and progressive
|
|
141
|
+
enhancement just works.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { useMedia, ScaleMuleMedia } from '@scalemule/nextjs'
|
|
145
|
+
|
|
146
|
+
function Composer({ conversationId }: { conversationId: string }) {
|
|
147
|
+
const { upload, uploading } = useMedia()
|
|
148
|
+
|
|
149
|
+
async function handleFile(file: File) {
|
|
150
|
+
const result = await upload(file)
|
|
151
|
+
// result.file_id is private by default; post the message with it.
|
|
152
|
+
await postMessage({ conversationId, attachments: [{ file_id: result.file_id }] })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return <input type="file" disabled={uploading} onChange={(e) => handleFile(e.target.files![0])} />
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Attachment({ fileId, conversationId, mimeType }: {
|
|
159
|
+
fileId: string; conversationId: string; mimeType: string
|
|
160
|
+
}) {
|
|
161
|
+
// <ScaleMuleMedia /> uses useFileStatus internally; with conversationId
|
|
162
|
+
// it subscribes to the conversation channel for live optimize/transcode
|
|
163
|
+
// updates instead of polling.
|
|
164
|
+
return (
|
|
165
|
+
<ScaleMuleMedia
|
|
166
|
+
fileId={fileId}
|
|
167
|
+
mimeType={mimeType}
|
|
168
|
+
conversationId={conversationId}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
See [`docs/MEDIA-UPLOADS.md`](https://github.com/scalemule/scalemule-repos/blob/main/docs/MEDIA-UPLOADS.md)
|
|
175
|
+
for the full decision tree (which hook for which app, fast/safe policy modes,
|
|
176
|
+
private vs public visibility, anti-patterns).
|
|
177
|
+
|
|
178
|
+
### `useFileStatus()` — read-side progressive enhancement
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { useFileStatus } from '@scalemule/nextjs'
|
|
182
|
+
|
|
183
|
+
// Chat surface — subscribe to the conversation channel for push updates.
|
|
184
|
+
const { status, isReady } = useFileStatus({ fileId, conversationId })
|
|
185
|
+
|
|
186
|
+
// Non-chat — pull-only with optional polling.
|
|
187
|
+
const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `useContent()` — generic file uploads (legacy)
|
|
191
|
+
|
|
192
|
+
> **Deprecated for chat / progressive media.** Use `useMedia()` instead.
|
|
193
|
+
> `useContent()` remains available for plain non-image/video file lists
|
|
194
|
+
> (documents, downloads). It does not auto-route through the photo/video
|
|
195
|
+
> pipelines and does not enforce private-by-default for chat surfaces.
|
|
136
196
|
|
|
137
197
|
```tsx
|
|
138
198
|
import { useContent } from '@scalemule/nextjs'
|
package/dist/index.d.mts
CHANGED
|
@@ -216,13 +216,13 @@ interface ScaleMuleProviderProps extends ScaleMuleConfig {
|
|
|
216
216
|
*
|
|
217
217
|
* The platform stores the per-app policy in
|
|
218
218
|
* `application_storage_settings.media_policy` (Phase 4 / P3, live in
|
|
219
|
-
* prod).
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* platform admin set.
|
|
219
|
+
* prod). On boot, the provider fetches `GET /v1/storage/policy` (a
|
|
220
|
+
* lightweight `EndUserOnly` endpoint added in `@scalemule/sdk@0.0.45`)
|
|
221
|
+
* and uses the returned policy as the effective default.
|
|
223
222
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* Passing this prop overrides the auto-fetched value — useful for
|
|
224
|
+
* tests or when an app needs to force a specific mode regardless of
|
|
225
|
+
* platform config.
|
|
226
226
|
*/
|
|
227
227
|
mediaPolicy?: MediaPolicy;
|
|
228
228
|
}
|
|
@@ -324,25 +324,39 @@ interface UseContentOptions {
|
|
|
324
324
|
*/
|
|
325
325
|
declare function useContent(options?: UseContentOptions): UseContentReturn;
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Conversation kinds recognized by the chat realtime channel naming scheme.
|
|
329
|
+
* Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
|
|
330
|
+
*/
|
|
331
|
+
type ConversationKind = 'standard' | 'large_room' | 'broadcast' | 'support';
|
|
327
332
|
interface UseFileStatusOptions {
|
|
328
333
|
/** Storage file_id to read status for. */
|
|
329
334
|
fileId: string | null | undefined;
|
|
330
335
|
/**
|
|
331
336
|
* Optional poll interval in milliseconds. If set, the hook re-fetches
|
|
332
|
-
* status every `pollIntervalMs
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* Polling stops automatically once `scan.status === 'clean'` AND the
|
|
336
|
-
* caller's expected pipeline is done (`urls.optimized` returns 200 for
|
|
337
|
-
* images, `urls.hls` returns 200 for videos). Today the hook can only
|
|
338
|
-
* detect scan clean — broader pipeline-done detection lands when Phase 3
|
|
339
|
-
* enriches the optimize/transcode response fields.
|
|
337
|
+
* status every `pollIntervalMs` until scan goes clean. Useful for
|
|
338
|
+
* non-chat surfaces or as a fallback alongside `conversationId` push.
|
|
340
339
|
*/
|
|
341
340
|
pollIntervalMs?: number | null;
|
|
341
|
+
/** Disable the hook (don't fetch). Useful when `fileId` is conditional. */
|
|
342
|
+
disabled?: boolean;
|
|
342
343
|
/**
|
|
343
|
-
*
|
|
344
|
+
* Chat-surface push variant: subscribe to the conversation's realtime
|
|
345
|
+
* channel and refresh when a `file_status_changed` event arrives for
|
|
346
|
+
* `fileId`. Auth rides the existing per-conversation channel ACL.
|
|
344
347
|
*/
|
|
345
|
-
|
|
348
|
+
conversationId?: string | null;
|
|
349
|
+
/**
|
|
350
|
+
* Conversation kind, controls the channel name prefix:
|
|
351
|
+
* - `standard` (default) → `conversation:{id}`
|
|
352
|
+
* - `large_room` → `conversation:lr:{id}`
|
|
353
|
+
* - `broadcast` → `conversation:bc:{id}`
|
|
354
|
+
* - `support` → `conversation:support:{id}`
|
|
355
|
+
*
|
|
356
|
+
* If you only have a `messageId`, look up the conversation first via
|
|
357
|
+
* `client.chat.getMessage(messageId)` and pass the result here.
|
|
358
|
+
*/
|
|
359
|
+
conversationKind?: ConversationKind;
|
|
346
360
|
}
|
|
347
361
|
interface UseFileStatusReturn {
|
|
348
362
|
/** The latest status response, or null on first render / disabled. */
|
|
@@ -362,29 +376,39 @@ interface UseFileStatusReturn {
|
|
|
362
376
|
refresh: () => Promise<void>;
|
|
363
377
|
}
|
|
364
378
|
/**
|
|
365
|
-
* Subscribes to {@link FileStatus} for a single file.
|
|
366
|
-
* pull-only hook — single fetch by default, optional polling.
|
|
379
|
+
* Subscribes to {@link FileStatus} for a single file.
|
|
367
380
|
*
|
|
368
|
-
*
|
|
369
|
-
* for chat surfaces — `useFileStatus({ messageId })` will subscribe to
|
|
370
|
-
* `file.status` events on the existing per-conversation realtime channel
|
|
371
|
-
* via the `scalemule-chat` translation bridge (P5'). Until that lands,
|
|
372
|
-
* customers using this hook from chat surfaces should pass `pollIntervalMs`
|
|
373
|
-
* in the 1–3 second range while a media pipeline is expected to be running,
|
|
374
|
-
* then drop the polling once `isReady` is true.
|
|
381
|
+
* Three call shapes:
|
|
375
382
|
*
|
|
376
|
-
*
|
|
383
|
+
* 1. **Pull-only** — `useFileStatus({ fileId })` plus optional `pollIntervalMs`.
|
|
384
|
+
* One-shot fetch; useful for non-chat surfaces or static reads.
|
|
385
|
+
*
|
|
386
|
+
* 2. **Chat-surface push** — `useFileStatus({ fileId, conversationId, conversationKind? })`.
|
|
387
|
+
* Subscribes to the conversation channel and refreshes when
|
|
388
|
+
* `file_status_changed` arrives for this `fileId`. The chat service's
|
|
389
|
+
* media-status bridge fans photo/video lifecycle events into the per-conversation
|
|
390
|
+
* channel; the hook drops events for other files and dedupes against polling.
|
|
391
|
+
*
|
|
392
|
+
* 3. **Push + slow poll fallback** — combine `conversationId` with `pollIntervalMs`
|
|
393
|
+
* if you want belt-and-suspenders for environments where the websocket may drop.
|
|
394
|
+
*
|
|
395
|
+
* The `surface: 'profile'` push variant (private-user channel) is deferred until
|
|
396
|
+
* the realtime SDK exposes private-channel subscription by user.
|
|
397
|
+
*
|
|
398
|
+
* @example Chat surface (push)
|
|
377
399
|
* ```tsx
|
|
378
|
-
* function ChatImage({ fileId }: { fileId: string }) {
|
|
379
|
-
* const { status, isReady } = useFileStatus({
|
|
380
|
-
* fileId,
|
|
381
|
-
* pollIntervalMs: 2000,
|
|
382
|
-
* });
|
|
400
|
+
* function ChatImage({ fileId, conversationId }: { fileId: string; conversationId: string }) {
|
|
401
|
+
* const { status, isReady } = useFileStatus({ fileId, conversationId });
|
|
383
402
|
* if (!isReady) return <div>Scanning…</div>;
|
|
384
403
|
* const src = status?.urls.optimized ?? status?.urls.original;
|
|
385
404
|
* return <img src={src} />;
|
|
386
405
|
* }
|
|
387
406
|
* ```
|
|
407
|
+
*
|
|
408
|
+
* @example Non-chat surface (pull + poll)
|
|
409
|
+
* ```tsx
|
|
410
|
+
* const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 });
|
|
411
|
+
* ```
|
|
388
412
|
*/
|
|
389
413
|
declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
|
|
390
414
|
|
|
@@ -413,9 +437,23 @@ interface ScaleMuleMediaProps {
|
|
|
413
437
|
/**
|
|
414
438
|
* Polling interval while waiting for the media pipeline to complete.
|
|
415
439
|
* Defaults to 2000ms; set to `null` to disable polling. Polling stops
|
|
416
|
-
* automatically once scan is clean.
|
|
440
|
+
* automatically once scan is clean. When `conversationId` is set the
|
|
441
|
+
* component receives push updates and you can usually pass `null`.
|
|
417
442
|
*/
|
|
418
443
|
pollIntervalMs?: number | null;
|
|
444
|
+
/**
|
|
445
|
+
* Chat-surface push: when set, the underlying `useFileStatus` hook
|
|
446
|
+
* subscribes to the conversation's realtime channel and refreshes on
|
|
447
|
+
* `file_status_changed` events for this `fileId`. See `useFileStatus`
|
|
448
|
+
* docs for channel naming and bridge behaviour.
|
|
449
|
+
*/
|
|
450
|
+
conversationId?: string | null;
|
|
451
|
+
/**
|
|
452
|
+
* Conversation kind, controls the channel name prefix
|
|
453
|
+
* (`conversation:{id}` vs `conversation:lr|bc|support:{id}`).
|
|
454
|
+
* Default is `'standard'`.
|
|
455
|
+
*/
|
|
456
|
+
conversationKind?: ConversationKind;
|
|
419
457
|
/**
|
|
420
458
|
* Render a custom placeholder while waiting for scan / upload. Defaults
|
|
421
459
|
* to a tiny "Loading…" div. Receives the current FileStatus (or null).
|
|
@@ -950,4 +988,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
950
988
|
error: (message: string, data?: unknown) => void;
|
|
951
989
|
};
|
|
952
990
|
|
|
953
|
-
export { ApiError$1 as ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
991
|
+
export { ApiError$1 as ApiError, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
package/dist/index.d.ts
CHANGED
|
@@ -216,13 +216,13 @@ interface ScaleMuleProviderProps extends ScaleMuleConfig {
|
|
|
216
216
|
*
|
|
217
217
|
* The platform stores the per-app policy in
|
|
218
218
|
* `application_storage_settings.media_policy` (Phase 4 / P3, live in
|
|
219
|
-
* prod).
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* platform admin set.
|
|
219
|
+
* prod). On boot, the provider fetches `GET /v1/storage/policy` (a
|
|
220
|
+
* lightweight `EndUserOnly` endpoint added in `@scalemule/sdk@0.0.45`)
|
|
221
|
+
* and uses the returned policy as the effective default.
|
|
223
222
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* Passing this prop overrides the auto-fetched value — useful for
|
|
224
|
+
* tests or when an app needs to force a specific mode regardless of
|
|
225
|
+
* platform config.
|
|
226
226
|
*/
|
|
227
227
|
mediaPolicy?: MediaPolicy;
|
|
228
228
|
}
|
|
@@ -324,25 +324,39 @@ interface UseContentOptions {
|
|
|
324
324
|
*/
|
|
325
325
|
declare function useContent(options?: UseContentOptions): UseContentReturn;
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Conversation kinds recognized by the chat realtime channel naming scheme.
|
|
329
|
+
* Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
|
|
330
|
+
*/
|
|
331
|
+
type ConversationKind = 'standard' | 'large_room' | 'broadcast' | 'support';
|
|
327
332
|
interface UseFileStatusOptions {
|
|
328
333
|
/** Storage file_id to read status for. */
|
|
329
334
|
fileId: string | null | undefined;
|
|
330
335
|
/**
|
|
331
336
|
* Optional poll interval in milliseconds. If set, the hook re-fetches
|
|
332
|
-
* status every `pollIntervalMs
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* Polling stops automatically once `scan.status === 'clean'` AND the
|
|
336
|
-
* caller's expected pipeline is done (`urls.optimized` returns 200 for
|
|
337
|
-
* images, `urls.hls` returns 200 for videos). Today the hook can only
|
|
338
|
-
* detect scan clean — broader pipeline-done detection lands when Phase 3
|
|
339
|
-
* enriches the optimize/transcode response fields.
|
|
337
|
+
* status every `pollIntervalMs` until scan goes clean. Useful for
|
|
338
|
+
* non-chat surfaces or as a fallback alongside `conversationId` push.
|
|
340
339
|
*/
|
|
341
340
|
pollIntervalMs?: number | null;
|
|
341
|
+
/** Disable the hook (don't fetch). Useful when `fileId` is conditional. */
|
|
342
|
+
disabled?: boolean;
|
|
342
343
|
/**
|
|
343
|
-
*
|
|
344
|
+
* Chat-surface push variant: subscribe to the conversation's realtime
|
|
345
|
+
* channel and refresh when a `file_status_changed` event arrives for
|
|
346
|
+
* `fileId`. Auth rides the existing per-conversation channel ACL.
|
|
344
347
|
*/
|
|
345
|
-
|
|
348
|
+
conversationId?: string | null;
|
|
349
|
+
/**
|
|
350
|
+
* Conversation kind, controls the channel name prefix:
|
|
351
|
+
* - `standard` (default) → `conversation:{id}`
|
|
352
|
+
* - `large_room` → `conversation:lr:{id}`
|
|
353
|
+
* - `broadcast` → `conversation:bc:{id}`
|
|
354
|
+
* - `support` → `conversation:support:{id}`
|
|
355
|
+
*
|
|
356
|
+
* If you only have a `messageId`, look up the conversation first via
|
|
357
|
+
* `client.chat.getMessage(messageId)` and pass the result here.
|
|
358
|
+
*/
|
|
359
|
+
conversationKind?: ConversationKind;
|
|
346
360
|
}
|
|
347
361
|
interface UseFileStatusReturn {
|
|
348
362
|
/** The latest status response, or null on first render / disabled. */
|
|
@@ -362,29 +376,39 @@ interface UseFileStatusReturn {
|
|
|
362
376
|
refresh: () => Promise<void>;
|
|
363
377
|
}
|
|
364
378
|
/**
|
|
365
|
-
* Subscribes to {@link FileStatus} for a single file.
|
|
366
|
-
* pull-only hook — single fetch by default, optional polling.
|
|
379
|
+
* Subscribes to {@link FileStatus} for a single file.
|
|
367
380
|
*
|
|
368
|
-
*
|
|
369
|
-
* for chat surfaces — `useFileStatus({ messageId })` will subscribe to
|
|
370
|
-
* `file.status` events on the existing per-conversation realtime channel
|
|
371
|
-
* via the `scalemule-chat` translation bridge (P5'). Until that lands,
|
|
372
|
-
* customers using this hook from chat surfaces should pass `pollIntervalMs`
|
|
373
|
-
* in the 1–3 second range while a media pipeline is expected to be running,
|
|
374
|
-
* then drop the polling once `isReady` is true.
|
|
381
|
+
* Three call shapes:
|
|
375
382
|
*
|
|
376
|
-
*
|
|
383
|
+
* 1. **Pull-only** — `useFileStatus({ fileId })` plus optional `pollIntervalMs`.
|
|
384
|
+
* One-shot fetch; useful for non-chat surfaces or static reads.
|
|
385
|
+
*
|
|
386
|
+
* 2. **Chat-surface push** — `useFileStatus({ fileId, conversationId, conversationKind? })`.
|
|
387
|
+
* Subscribes to the conversation channel and refreshes when
|
|
388
|
+
* `file_status_changed` arrives for this `fileId`. The chat service's
|
|
389
|
+
* media-status bridge fans photo/video lifecycle events into the per-conversation
|
|
390
|
+
* channel; the hook drops events for other files and dedupes against polling.
|
|
391
|
+
*
|
|
392
|
+
* 3. **Push + slow poll fallback** — combine `conversationId` with `pollIntervalMs`
|
|
393
|
+
* if you want belt-and-suspenders for environments where the websocket may drop.
|
|
394
|
+
*
|
|
395
|
+
* The `surface: 'profile'` push variant (private-user channel) is deferred until
|
|
396
|
+
* the realtime SDK exposes private-channel subscription by user.
|
|
397
|
+
*
|
|
398
|
+
* @example Chat surface (push)
|
|
377
399
|
* ```tsx
|
|
378
|
-
* function ChatImage({ fileId }: { fileId: string }) {
|
|
379
|
-
* const { status, isReady } = useFileStatus({
|
|
380
|
-
* fileId,
|
|
381
|
-
* pollIntervalMs: 2000,
|
|
382
|
-
* });
|
|
400
|
+
* function ChatImage({ fileId, conversationId }: { fileId: string; conversationId: string }) {
|
|
401
|
+
* const { status, isReady } = useFileStatus({ fileId, conversationId });
|
|
383
402
|
* if (!isReady) return <div>Scanning…</div>;
|
|
384
403
|
* const src = status?.urls.optimized ?? status?.urls.original;
|
|
385
404
|
* return <img src={src} />;
|
|
386
405
|
* }
|
|
387
406
|
* ```
|
|
407
|
+
*
|
|
408
|
+
* @example Non-chat surface (pull + poll)
|
|
409
|
+
* ```tsx
|
|
410
|
+
* const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 });
|
|
411
|
+
* ```
|
|
388
412
|
*/
|
|
389
413
|
declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
|
|
390
414
|
|
|
@@ -413,9 +437,23 @@ interface ScaleMuleMediaProps {
|
|
|
413
437
|
/**
|
|
414
438
|
* Polling interval while waiting for the media pipeline to complete.
|
|
415
439
|
* Defaults to 2000ms; set to `null` to disable polling. Polling stops
|
|
416
|
-
* automatically once scan is clean.
|
|
440
|
+
* automatically once scan is clean. When `conversationId` is set the
|
|
441
|
+
* component receives push updates and you can usually pass `null`.
|
|
417
442
|
*/
|
|
418
443
|
pollIntervalMs?: number | null;
|
|
444
|
+
/**
|
|
445
|
+
* Chat-surface push: when set, the underlying `useFileStatus` hook
|
|
446
|
+
* subscribes to the conversation's realtime channel and refreshes on
|
|
447
|
+
* `file_status_changed` events for this `fileId`. See `useFileStatus`
|
|
448
|
+
* docs for channel naming and bridge behaviour.
|
|
449
|
+
*/
|
|
450
|
+
conversationId?: string | null;
|
|
451
|
+
/**
|
|
452
|
+
* Conversation kind, controls the channel name prefix
|
|
453
|
+
* (`conversation:{id}` vs `conversation:lr|bc|support:{id}`).
|
|
454
|
+
* Default is `'standard'`.
|
|
455
|
+
*/
|
|
456
|
+
conversationKind?: ConversationKind;
|
|
419
457
|
/**
|
|
420
458
|
* Render a custom placeholder while waiting for scan / upload. Defaults
|
|
421
459
|
* to a tiny "Loading…" div. Receives the current FileStatus (or null).
|
|
@@ -950,4 +988,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
950
988
|
error: (message: string, data?: unknown) => void;
|
|
951
989
|
};
|
|
952
990
|
|
|
953
|
-
export { ApiError$1 as ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
991
|
+
export { ApiError$1 as ApiError, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
package/dist/index.js
CHANGED
|
@@ -7569,6 +7569,28 @@ function ScaleMuleProvider({
|
|
|
7569
7569
|
debug
|
|
7570
7570
|
});
|
|
7571
7571
|
}, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
|
|
7572
|
+
const [fetchedPolicy, setFetchedPolicy] = React.useState(void 0);
|
|
7573
|
+
React.useEffect(() => {
|
|
7574
|
+
if (mediaPolicy) return;
|
|
7575
|
+
let mounted = true;
|
|
7576
|
+
void (async () => {
|
|
7577
|
+
try {
|
|
7578
|
+
const fn = baseClient.storage.getPolicy;
|
|
7579
|
+
if (typeof fn !== "function") return;
|
|
7580
|
+
const r = await fn.call(baseClient.storage);
|
|
7581
|
+
if (!mounted) return;
|
|
7582
|
+
const v = r?.data?.media_policy;
|
|
7583
|
+
if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
|
|
7584
|
+
setFetchedPolicy(v);
|
|
7585
|
+
}
|
|
7586
|
+
} catch {
|
|
7587
|
+
}
|
|
7588
|
+
})();
|
|
7589
|
+
return () => {
|
|
7590
|
+
mounted = false;
|
|
7591
|
+
};
|
|
7592
|
+
}, [baseClient, mediaPolicy]);
|
|
7593
|
+
const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
|
|
7572
7594
|
React.useEffect(() => {
|
|
7573
7595
|
const token = client.getSessionToken();
|
|
7574
7596
|
if (token) {
|
|
@@ -7672,7 +7694,7 @@ function ScaleMuleProvider({
|
|
|
7672
7694
|
photo: baseClient.photo,
|
|
7673
7695
|
video: baseClient.video,
|
|
7674
7696
|
audio: baseClient.audio,
|
|
7675
|
-
mediaPolicy,
|
|
7697
|
+
mediaPolicy: effectiveMediaPolicy,
|
|
7676
7698
|
user,
|
|
7677
7699
|
setUser: handleSetUser,
|
|
7678
7700
|
initializing,
|
|
@@ -7687,7 +7709,7 @@ function ScaleMuleProvider({
|
|
|
7687
7709
|
accountSwitcherPrivacy,
|
|
7688
7710
|
bootstrapFlags
|
|
7689
7711
|
}),
|
|
7690
|
-
[client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags,
|
|
7712
|
+
[client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
|
|
7691
7713
|
);
|
|
7692
7714
|
return /* @__PURE__ */ jsxRuntime.jsx(ScaleMuleContext.Provider, { value, children });
|
|
7693
7715
|
}
|
|
@@ -9103,10 +9125,13 @@ function useMedia() {
|
|
|
9103
9125
|
async (fileId) => {
|
|
9104
9126
|
setError(null);
|
|
9105
9127
|
try {
|
|
9106
|
-
const
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9128
|
+
const [, storageResult] = await Promise.all([
|
|
9129
|
+
photo.delete(fileId).catch(() => void 0),
|
|
9130
|
+
storage.delete(fileId)
|
|
9131
|
+
]);
|
|
9132
|
+
if (storageResult.error) {
|
|
9133
|
+
if (storageResult.error.status === 404) return;
|
|
9134
|
+
throw storageResult.error;
|
|
9110
9135
|
}
|
|
9111
9136
|
} catch (err) {
|
|
9112
9137
|
const e = err;
|
|
@@ -9114,13 +9139,32 @@ function useMedia() {
|
|
|
9114
9139
|
throw e;
|
|
9115
9140
|
}
|
|
9116
9141
|
},
|
|
9117
|
-
[storage]
|
|
9142
|
+
[storage, photo]
|
|
9118
9143
|
);
|
|
9119
9144
|
return { upload, cancelUpload, error, uploading };
|
|
9120
9145
|
}
|
|
9146
|
+
function conversationChannel(kind, id) {
|
|
9147
|
+
switch (kind) {
|
|
9148
|
+
case "large_room":
|
|
9149
|
+
return `conversation:lr:${id}`;
|
|
9150
|
+
case "broadcast":
|
|
9151
|
+
return `conversation:bc:${id}`;
|
|
9152
|
+
case "support":
|
|
9153
|
+
return `conversation:support:${id}`;
|
|
9154
|
+
case "standard":
|
|
9155
|
+
default:
|
|
9156
|
+
return `conversation:${id}`;
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9121
9159
|
function useFileStatus(options) {
|
|
9122
|
-
const { storage } = useScaleMule();
|
|
9123
|
-
const {
|
|
9160
|
+
const { storage, realtime } = useScaleMule();
|
|
9161
|
+
const {
|
|
9162
|
+
fileId,
|
|
9163
|
+
pollIntervalMs = null,
|
|
9164
|
+
disabled = false,
|
|
9165
|
+
conversationId = null,
|
|
9166
|
+
conversationKind = "standard"
|
|
9167
|
+
} = options;
|
|
9124
9168
|
const [status, setStatus] = React.useState(null);
|
|
9125
9169
|
const [loading, setLoading] = React.useState(false);
|
|
9126
9170
|
const [error, setError] = React.useState(null);
|
|
@@ -9152,13 +9196,40 @@ function useFileStatus(options) {
|
|
|
9152
9196
|
}, [fetchStatus]);
|
|
9153
9197
|
React.useEffect(() => {
|
|
9154
9198
|
if (!pollIntervalMs || disabled || !fileId) return;
|
|
9155
|
-
const
|
|
9156
|
-
if (
|
|
9199
|
+
const scan = status?.scan.status;
|
|
9200
|
+
if (scan === "threat" || scan === "quarantined" || scan === "error") return;
|
|
9201
|
+
const optimizePending = status?.optimize != null && status.optimize.status !== "done";
|
|
9202
|
+
const transcodePending = status?.transcode != null && status.transcode.status !== "done";
|
|
9203
|
+
const pipelinePending = optimizePending || transcodePending;
|
|
9204
|
+
const isSettled = scan === "clean" && !pipelinePending;
|
|
9205
|
+
if (isSettled) return;
|
|
9157
9206
|
const id = setInterval(() => {
|
|
9158
9207
|
void fetchStatus();
|
|
9159
9208
|
}, pollIntervalMs);
|
|
9160
9209
|
return () => clearInterval(id);
|
|
9161
|
-
}, [
|
|
9210
|
+
}, [
|
|
9211
|
+
pollIntervalMs,
|
|
9212
|
+
disabled,
|
|
9213
|
+
fileId,
|
|
9214
|
+
status?.scan.status,
|
|
9215
|
+
status?.optimize,
|
|
9216
|
+
status?.transcode,
|
|
9217
|
+
fetchStatus
|
|
9218
|
+
]);
|
|
9219
|
+
React.useEffect(() => {
|
|
9220
|
+
if (!conversationId || !fileId || disabled) return;
|
|
9221
|
+
const channel = conversationChannel(conversationKind, conversationId);
|
|
9222
|
+
const unsub = realtime.subscribe(channel, (data) => {
|
|
9223
|
+
if (typeof data !== "object" || data === null) return;
|
|
9224
|
+
const payload = data;
|
|
9225
|
+
const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
|
|
9226
|
+
const evt = typeof payload.event === "string" ? payload.event : void 0;
|
|
9227
|
+
if (evt && evt !== "file_status_changed") return;
|
|
9228
|
+
if (wrapped.file_id !== fileId) return;
|
|
9229
|
+
void fetchStatus();
|
|
9230
|
+
});
|
|
9231
|
+
return unsub;
|
|
9232
|
+
}, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
|
|
9162
9233
|
const isReady = status?.scan.status === "clean";
|
|
9163
9234
|
return { status, loading, error, isReady, refresh: fetchStatus };
|
|
9164
9235
|
}
|
|
@@ -9173,32 +9244,54 @@ function ScaleMuleMedia(props) {
|
|
|
9173
9244
|
style,
|
|
9174
9245
|
alt,
|
|
9175
9246
|
pollIntervalMs = 2e3,
|
|
9247
|
+
conversationId = null,
|
|
9248
|
+
conversationKind,
|
|
9176
9249
|
renderPlaceholder,
|
|
9177
9250
|
renderBlocked,
|
|
9178
9251
|
renderOverride
|
|
9179
9252
|
} = props;
|
|
9180
|
-
const {
|
|
9253
|
+
const { gatewayUrl } = useScaleMule();
|
|
9254
|
+
const { status, isReady } = useFileStatus({
|
|
9255
|
+
fileId,
|
|
9256
|
+
pollIntervalMs,
|
|
9257
|
+
conversationId,
|
|
9258
|
+
conversationKind
|
|
9259
|
+
});
|
|
9260
|
+
const absoluteUrl = React__namespace.useCallback(
|
|
9261
|
+
(url) => {
|
|
9262
|
+
if (!url) return null;
|
|
9263
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
|
|
9264
|
+
return url;
|
|
9265
|
+
}
|
|
9266
|
+
if (!gatewayUrl) return url;
|
|
9267
|
+
const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
|
|
9268
|
+
const path = url.startsWith("/") ? url : `/${url}`;
|
|
9269
|
+
return `${base}${path}`;
|
|
9270
|
+
},
|
|
9271
|
+
[gatewayUrl]
|
|
9272
|
+
);
|
|
9181
9273
|
const isImage = mimeType.startsWith("image/");
|
|
9182
9274
|
const isVideo = mimeType.startsWith("video/");
|
|
9183
9275
|
const isAudio = mimeType.startsWith("audio/");
|
|
9184
9276
|
const src = React.useMemo(() => {
|
|
9185
9277
|
const scan = status?.scan.status;
|
|
9186
9278
|
if (scan === "threat" || scan === "quarantined") return null;
|
|
9187
|
-
if (status
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
if (
|
|
9193
|
-
return null;
|
|
9194
|
-
}, [status,
|
|
9279
|
+
if (!status) {
|
|
9280
|
+
return blobPreview ?? null;
|
|
9281
|
+
}
|
|
9282
|
+
if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
|
|
9283
|
+
if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
|
|
9284
|
+
if (status.urls.original) return absoluteUrl(status.urls.original);
|
|
9285
|
+
return blobPreview ?? null;
|
|
9286
|
+
}, [status, isImage, isVideo, blobPreview, absoluteUrl]);
|
|
9195
9287
|
const renderState = React.useMemo(() => {
|
|
9196
9288
|
const scan = status?.scan.status;
|
|
9197
9289
|
if (scan === "threat" || scan === "quarantined") return "blocked";
|
|
9290
|
+
if (status?.urls.original) return "ready";
|
|
9198
9291
|
if (isReady) return "ready";
|
|
9199
9292
|
if (blobPreview) return "preview";
|
|
9200
9293
|
return "pending";
|
|
9201
|
-
}, [status?.scan.status, isReady, blobPreview]);
|
|
9294
|
+
}, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
|
|
9202
9295
|
const videoRef = React__namespace.useRef(null);
|
|
9203
9296
|
React.useEffect(() => {
|
|
9204
9297
|
if (!isVideo) return;
|
package/dist/index.mjs
CHANGED
|
@@ -7549,6 +7549,28 @@ function ScaleMuleProvider({
|
|
|
7549
7549
|
debug
|
|
7550
7550
|
});
|
|
7551
7551
|
}, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
|
|
7552
|
+
const [fetchedPolicy, setFetchedPolicy] = useState(void 0);
|
|
7553
|
+
useEffect(() => {
|
|
7554
|
+
if (mediaPolicy) return;
|
|
7555
|
+
let mounted = true;
|
|
7556
|
+
void (async () => {
|
|
7557
|
+
try {
|
|
7558
|
+
const fn = baseClient.storage.getPolicy;
|
|
7559
|
+
if (typeof fn !== "function") return;
|
|
7560
|
+
const r = await fn.call(baseClient.storage);
|
|
7561
|
+
if (!mounted) return;
|
|
7562
|
+
const v = r?.data?.media_policy;
|
|
7563
|
+
if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
|
|
7564
|
+
setFetchedPolicy(v);
|
|
7565
|
+
}
|
|
7566
|
+
} catch {
|
|
7567
|
+
}
|
|
7568
|
+
})();
|
|
7569
|
+
return () => {
|
|
7570
|
+
mounted = false;
|
|
7571
|
+
};
|
|
7572
|
+
}, [baseClient, mediaPolicy]);
|
|
7573
|
+
const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
|
|
7552
7574
|
useEffect(() => {
|
|
7553
7575
|
const token = client.getSessionToken();
|
|
7554
7576
|
if (token) {
|
|
@@ -7652,7 +7674,7 @@ function ScaleMuleProvider({
|
|
|
7652
7674
|
photo: baseClient.photo,
|
|
7653
7675
|
video: baseClient.video,
|
|
7654
7676
|
audio: baseClient.audio,
|
|
7655
|
-
mediaPolicy,
|
|
7677
|
+
mediaPolicy: effectiveMediaPolicy,
|
|
7656
7678
|
user,
|
|
7657
7679
|
setUser: handleSetUser,
|
|
7658
7680
|
initializing,
|
|
@@ -7667,7 +7689,7 @@ function ScaleMuleProvider({
|
|
|
7667
7689
|
accountSwitcherPrivacy,
|
|
7668
7690
|
bootstrapFlags
|
|
7669
7691
|
}),
|
|
7670
|
-
[client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags,
|
|
7692
|
+
[client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
|
|
7671
7693
|
);
|
|
7672
7694
|
return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
|
|
7673
7695
|
}
|
|
@@ -9083,10 +9105,13 @@ function useMedia() {
|
|
|
9083
9105
|
async (fileId) => {
|
|
9084
9106
|
setError(null);
|
|
9085
9107
|
try {
|
|
9086
|
-
const
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9108
|
+
const [, storageResult] = await Promise.all([
|
|
9109
|
+
photo.delete(fileId).catch(() => void 0),
|
|
9110
|
+
storage.delete(fileId)
|
|
9111
|
+
]);
|
|
9112
|
+
if (storageResult.error) {
|
|
9113
|
+
if (storageResult.error.status === 404) return;
|
|
9114
|
+
throw storageResult.error;
|
|
9090
9115
|
}
|
|
9091
9116
|
} catch (err) {
|
|
9092
9117
|
const e = err;
|
|
@@ -9094,13 +9119,32 @@ function useMedia() {
|
|
|
9094
9119
|
throw e;
|
|
9095
9120
|
}
|
|
9096
9121
|
},
|
|
9097
|
-
[storage]
|
|
9122
|
+
[storage, photo]
|
|
9098
9123
|
);
|
|
9099
9124
|
return { upload, cancelUpload, error, uploading };
|
|
9100
9125
|
}
|
|
9126
|
+
function conversationChannel(kind, id) {
|
|
9127
|
+
switch (kind) {
|
|
9128
|
+
case "large_room":
|
|
9129
|
+
return `conversation:lr:${id}`;
|
|
9130
|
+
case "broadcast":
|
|
9131
|
+
return `conversation:bc:${id}`;
|
|
9132
|
+
case "support":
|
|
9133
|
+
return `conversation:support:${id}`;
|
|
9134
|
+
case "standard":
|
|
9135
|
+
default:
|
|
9136
|
+
return `conversation:${id}`;
|
|
9137
|
+
}
|
|
9138
|
+
}
|
|
9101
9139
|
function useFileStatus(options) {
|
|
9102
|
-
const { storage } = useScaleMule();
|
|
9103
|
-
const {
|
|
9140
|
+
const { storage, realtime } = useScaleMule();
|
|
9141
|
+
const {
|
|
9142
|
+
fileId,
|
|
9143
|
+
pollIntervalMs = null,
|
|
9144
|
+
disabled = false,
|
|
9145
|
+
conversationId = null,
|
|
9146
|
+
conversationKind = "standard"
|
|
9147
|
+
} = options;
|
|
9104
9148
|
const [status, setStatus] = useState(null);
|
|
9105
9149
|
const [loading, setLoading] = useState(false);
|
|
9106
9150
|
const [error, setError] = useState(null);
|
|
@@ -9132,13 +9176,40 @@ function useFileStatus(options) {
|
|
|
9132
9176
|
}, [fetchStatus]);
|
|
9133
9177
|
useEffect(() => {
|
|
9134
9178
|
if (!pollIntervalMs || disabled || !fileId) return;
|
|
9135
|
-
const
|
|
9136
|
-
if (
|
|
9179
|
+
const scan = status?.scan.status;
|
|
9180
|
+
if (scan === "threat" || scan === "quarantined" || scan === "error") return;
|
|
9181
|
+
const optimizePending = status?.optimize != null && status.optimize.status !== "done";
|
|
9182
|
+
const transcodePending = status?.transcode != null && status.transcode.status !== "done";
|
|
9183
|
+
const pipelinePending = optimizePending || transcodePending;
|
|
9184
|
+
const isSettled = scan === "clean" && !pipelinePending;
|
|
9185
|
+
if (isSettled) return;
|
|
9137
9186
|
const id = setInterval(() => {
|
|
9138
9187
|
void fetchStatus();
|
|
9139
9188
|
}, pollIntervalMs);
|
|
9140
9189
|
return () => clearInterval(id);
|
|
9141
|
-
}, [
|
|
9190
|
+
}, [
|
|
9191
|
+
pollIntervalMs,
|
|
9192
|
+
disabled,
|
|
9193
|
+
fileId,
|
|
9194
|
+
status?.scan.status,
|
|
9195
|
+
status?.optimize,
|
|
9196
|
+
status?.transcode,
|
|
9197
|
+
fetchStatus
|
|
9198
|
+
]);
|
|
9199
|
+
useEffect(() => {
|
|
9200
|
+
if (!conversationId || !fileId || disabled) return;
|
|
9201
|
+
const channel = conversationChannel(conversationKind, conversationId);
|
|
9202
|
+
const unsub = realtime.subscribe(channel, (data) => {
|
|
9203
|
+
if (typeof data !== "object" || data === null) return;
|
|
9204
|
+
const payload = data;
|
|
9205
|
+
const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
|
|
9206
|
+
const evt = typeof payload.event === "string" ? payload.event : void 0;
|
|
9207
|
+
if (evt && evt !== "file_status_changed") return;
|
|
9208
|
+
if (wrapped.file_id !== fileId) return;
|
|
9209
|
+
void fetchStatus();
|
|
9210
|
+
});
|
|
9211
|
+
return unsub;
|
|
9212
|
+
}, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
|
|
9142
9213
|
const isReady = status?.scan.status === "clean";
|
|
9143
9214
|
return { status, loading, error, isReady, refresh: fetchStatus };
|
|
9144
9215
|
}
|
|
@@ -9153,32 +9224,54 @@ function ScaleMuleMedia(props) {
|
|
|
9153
9224
|
style,
|
|
9154
9225
|
alt,
|
|
9155
9226
|
pollIntervalMs = 2e3,
|
|
9227
|
+
conversationId = null,
|
|
9228
|
+
conversationKind,
|
|
9156
9229
|
renderPlaceholder,
|
|
9157
9230
|
renderBlocked,
|
|
9158
9231
|
renderOverride
|
|
9159
9232
|
} = props;
|
|
9160
|
-
const {
|
|
9233
|
+
const { gatewayUrl } = useScaleMule();
|
|
9234
|
+
const { status, isReady } = useFileStatus({
|
|
9235
|
+
fileId,
|
|
9236
|
+
pollIntervalMs,
|
|
9237
|
+
conversationId,
|
|
9238
|
+
conversationKind
|
|
9239
|
+
});
|
|
9240
|
+
const absoluteUrl = React.useCallback(
|
|
9241
|
+
(url) => {
|
|
9242
|
+
if (!url) return null;
|
|
9243
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
|
|
9244
|
+
return url;
|
|
9245
|
+
}
|
|
9246
|
+
if (!gatewayUrl) return url;
|
|
9247
|
+
const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
|
|
9248
|
+
const path = url.startsWith("/") ? url : `/${url}`;
|
|
9249
|
+
return `${base}${path}`;
|
|
9250
|
+
},
|
|
9251
|
+
[gatewayUrl]
|
|
9252
|
+
);
|
|
9161
9253
|
const isImage = mimeType.startsWith("image/");
|
|
9162
9254
|
const isVideo = mimeType.startsWith("video/");
|
|
9163
9255
|
const isAudio = mimeType.startsWith("audio/");
|
|
9164
9256
|
const src = useMemo(() => {
|
|
9165
9257
|
const scan = status?.scan.status;
|
|
9166
9258
|
if (scan === "threat" || scan === "quarantined") return null;
|
|
9167
|
-
if (status
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
if (
|
|
9173
|
-
return null;
|
|
9174
|
-
}, [status,
|
|
9259
|
+
if (!status) {
|
|
9260
|
+
return blobPreview ?? null;
|
|
9261
|
+
}
|
|
9262
|
+
if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
|
|
9263
|
+
if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
|
|
9264
|
+
if (status.urls.original) return absoluteUrl(status.urls.original);
|
|
9265
|
+
return blobPreview ?? null;
|
|
9266
|
+
}, [status, isImage, isVideo, blobPreview, absoluteUrl]);
|
|
9175
9267
|
const renderState = useMemo(() => {
|
|
9176
9268
|
const scan = status?.scan.status;
|
|
9177
9269
|
if (scan === "threat" || scan === "quarantined") return "blocked";
|
|
9270
|
+
if (status?.urls.original) return "ready";
|
|
9178
9271
|
if (isReady) return "ready";
|
|
9179
9272
|
if (blobPreview) return "preview";
|
|
9180
9273
|
return "pending";
|
|
9181
|
-
}, [status?.scan.status, isReady, blobPreview]);
|
|
9274
|
+
}, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
|
|
9182
9275
|
const videoRef = React.useRef(null);
|
|
9183
9276
|
useEffect(() => {
|
|
9184
9277
|
if (!isVideo) return;
|
package/package.json
CHANGED