@scalemule/nextjs 0.1.8 → 0.1.9

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
@@ -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
@@ -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
@@ -9118,9 +9118,28 @@ function useMedia() {
9118
9118
  );
9119
9119
  return { upload, cancelUpload, error, uploading };
9120
9120
  }
9121
+ function conversationChannel(kind, id) {
9122
+ switch (kind) {
9123
+ case "large_room":
9124
+ return `conversation:lr:${id}`;
9125
+ case "broadcast":
9126
+ return `conversation:bc:${id}`;
9127
+ case "support":
9128
+ return `conversation:support:${id}`;
9129
+ case "standard":
9130
+ default:
9131
+ return `conversation:${id}`;
9132
+ }
9133
+ }
9121
9134
  function useFileStatus(options) {
9122
- const { storage } = useScaleMule();
9123
- const { fileId, pollIntervalMs = null, disabled = false } = options;
9135
+ const { storage, realtime } = useScaleMule();
9136
+ const {
9137
+ fileId,
9138
+ pollIntervalMs = null,
9139
+ disabled = false,
9140
+ conversationId = null,
9141
+ conversationKind = "standard"
9142
+ } = options;
9124
9143
  const [status, setStatus] = React.useState(null);
9125
9144
  const [loading, setLoading] = React.useState(false);
9126
9145
  const [error, setError] = React.useState(null);
@@ -9159,6 +9178,20 @@ function useFileStatus(options) {
9159
9178
  }, pollIntervalMs);
9160
9179
  return () => clearInterval(id);
9161
9180
  }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9181
+ React.useEffect(() => {
9182
+ if (!conversationId || !fileId || disabled) return;
9183
+ const channel = conversationChannel(conversationKind, conversationId);
9184
+ const unsub = realtime.subscribe(channel, (data) => {
9185
+ if (typeof data !== "object" || data === null) return;
9186
+ const payload = data;
9187
+ const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
9188
+ const evt = typeof payload.event === "string" ? payload.event : void 0;
9189
+ if (evt && evt !== "file_status_changed") return;
9190
+ if (wrapped.file_id !== fileId) return;
9191
+ void fetchStatus();
9192
+ });
9193
+ return unsub;
9194
+ }, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
9162
9195
  const isReady = status?.scan.status === "clean";
9163
9196
  return { status, loading, error, isReady, refresh: fetchStatus };
9164
9197
  }
@@ -9173,11 +9206,18 @@ function ScaleMuleMedia(props) {
9173
9206
  style,
9174
9207
  alt,
9175
9208
  pollIntervalMs = 2e3,
9209
+ conversationId = null,
9210
+ conversationKind,
9176
9211
  renderPlaceholder,
9177
9212
  renderBlocked,
9178
9213
  renderOverride
9179
9214
  } = props;
9180
- const { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
9215
+ const { status, isReady } = useFileStatus({
9216
+ fileId,
9217
+ pollIntervalMs,
9218
+ conversationId,
9219
+ conversationKind
9220
+ });
9181
9221
  const isImage = mimeType.startsWith("image/");
9182
9222
  const isVideo = mimeType.startsWith("video/");
9183
9223
  const isAudio = mimeType.startsWith("audio/");
package/dist/index.mjs CHANGED
@@ -9098,9 +9098,28 @@ function useMedia() {
9098
9098
  );
9099
9099
  return { upload, cancelUpload, error, uploading };
9100
9100
  }
9101
+ function conversationChannel(kind, id) {
9102
+ switch (kind) {
9103
+ case "large_room":
9104
+ return `conversation:lr:${id}`;
9105
+ case "broadcast":
9106
+ return `conversation:bc:${id}`;
9107
+ case "support":
9108
+ return `conversation:support:${id}`;
9109
+ case "standard":
9110
+ default:
9111
+ return `conversation:${id}`;
9112
+ }
9113
+ }
9101
9114
  function useFileStatus(options) {
9102
- const { storage } = useScaleMule();
9103
- const { fileId, pollIntervalMs = null, disabled = false } = options;
9115
+ const { storage, realtime } = useScaleMule();
9116
+ const {
9117
+ fileId,
9118
+ pollIntervalMs = null,
9119
+ disabled = false,
9120
+ conversationId = null,
9121
+ conversationKind = "standard"
9122
+ } = options;
9104
9123
  const [status, setStatus] = useState(null);
9105
9124
  const [loading, setLoading] = useState(false);
9106
9125
  const [error, setError] = useState(null);
@@ -9139,6 +9158,20 @@ function useFileStatus(options) {
9139
9158
  }, pollIntervalMs);
9140
9159
  return () => clearInterval(id);
9141
9160
  }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9161
+ useEffect(() => {
9162
+ if (!conversationId || !fileId || disabled) return;
9163
+ const channel = conversationChannel(conversationKind, conversationId);
9164
+ const unsub = realtime.subscribe(channel, (data) => {
9165
+ if (typeof data !== "object" || data === null) return;
9166
+ const payload = data;
9167
+ const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
9168
+ const evt = typeof payload.event === "string" ? payload.event : void 0;
9169
+ if (evt && evt !== "file_status_changed") return;
9170
+ if (wrapped.file_id !== fileId) return;
9171
+ void fetchStatus();
9172
+ });
9173
+ return unsub;
9174
+ }, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
9142
9175
  const isReady = status?.scan.status === "clean";
9143
9176
  return { status, loading, error, isReady, refresh: fetchStatus };
9144
9177
  }
@@ -9153,11 +9186,18 @@ function ScaleMuleMedia(props) {
9153
9186
  style,
9154
9187
  alt,
9155
9188
  pollIntervalMs = 2e3,
9189
+ conversationId = null,
9190
+ conversationKind,
9156
9191
  renderPlaceholder,
9157
9192
  renderBlocked,
9158
9193
  renderOverride
9159
9194
  } = props;
9160
- const { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
9195
+ const { status, isReady } = useFileStatus({
9196
+ fileId,
9197
+ pollIntervalMs,
9198
+ conversationId,
9199
+ conversationKind
9200
+ });
9161
9201
  const isImage = mimeType.startsWith("image/");
9162
9202
  const isVideo = mimeType.startsWith("video/");
9163
9203
  const isAudio = mimeType.startsWith("audio/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",