@shortkitsdk/react-native 0.2.5 → 0.2.11

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.
Files changed (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +5 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +319 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +559 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +984 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +88 -220
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +12 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +126 -706
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +2 -2
  11. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
  12. package/ios/ReactCarouselOverlayHost.swift +177 -0
  13. package/ios/ReactLoadingHost.swift +38 -0
  14. package/ios/ReactOverlayHost.swift +458 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +266 -65
  18. package/ios/ShortKitFeedView.swift +63 -207
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +86 -32
  21. package/ios/ShortKitPlayerNativeView.swift +39 -8
  22. package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +2 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3998 -962
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +85 -24
  26. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +85 -24
  28. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +2 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3998 -962
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +85 -24
  32. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +85 -24
  34. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  35. package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
  36. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  37. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  38. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
  39. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
  40. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  41. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
  42. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  43. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  44. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  45. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  46. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
  47. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
  48. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  52. package/ios/ShortKitWidgetNativeView.swift +3 -3
  53. package/package.json +1 -1
  54. package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
  55. package/src/ShortKitCommands.ts +31 -0
  56. package/src/ShortKitContext.ts +11 -25
  57. package/src/ShortKitFeed.tsx +110 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +205 -0
  60. package/src/ShortKitPlayer.tsx +6 -7
  61. package/src/ShortKitProvider.tsx +65 -250
  62. package/src/index.ts +9 -4
  63. package/src/serialization.ts +22 -42
  64. package/src/specs/NativeShortKitModule.ts +67 -53
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +104 -19
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -8
  69. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
  70. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
  71. package/ios/ShortKitCarouselOverlayBridge.swift +0 -54
  72. package/ios/ShortKitOverlayBridge.swift +0 -113
  73. package/src/CarouselOverlayManager.tsx +0 -71
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -0,0 +1,4 @@
1
+ framework module ShortKitSDK {
2
+ header "ShortKitSDK-Swift.h"
3
+ export *
4
+ }
@@ -127,9 +127,9 @@ import ShortKitSDK
127
127
  if let overlayObj = obj["overlay"] as? [String: Any],
128
128
  overlayObj["type"] as? String == "custom" {
129
129
  overlayMode = .custom { @Sendable in
130
- let overlay = ShortKitOverlayBridge()
131
- overlay.bridge = ShortKitBridge.shared
132
- return overlay
130
+ let host = ReactOverlayHost()
131
+ host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
132
+ return host
133
133
  }
134
134
  } else {
135
135
  overlayMode = .none
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.5",
3
+ "version": "0.2.11",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",
@@ -0,0 +1,55 @@
1
+ import React, { useMemo } from 'react';
2
+ import { AppRegistry } from 'react-native';
3
+ import type { CarouselOverlayProps, ImageCarouselItem } from './types';
4
+
5
+ const _carouselRegistry = new Map<string, React.ComponentType<CarouselOverlayProps>>();
6
+
7
+ /**
8
+ * Register a named carousel overlay component for rendering inside feed cells.
9
+ * Called by ShortKitFeed on mount via useLayoutEffect.
10
+ * Idempotent — registering the same name twice is a no-op.
11
+ */
12
+ export function registerCarouselOverlayComponent(
13
+ name: string,
14
+ component: React.ComponentType<CarouselOverlayProps>,
15
+ ) {
16
+ if (_carouselRegistry.has(name)) return;
17
+ _carouselRegistry.set(name, component);
18
+
19
+ const moduleName = `ShortKitCarouselOverlay_${name}`;
20
+ AppRegistry.registerComponent(moduleName, () => {
21
+ return function NamedCarouselSurface(props: RawCarouselSurfaceProps) {
22
+ return <CarouselSurfaceInner {...props} overlayName={name} />;
23
+ };
24
+ });
25
+ }
26
+
27
+ interface RawCarouselSurfaceProps {
28
+ item?: string;
29
+ }
30
+
31
+ interface InnerProps extends RawCarouselSurfaceProps {
32
+ overlayName: string;
33
+ }
34
+
35
+ function CarouselSurfaceInner(props: InnerProps) {
36
+ const Component = _carouselRegistry.get(props.overlayName);
37
+ if (!Component) {
38
+ return null;
39
+ }
40
+
41
+ const item: ImageCarouselItem | null = useMemo(() => {
42
+ if (!props.item) {
43
+ return null;
44
+ }
45
+ try {
46
+ return JSON.parse(props.item);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }, [props.item]);
51
+
52
+ if (!item) return null;
53
+
54
+ return <Component item={item} />;
55
+ }
@@ -0,0 +1,31 @@
1
+ import NativeShortKitModule from './specs/NativeShortKitModule';
2
+
3
+ /**
4
+ * Imperative player commands for use inside overlay components.
5
+ *
6
+ * Overlay components run in isolated React surfaces and cannot access
7
+ * ShortKitProvider context. Use these commands instead of useShortKitPlayer()
8
+ * hooks for player control.
9
+ */
10
+ export const ShortKitCommands = {
11
+ play: () => NativeShortKitModule?.play(),
12
+ pause: () => NativeShortKitModule?.pause(),
13
+ seek: (seconds: number) => NativeShortKitModule?.seek(seconds),
14
+ seekAndPlay: (seconds: number) => NativeShortKitModule?.seekAndPlay(seconds),
15
+ skipToNext: () => NativeShortKitModule?.skipToNext(),
16
+ skipToPrevious: () => NativeShortKitModule?.skipToPrevious(),
17
+ setMuted: (muted: boolean) => NativeShortKitModule?.setMuted(muted),
18
+ setPlaybackRate: (rate: number) => NativeShortKitModule?.setPlaybackRate(rate),
19
+ setCaptionsEnabled: (enabled: boolean) =>
20
+ NativeShortKitModule?.setCaptionsEnabled(enabled),
21
+ selectCaptionTrack: (language: string) =>
22
+ NativeShortKitModule?.selectCaptionTrack(language),
23
+ sendContentSignal: (signal: 'positive' | 'negative') =>
24
+ NativeShortKitModule?.sendContentSignal(signal),
25
+ setMaxBitrate: (bitrate: number) =>
26
+ NativeShortKitModule?.setMaxBitrate(bitrate),
27
+ prefetchStoryboard: (playbackId: string) =>
28
+ NativeShortKitModule?.prefetchStoryboard(playbackId),
29
+ getStoryboardData: (playbackId: string): Promise<string | null> =>
30
+ NativeShortKitModule?.getStoryboardData(playbackId) ?? Promise.resolve(null),
31
+ } as const;
@@ -1,21 +1,20 @@
1
1
  import { createContext } from 'react';
2
2
  import type {
3
3
  ContentItem,
4
- ImageCarouselItem,
5
- CustomFeedItem,
4
+ FeedConfig,
5
+ FeedFilter,
6
6
  PlayerTime,
7
7
  PlayerState,
8
8
  CaptionTrack,
9
+ FeedScrollPhase,
9
10
  ContentSignal,
10
- OverlayConfig,
11
- CarouselOverlayConfig,
11
+ StoryboardData,
12
12
  } from './types';
13
13
 
14
14
  export interface ShortKitContextValue {
15
15
  // Player state
16
16
  playerState: PlayerState;
17
17
  currentItem: ContentItem | null;
18
- nextItem: ContentItem | null;
19
18
  time: PlayerTime;
20
19
  isMuted: boolean;
21
20
  playbackRate: number;
@@ -23,11 +22,8 @@ export interface ShortKitContextValue {
23
22
  activeCaptionTrack: CaptionTrack | null;
24
23
  activeCue: { text: string; startTime: number; endTime: number } | null;
25
24
  prefetchedAheadCount: number;
26
- remainingContentCount: number;
27
25
  isActive: boolean;
28
- isTransitioning: boolean;
29
- lastOverlayTap: number;
30
- lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
26
+ feedScrollPhase: FeedScrollPhase | null;
31
27
 
32
28
  // Player commands
33
29
  play: () => void;
@@ -46,24 +42,14 @@ export interface ShortKitContextValue {
46
42
  // SDK operations
47
43
  setUserId: (id: string) => void;
48
44
  clearUserId: () => void;
49
- setFeedItems: (items: CustomFeedItem[]) => void;
50
- appendFeedItems: (items: CustomFeedItem[]) => void;
51
- fetchContent: (limit?: number) => Promise<ContentItem[]>;
45
+ fetchContent: (limit?: number, filter?: FeedFilter) => Promise<ContentItem[]>;
52
46
 
53
- // Carousel overlay state
54
- currentCarouselItem: ImageCarouselItem | null;
55
- nextCarouselItem: ImageCarouselItem | null;
56
- isCarouselActive: boolean;
57
- isCarouselTransitioning: boolean;
47
+ // Preload
48
+ preloadFeed: (config?: Partial<FeedConfig>) => Promise<string>;
58
49
 
59
- // Active cell type — used by overlay managers to show/hide
60
- activeCellType: 'video' | 'carousel' | null;
61
-
62
- // Internal — used by ShortKitFeed to render custom overlays
63
- /** @internal */
64
- _overlayConfig: OverlayConfig;
65
- /** @internal */
66
- _carouselOverlayConfig: CarouselOverlayConfig;
50
+ // Storyboard / seek thumbnails
51
+ prefetchStoryboard: (playbackId: string) => void;
52
+ getStoryboardData: (playbackId: string) => Promise<StoryboardData | null>;
67
53
  }
68
54
 
69
55
  export const ShortKitContext = createContext<ShortKitContextValue | null>(null);
@@ -1,36 +1,46 @@
1
- import React, { useContext, useEffect } from 'react';
1
+ import React, { useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, forwardRef } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
- import type { ShortKitFeedProps } from './types';
3
+ import type { ShortKitFeedProps, FeedInput, FeedFilter, ShortKitFeedHandle } from './types';
4
4
  import ShortKitFeedView from './specs/ShortKitFeedViewNativeComponent';
5
5
  import NativeShortKitModule from './specs/NativeShortKitModule';
6
- import { OverlayManager } from './OverlayManager';
7
- import { CarouselOverlayManager } from './CarouselOverlayManager';
8
6
  import { ShortKitContext } from './ShortKitContext';
9
- import { deserializeContentItem } from './serialization';
7
+ import { deserializeContentItem, serializeFeedConfig, serializeFeedInputs } from './serialization';
8
+ import { registerOverlayComponent } from './ShortKitOverlaySurface';
9
+ import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
10
+
11
+ function generateFeedId(): string {
12
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
13
+ const r = (Math.random() * 16) | 0;
14
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
15
+ return v.toString(16);
16
+ });
17
+ }
10
18
 
11
19
  /**
12
20
  * Renders the native ShortKit video feed and forwards feed-level events to
13
21
  * callback props.
14
22
  *
15
- * Must be rendered inside a `<ShortKitProvider>`. The native feed view
16
- * receives its configuration from the `NativeShortKitModule.initialize()` call
17
- * made by the provider, so the view props here are placeholders required by
18
- * the Fabric codegen contract.
19
- *
20
- * When the overlay config is of type `custom`, the `OverlayManager` is
21
- * rendered alongside the native view to display the developer's React overlay
22
- * component.
23
+ * Must be rendered inside a `<ShortKitProvider>`. Feed configuration is passed
24
+ * via the `config` prop. Custom overlay components are registered with
25
+ * AppRegistry via the named overlay registry and mounted directly by native
26
+ * per-cell RCTFabricSurface instances.
23
27
  */
24
- export function ShortKitFeed(props: ShortKitFeedProps) {
28
+ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
29
+ function ShortKitFeed(props, ref) {
25
30
  const {
31
+ config,
32
+ preloadId,
26
33
  style,
27
- onError,
28
- onShareTapped,
34
+ startAtItemId,
29
35
  onSurveyResponse,
30
36
  onLoop,
31
37
  onFeedTransition,
32
38
  onFormatChange,
33
39
  onContentTapped,
40
+ onDismiss,
41
+ onRefreshRequested,
42
+ onDidFetchContentItems,
43
+ onRemainingContentCountChange,
34
44
  } = props;
35
45
 
36
46
  const context = useContext(ShortKitContext);
@@ -38,8 +48,53 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
38
48
  throw new Error('ShortKitFeed must be used within a ShortKitProvider');
39
49
  }
40
50
 
41
- const overlayConfig = context._overlayConfig;
42
- const carouselOverlayConfig = context._carouselOverlayConfig;
51
+ // Stable feed instance ID — survives re-renders and React fast refresh
52
+ const feedIdRef = useRef(generateFeedId());
53
+ const feedId = feedIdRef.current;
54
+
55
+ // Expose per-feed imperative methods on the ref
56
+ useImperativeHandle(ref, () => ({
57
+ setFeedItems: (items: FeedInput[]) => {
58
+ NativeShortKitModule?.setFeedItems(feedId, serializeFeedInputs(items));
59
+ },
60
+ appendFeedItems: (items: FeedInput[]) => {
61
+ NativeShortKitModule?.appendFeedItems(feedId, serializeFeedInputs(items));
62
+ },
63
+ applyFilter: (filter: FeedFilter | null) => {
64
+ NativeShortKitModule?.applyFilter(feedId, filter ? JSON.stringify(filter) : null);
65
+ },
66
+ }), [feedId]);
67
+
68
+ // Subscribe to per-feed remaining content count events
69
+ useEffect(() => {
70
+ if (!NativeShortKitModule || !onRemainingContentCountChange) return;
71
+
72
+ const subscription = NativeShortKitModule.onRemainingContentCountChanged((event) => {
73
+ // Match by feedId, or accept empty feedId as fallback (Android global routing)
74
+ if (event.feedId === feedId || event.feedId === '') {
75
+ onRemainingContentCountChange(event.count);
76
+ }
77
+ });
78
+
79
+ return () => subscription.remove();
80
+ }, [feedId, onRemainingContentCountChange]);
81
+
82
+ // Register overlay components before native view mounts.
83
+ // useLayoutEffect fires after commit but before paint — before
84
+ // didMoveToWindow/onAttachedToWindow on the native view.
85
+ useLayoutEffect(() => {
86
+ if (config?.overlay && config.overlay !== 'none') {
87
+ registerOverlayComponent(config.overlay.name, config.overlay.component);
88
+ }
89
+ if (config?.carouselOverlay && config.carouselOverlay !== 'none') {
90
+ registerCarouselOverlayComponent(config.carouselOverlay.name, config.carouselOverlay.component);
91
+ }
92
+ }, [config?.overlay, config?.carouselOverlay]);
93
+
94
+ const serializedConfig = useMemo(
95
+ () => (config ? serializeFeedConfig(config) : '{}'),
96
+ [config],
97
+ );
43
98
 
44
99
  // ---------------------------------------------------------------------------
45
100
  // Subscribe to feed-level events and forward to callback props
@@ -49,23 +104,6 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
49
104
 
50
105
  const subscriptions: Array<{ remove: () => void }> = [];
51
106
 
52
- if (onError) {
53
- subscriptions.push(
54
- NativeShortKitModule.onError((event) => {
55
- onError({ code: event.code, message: event.message });
56
- }),
57
- );
58
- }
59
-
60
- if (onShareTapped) {
61
- subscriptions.push(
62
- NativeShortKitModule.onShareTapped((event) => {
63
- const item = deserializeContentItem(event.item);
64
- if (item) onShareTapped(item);
65
- }),
66
- );
67
- }
68
-
69
107
  if (onSurveyResponse) {
70
108
  subscriptions.push(
71
109
  NativeShortKitModule.onSurveyResponse((event) => {
@@ -120,19 +158,49 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
120
158
  );
121
159
  }
122
160
 
161
+ if (onDismiss) {
162
+ subscriptions.push(
163
+ NativeShortKitModule.onDismiss(() => {
164
+ onDismiss();
165
+ }),
166
+ );
167
+ }
168
+
169
+ if (onRefreshRequested) {
170
+ subscriptions.push(
171
+ NativeShortKitModule.onRefreshRequested(() => {
172
+ onRefreshRequested();
173
+ }),
174
+ );
175
+ }
176
+
177
+ if (onDidFetchContentItems) {
178
+ subscriptions.push(
179
+ NativeShortKitModule.onDidFetchContentItems((event) => {
180
+ try {
181
+ const items = JSON.parse(event.items);
182
+ onDidFetchContentItems(items);
183
+ } catch {
184
+ // ignore malformed JSON
185
+ }
186
+ }),
187
+ );
188
+ }
189
+
123
190
  return () => {
124
191
  for (const sub of subscriptions) {
125
192
  sub.remove();
126
193
  }
127
194
  };
128
195
  }, [
129
- onError,
130
- onShareTapped,
131
196
  onSurveyResponse,
132
197
  onLoop,
133
198
  onFeedTransition,
134
199
  onFormatChange,
135
200
  onContentTapped,
201
+ onDismiss,
202
+ onRefreshRequested,
203
+ onDidFetchContentItems,
136
204
  ]);
137
205
 
138
206
  // ---------------------------------------------------------------------------
@@ -142,13 +210,14 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
142
210
  <View style={[feedStyles.container, style]}>
143
211
  <ShortKitFeedView
144
212
  style={feedStyles.feed}
145
- config="{}"
213
+ config={serializedConfig}
214
+ feedId={feedId}
215
+ startAtItemId={startAtItemId}
216
+ preloadId={preloadId}
146
217
  />
147
- <OverlayManager overlay={overlayConfig} />
148
- <CarouselOverlayManager carouselOverlay={carouselOverlayConfig} />
149
218
  </View>
150
219
  );
151
- }
220
+ });
152
221
 
