@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 CHANGED
@@ -132,7 +132,67 @@ function LoginPage() {
132
132
  }
133
133
  ```
134
134
 
135
- ### `useContent()`
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). That endpoint is `MemberOnly` admin-auth, so end-user-context
220
- * provider can't fetch it directly — apps that want policy-driven
221
- * defaults should declare the value here, mirroring whatever the
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
- * Default: undefined → `useMedia()` falls back to its built-in
225
- * `safe_visible` default.
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`. Useful while waiting for transcode /
333
- * optimization to complete. Pass `null` (default) for a one-shot read.
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
- * Disable the hook (don't fetch). Useful when `fileId` is conditional.
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
- disabled?: boolean;
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. Today this is a
366
- * pull-only hook — single fetch by default, optional polling.
379
+ * Subscribes to {@link FileStatus} for a single file.
367
380
  *
368
- * Phase 3 of the realtime-chat media pipeline ADR will add a push variant
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
- * @example
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). That endpoint is `MemberOnly` admin-auth, so end-user-context
220
- * provider can't fetch it directly — apps that want policy-driven
221
- * defaults should declare the value here, mirroring whatever the
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
- * Default: undefined → `useMedia()` falls back to its built-in
225
- * `safe_visible` default.
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`. Useful while waiting for transcode /
333
- * optimization to complete. Pass `null` (default) for a one-shot read.
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
- * Disable the hook (don't fetch). Useful when `fileId` is conditional.
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
- disabled?: boolean;
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. Today this is a
366
- * pull-only hook — single fetch by default, optional polling.
379
+ * Subscribes to {@link FileStatus} for a single file.
367
380
  *
368
- * Phase 3 of the realtime-chat media pipeline ADR will add a push variant
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
- * @example
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, mediaPolicy]
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 r = await storage.delete(fileId);
9107
- if (r.error) {
9108
- if (r.error.status === 404) return;
9109
- throw r.error;
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 { fileId, pollIntervalMs = null, disabled = false } = options;
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 isClean = status?.scan.status === "clean";
9156
- if (isClean) return;
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
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
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 { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
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 && isReady) {
9188
- if (isImage && status.urls.optimized) return status.urls.optimized;
9189
- if (isVideo && status.urls.hls) return status.urls.hls;
9190
- return status.urls.original ?? null;
9191
- }
9192
- if (blobPreview) return blobPreview;
9193
- return null;
9194
- }, [status, isReady, isImage, isVideo, blobPreview]);
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, mediaPolicy]
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 r = await storage.delete(fileId);
9087
- if (r.error) {
9088
- if (r.error.status === 404) return;
9089
- throw r.error;
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 { fileId, pollIntervalMs = null, disabled = false } = options;
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 isClean = status?.scan.status === "clean";
9136
- if (isClean) return;
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
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
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 { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
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 && isReady) {
9168
- if (isImage && status.urls.optimized) return status.urls.optimized;
9169
- if (isVideo && status.urls.hls) return status.urls.hls;
9170
- return status.urls.original ?? null;
9171
- }
9172
- if (blobPreview) return blobPreview;
9173
- return null;
9174
- }, [status, isReady, isImage, isVideo, blobPreview]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",