153
222
  const feedStyles = StyleSheet.create({
154
223
  container: {
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { AppRegistry, View, StyleSheet } from 'react-native';
3
+
4
+ const SURFACE_NAME = 'ShortKitLoading';
5
+ let registered = false;
6
+
7
+ export function registerLoadingComponent(
8
+ Component: React.ComponentType<{}>,
9
+ ): void {
10
+ if (registered) return;
11
+ registered = true;
12
+
13
+ const LoadingRoot = () => (
14
+ <View style={styles.container}>
15
+ <Component />
16
+ </View>
17
+ );
18
+
19
+ AppRegistry.registerComponent(SURFACE_NAME, () => LoadingRoot);
20
+ }
21
+
22
+ const styles = StyleSheet.create({
23
+ container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
24
+ });
@@ -0,0 +1,205 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { AppRegistry, DeviceEventEmitter, Platform } from 'react-native';
3
+ import type { OverlayProps, PlayerState, PlayerTime, FeedScrollPhase } from './types';
4
+ import { deserializeContentItem } from './serialization';
5
+ import NativeShortKitModule from './specs/NativeShortKitModule';
6
+
7
+ // Named registry — supports different overlay components per feed
8
+ const _overlayRegistry = new Map<string, React.ComponentType<OverlayProps>>();
9
+
10
+ /**
11
+ * Register a named overlay component for rendering inside feed cells.
12
+ * Called by ShortKitFeed on mount via useLayoutEffect.
13
+ * Idempotent — registering the same name twice is a no-op.
14
+ */
15
+ export function registerOverlayComponent(
16
+ name: string,
17
+ component: React.ComponentType<OverlayProps>,
18
+ ) {
19
+ if (_overlayRegistry.has(name)) return;
20
+ _overlayRegistry.set(name, component);
21
+
22
+ const moduleName = `ShortKitOverlay_${name}`;
23
+ AppRegistry.registerComponent(moduleName, () => {
24
+ return function NamedOverlaySurface(props: RawOverlaySurfaceProps) {
25
+ return <ShortKitOverlaySurfaceInner {...props} overlayName={name} />;
26
+ };
27
+ });
28
+ }
29
+
30
+ /** Raw props received from native appProperties (set once per item in configure()). */
31
+ interface RawOverlaySurfaceProps {
32
+ surfaceId?: string;
33
+ item?: string;
34
+ isActive?: boolean;
35
+ playerState?: string;
36
+ isMuted?: boolean;
37
+ playbackRate?: number;
38
+ captionsEnabled?: boolean;
39
+ activeCue?: string;
40
+ feedScrollPhase?: string;
41
+ }
42
+
43
+ interface InnerProps extends RawOverlaySurfaceProps {
44
+ overlayName: string;
45
+ }
46
+
47
+ /**
48
+ * Subscribe to a native overlay event, filtered by surfaceId.
49
+ *
50
+ * iOS and Android new arch emit events through different paths:
51
+ * - iOS: _eventEmitterCallback (codegen path) — only the codegen EventEmitter
52
+ * subscriptions (NativeShortKitModule.onXxx) receive these events.
53
+ * - Android: getJSModule(RCTDeviceEventEmitter).emit() — only
54
+ * DeviceEventEmitter.addListener() receives these events (codegen
55
+ * EventEmitter subscriptions do NOT receive them on Android new arch).
56
+ *
57
+ * We branch per platform to use the path that actually delivers events.
58
+ */
59
+ function useOverlayEvent<T extends { surfaceId: string }>(
60
+ eventName: string,
61
+ surfaceId: string | undefined,
62
+ handler: (event: T) => void,
63
+ ) {
64
+ useEffect(() => {
65
+ if (!surfaceId) return;
66
+
67
+ let sub: { remove: () => void } | undefined;
68
+
69
+ if (Platform.OS === 'ios') {
70
+ // iOS new arch: events come through the codegen EventEmitterCallback.
71
+ // Access the codegen emitter method by name on the TurboModule.
72
+ const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
73
+ if (typeof emitter === 'function') {
74
+ sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
75
+ if (e.surfaceId !== surfaceId) return;
76
+ handler(e);
77
+ });
78
+ }
79
+ } else {
80
+ // Android new arch: events come through RCTDeviceEventEmitter.emit().
81
+ sub = DeviceEventEmitter.addListener(eventName, (e: T) => {
82
+ if (e.surfaceId !== surfaceId) return;
83
+ handler(e);
84
+ });
85
+ }
86
+
87
+ return () => sub?.remove();
88
+ }, [surfaceId]);
89
+ }
90
+
91
+ function ShortKitOverlaySurfaceInner(props: InnerProps) {
92
+ const Component = _overlayRegistry.get(props.overlayName);
93
+ if (!Component) return null;
94
+
95
+ const item = useMemo(
96
+ () => (props.item ? deserializeContentItem(props.item) : null),
97
+ [props.item],
98
+ );
99
+
100
+ const sid = props.surfaceId;
101
+
102
+ // Initialize state from surface properties (set once in configure()).
103
+ // All subsequent updates arrive via events filtered by surfaceId.
104
+ const [isActive, setIsActive] = useState(props.isActive ?? false);
105
+ const [playerState, setPlayerState] = useState<PlayerState>(
106
+ (props.playerState ?? 'idle') as PlayerState,
107
+ );
108
+ const [isMuted, setIsMuted] = useState(props.isMuted ?? true);
109
+ const [playbackRate, setPlaybackRate] = useState(props.playbackRate ?? 1);
110
+ const [captionsEnabled, setCaptionsEnabled] = useState(
111
+ props.captionsEnabled ?? false,
112
+ );
113
+ const [time, setTime] = useState<PlayerTime>({
114
+ current: 0,
115
+ duration: 0,
116
+ buffered: 0,
117
+ });
118
+ const [activeCue, setActiveCue] = useState<OverlayProps['activeCue']>(
119
+ props.activeCue ? JSON.parse(props.activeCue) : null,
120
+ );
121
+ const [feedScrollPhase, setFeedScrollPhase] =
122
+ useState<FeedScrollPhase | null>(
123
+ props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
124
+ );
125
+
126
+ // Reset state when the item changes (cell reuse). useState initial values
127
+ // only apply on mount, so on re-render with new props we must sync manually.
128
+ // This prevents stale progress bar / player state flashing from the previous cell.
129
+ useEffect(() => {
130
+ setIsActive(props.isActive ?? false);
131
+ setPlayerState((props.playerState ?? 'idle') as PlayerState);
132
+ setTime({ current: 0, duration: 0, buffered: 0 });
133
+ setIsMuted(props.isMuted ?? true);
134
+ setPlaybackRate(props.playbackRate ?? 1);
135
+ setCaptionsEnabled(props.captionsEnabled ?? false);
136
+ setActiveCue(props.activeCue ? JSON.parse(props.activeCue) : null);
137
+ setFeedScrollPhase(
138
+ props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
139
+ );
140
+ }, [props.item]);
141
+
142
+ // --- Event subscriptions (filtered by surfaceId) ---
143
+
144
+ useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
145
+ 'onOverlayActiveChanged', sid,
146
+ (e) => setIsActive(e.isActive),
147
+ );
148
+
149
+ useOverlayEvent<{ surfaceId: string; playerState: string }>(
150
+ 'onOverlayPlayerStateChanged', sid,
151
+ (e) => setPlayerState(e.playerState as PlayerState),
152
+ );
153
+
154
+ useOverlayEvent<{ surfaceId: string; isMuted: boolean }>(
155
+ 'onOverlayMutedChanged', sid,
156
+ (e) => setIsMuted(e.isMuted),
157
+ );
158
+
159
+ useOverlayEvent<{ surfaceId: string; playbackRate: number }>(
160
+ 'onOverlayPlaybackRateChanged', sid,
161
+ (e) => setPlaybackRate(e.playbackRate),
162
+ );
163
+
164
+ useOverlayEvent<{ surfaceId: string; captionsEnabled: boolean }>(
165
+ 'onOverlayCaptionsEnabledChanged', sid,
166
+ (e) => setCaptionsEnabled(e.captionsEnabled),
167
+ );
168
+
169
+ useOverlayEvent<{ surfaceId: string; activeCue: string | null }>(
170
+ 'onOverlayActiveCueChanged', sid,
171
+ (e) => setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null),
172
+ );
173
+
174
+ useOverlayEvent<{ surfaceId: string; feedScrollPhase: string | null }>(
175
+ 'onOverlayFeedScrollPhaseChanged', sid,
176
+ (e) => {
177
+ try {
178
+ setFeedScrollPhase(e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null);
179
+ } catch {
180
+ setFeedScrollPhase(null);
181
+ }
182
+ },
183
+ );
184
+
185
+ useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
186
+ 'onOverlayTimeUpdate', sid,
187
+ (e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
188
+ );
189
+
190
+ if (!item) return null;
191
+
192
+ return (
193
+ <Component
194
+ item={item}
195
+ isActive={isActive}
196
+ playerState={playerState}
197
+ time={time}
198
+ isMuted={isMuted}
199
+ playbackRate={playbackRate}
200
+ captionsEnabled={captionsEnabled}
201
+ activeCue={activeCue}
202
+ feedScrollPhase={feedScrollPhase}
203
+ />
204
+ );
205
+ }
@@ -1,23 +1,22 @@
1
- import React, { useContext, useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
3
  import type { ShortKitPlayerProps } from './types';
4
4
  import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
5
- import { ShortKitContext } from './ShortKitContext';
6
5
 
7
6
  /**
8
7
  * Single-video player component. Displays one video with thumbnail fallback
9
8
  * and optional overlay. Wraps a native Fabric view.
10
9
  *
11
10
  * Must be rendered inside a `<ShortKitProvider>`.
11
+ *
12
+ * NOTE: Removed useContext(ShortKitContext) subscription — it was only used
13
+ * for an existence check but caused re-renders on every TIME update (~4x/sec)
14
+ * from the feed, flickering all grid tiles. The provider check is the host
15
+ * app's responsibility.
12
16
  */
13
17
  export function ShortKitPlayer(props: ShortKitPlayerProps) {
14
18
  const { config, contentItem, active, style } = props;
15
19
 
16
- const context = useContext(ShortKitContext);
17
- if (!context) {
18
- throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
19
- }
20
-
21
20
  const serializedConfig = useMemo(() => {
22
21
  const cfg = config ?? {};
23
22
  return JSON.stringify({