@shortkitsdk/react-native 0.2.11 → 0.2.14
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/android/build.gradle.kts +13 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +157 -54
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +67 -56
- package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +154 -26
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +160 -35
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +5 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +45 -10
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +9 -0
- package/ios/ReactCarouselOverlayHost.swift +37 -17
- package/ios/ReactOverlayHost.swift +33 -35
- package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
- package/ios/ShortKitBridge.swift +78 -2
- package/ios/ShortKitFeedView.swift +24 -3
- package/ios/ShortKitModule.mm +6 -2
- package/ios/ShortKitSDK.xcframework/Info.plist +4 -4
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework.bak2/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
- package/src/ShortKitContext.ts +2 -1
- package/src/ShortKitFeed.tsx +19 -1
- package/src/ShortKitOverlaySurface.tsx +148 -41
- package/src/ShortKitPlayer.tsx +25 -3
- package/src/ShortKitProvider.tsx +4 -2
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
- package/src/index.ts +8 -1
- package/src/serialization.ts +8 -0
- package/src/specs/NativeShortKitModule.ts +31 -1
- package/src/types.ts +45 -1
package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { AppRegistry } from 'react-native';
|
|
3
3
|
import type { CarouselOverlayProps, ImageCarouselItem } from './types';
|
|
4
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
4
5
|
|
|
5
6
|
const _carouselRegistry = new Map<string, React.ComponentType<CarouselOverlayProps>>();
|
|
6
7
|
|
|
@@ -24,7 +25,30 @@ export function registerCarouselOverlayComponent(
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** Subscribe to a named native event, filtered by surfaceId. */
|
|
29
|
+
function useOverlayEvent<T extends { surfaceId: string }>(
|
|
30
|
+
eventName: string,
|
|
31
|
+
surfaceId: string | undefined,
|
|
32
|
+
handler: (event: T) => void,
|
|
33
|
+
) {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!surfaceId) return;
|
|
36
|
+
|
|
37
|
+
let sub: { remove: () => void } | undefined;
|
|
38
|
+
const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
|
|
39
|
+
if (typeof emitter === 'function') {
|
|
40
|
+
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
41
|
+
if (e.surfaceId !== surfaceId) return;
|
|
42
|
+
handler(e);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return () => sub?.remove();
|
|
47
|
+
}, [surfaceId]);
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
interface RawCarouselSurfaceProps {
|
|
51
|
+
surfaceId?: string;
|
|
28
52
|
item?: string;
|
|
29
53
|
}
|
|
30
54
|
|
|
@@ -38,6 +62,31 @@ function CarouselSurfaceInner(props: InnerProps) {
|
|
|
38
62
|
return null;
|
|
39
63
|
}
|
|
40
64
|
|
|
65
|
+
const sid = props.surfaceId;
|
|
66
|
+
|
|
67
|
+
const [isActive, setIsActive] = useState(false);
|
|
68
|
+
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
|
69
|
+
|
|
70
|
+
// isActive delivered via onOverlayFullState (same event as video overlay)
|
|
71
|
+
useOverlayEvent<{
|
|
72
|
+
surfaceId: string;
|
|
73
|
+
isActive: boolean;
|
|
74
|
+
playerState: string;
|
|
75
|
+
isMuted: boolean;
|
|
76
|
+
playbackRate: number;
|
|
77
|
+
captionsEnabled: boolean;
|
|
78
|
+
activeCue: string | null;
|
|
79
|
+
feedScrollPhase: string | null;
|
|
80
|
+
}>('onOverlayFullState', sid, (e) => {
|
|
81
|
+
setIsActive(e.isActive);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// activeImageIndex delivered via dedicated event
|
|
85
|
+
useOverlayEvent<{ surfaceId: string; activeImageIndex: number }>(
|
|
86
|
+
'onCarouselActiveImageChanged', sid,
|
|
87
|
+
(e) => setActiveImageIndex(e.activeImageIndex),
|
|
88
|
+
);
|
|
89
|
+
|
|
41
90
|
const item: ImageCarouselItem | null = useMemo(() => {
|
|
42
91
|
if (!props.item) {
|
|
43
92
|
return null;
|
|
@@ -51,5 +100,11 @@ function CarouselSurfaceInner(props: InnerProps) {
|
|
|
51
100
|
|
|
52
101
|
if (!item) return null;
|
|
53
102
|
|
|
54
|
-
return
|
|
103
|
+
return (
|
|
104
|
+
<Component
|
|
105
|
+
item={item}
|
|
106
|
+
isActive={isActive}
|
|
107
|
+
activeImageIndex={activeImageIndex}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
55
110
|
}
|
package/src/ShortKitContext.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ContentItem,
|
|
4
4
|
FeedConfig,
|
|
5
5
|
FeedFilter,
|
|
6
|
+
FeedInput,
|
|
6
7
|
PlayerTime,
|
|
7
8
|
PlayerState,
|
|
8
9
|
CaptionTrack,
|
|
@@ -45,7 +46,7 @@ export interface ShortKitContextValue {
|
|
|
45
46
|
fetchContent: (limit?: number, filter?: FeedFilter) => Promise<ContentItem[]>;
|
|
46
47
|
|
|
47
48
|
// Preload
|
|
48
|
-
preloadFeed: (config?: Partial<FeedConfig
|
|
49
|
+
preloadFeed: (config?: Partial<FeedConfig>, items?: FeedInput[]) => Promise<string>;
|
|
49
50
|
|
|
50
51
|
// Storyboard / seek thumbnails
|
|
51
52
|
prefetchStoryboard: (playbackId: string) => void;
|
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { ShortKitContext } from './ShortKitContext';
|
|
|
7
7
|
import { deserializeContentItem, serializeFeedConfig, serializeFeedInputs } from './serialization';
|
|
8
8
|
import { registerOverlayComponent } from './ShortKitOverlaySurface';
|
|
9
9
|
import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
|
|
10
|
+
import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
|
|
10
11
|
|
|
11
12
|
function generateFeedId(): string {
|
|
12
13
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
@@ -41,6 +42,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
41
42
|
onRefreshRequested,
|
|
42
43
|
onDidFetchContentItems,
|
|
43
44
|
onRemainingContentCountChange,
|
|
45
|
+
onFeedReady,
|
|
44
46
|
} = props;
|
|
45
47
|
|
|
46
48
|
const context = useContext(ShortKitContext);
|
|
@@ -79,6 +81,19 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
79
81
|
return () => subscription.remove();
|
|
80
82
|
}, [feedId, onRemainingContentCountChange]);
|
|
81
83
|
|
|
84
|
+
// Subscribe to per-feed ready event
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!NativeShortKitModule || !onFeedReady) return;
|
|
87
|
+
|
|
88
|
+
const subscription = NativeShortKitModule.onFeedReady((event) => {
|
|
89
|
+
if (event.feedId === feedId) {
|
|
90
|
+
onFeedReady();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return () => subscription.remove();
|
|
95
|
+
}, [feedId, onFeedReady]);
|
|
96
|
+
|
|
82
97
|
// Register overlay components before native view mounts.
|
|
83
98
|
// useLayoutEffect fires after commit but before paint — before
|
|
84
99
|
// didMoveToWindow/onAttachedToWindow on the native view.
|
|
@@ -89,7 +104,10 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
89
104
|
if (config?.carouselOverlay && config.carouselOverlay !== 'none') {
|
|
90
105
|
registerCarouselOverlayComponent(config.carouselOverlay.name, config.carouselOverlay.component);
|
|
91
106
|
}
|
|
92
|
-
|
|
107
|
+
if (config?.videoCarouselOverlay && config.videoCarouselOverlay !== 'none') {
|
|
108
|
+
registerVideoCarouselOverlayComponent(config.videoCarouselOverlay.name, config.videoCarouselOverlay.component);
|
|
109
|
+
}
|
|
110
|
+
}, [config?.overlay, config?.carouselOverlay, config?.videoCarouselOverlay]);
|
|
93
111
|
|
|
94
112
|
const serializedConfig = useMemo(
|
|
95
113
|
() => (config ? serializeFeedConfig(config) : '{}'),
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
2
|
-
import { AppRegistry,
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { AppRegistry, View, Text } from 'react-native';
|
|
3
3
|
import type { OverlayProps, PlayerState, PlayerTime, FeedScrollPhase } from './types';
|
|
4
4
|
import { deserializeContentItem } from './serialization';
|
|
5
5
|
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
6
6
|
|
|
7
|
+
const SK_OVERLAY_TAG = '[ShortKit:OverlaySurface]';
|
|
8
|
+
|
|
7
9
|
// Named registry — supports different overlay components per feed
|
|
8
10
|
const _overlayRegistry = new Map<string, React.ComponentType<OverlayProps>>();
|
|
9
11
|
|
|
@@ -16,7 +18,9 @@ export function registerOverlayComponent(
|
|
|
16
18
|
name: string,
|
|
17
19
|
component: React.ComponentType<OverlayProps>,
|
|
18
20
|
) {
|
|
19
|
-
if (_overlayRegistry.has(name))
|
|
21
|
+
if (_overlayRegistry.has(name)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
20
24
|
_overlayRegistry.set(name, component);
|
|
21
25
|
|
|
22
26
|
const moduleName = `ShortKitOverlay_${name}`;
|
|
@@ -27,6 +31,47 @@ export function registerOverlayComponent(
|
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Error boundary that catches crashes inside the overlay component.
|
|
36
|
+
* Without this, a crash in an isolated React surface is silent on Android.
|
|
37
|
+
*/
|
|
38
|
+
class OverlayErrorBoundary extends React.Component<
|
|
39
|
+
{ surfaceId?: string; overlayName: string; children: React.ReactNode },
|
|
40
|
+
{ error: Error | null }
|
|
41
|
+
> {
|
|
42
|
+
state: { error: Error | null } = { error: null };
|
|
43
|
+
|
|
44
|
+
static getDerivedStateFromError(error: Error) {
|
|
45
|
+
return { error };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
49
|
+
console.error(
|
|
50
|
+
`${SK_OVERLAY_TAG} CRASH in overlay '${this.props.overlayName}' (surfaceId=${this.props.surfaceId}):`,
|
|
51
|
+
error.message,
|
|
52
|
+
'\nComponent stack:',
|
|
53
|
+
info.componentStack,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
if (this.state.error) {
|
|
59
|
+
// Render a visible red indicator in __DEV__ so it's obvious
|
|
60
|
+
if (__DEV__) {
|
|
61
|
+
return (
|
|
62
|
+
<View style={{ flex: 1, backgroundColor: 'rgba(255,0,0,0.3)', justifyContent: 'center', alignItems: 'center', padding: 20 }}>
|
|
63
|
+
<Text style={{ color: 'white', fontSize: 14, textAlign: 'center' }}>
|
|
64
|
+
{`Overlay '${this.props.overlayName}' crashed:\n${this.state.error.message}`}
|
|
65
|
+
</Text>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return this.props.children;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
/** Raw props received from native appProperties (set once per item in configure()). */
|
|
31
76
|
interface RawOverlaySurfaceProps {
|
|
32
77
|
surfaceId?: string;
|
|
@@ -47,14 +92,9 @@ interface InnerProps extends RawOverlaySurfaceProps {
|
|
|
47
92
|
/**
|
|
48
93
|
* Subscribe to a native overlay event, filtered by surfaceId.
|
|
49
94
|
*
|
|
50
|
-
* iOS and Android new arch emit events through
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
95
|
+
* Both iOS and Android (new arch) emit events through the codegen
|
|
96
|
+
* EventEmitter path (mEventEmitterCallback / JSI direct channel).
|
|
97
|
+
* We subscribe via the codegen emitter method on the TurboModule.
|
|
58
98
|
*/
|
|
59
99
|
function useOverlayEvent<T extends { surfaceId: string }>(
|
|
60
100
|
eventName: string,
|
|
@@ -66,19 +106,11 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
66
106
|
|
|
67
107
|
let sub: { remove: () => void } | undefined;
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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) => {
|
|
109
|
+
// Both platforms: events come through the codegen EventEmitterCallback.
|
|
110
|
+
// Access the codegen emitter method by name on the TurboModule.
|
|
111
|
+
const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
|
|
112
|
+
if (typeof emitter === 'function') {
|
|
113
|
+
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
82
114
|
if (e.surfaceId !== surfaceId) return;
|
|
83
115
|
handler(e);
|
|
84
116
|
});
|
|
@@ -90,13 +122,24 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
90
122
|
|
|
91
123
|
function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
92
124
|
const Component = _overlayRegistry.get(props.overlayName);
|
|
93
|
-
if (!Component)
|
|
125
|
+
if (!Component) {
|
|
126
|
+
console.error(`${SK_OVERLAY_TAG} ShortKitOverlaySurfaceInner: Component NOT FOUND in registry for '${props.overlayName}'. Registered names: [${Array.from(_overlayRegistry.keys()).join(', ')}]`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
94
129
|
|
|
95
|
-
const item =
|
|
96
|
-
|
|
97
|
-
[props.item],
|
|
130
|
+
const [item, setItem] = useState(() =>
|
|
131
|
+
props.item ? deserializeContentItem(props.item) : null,
|
|
98
132
|
);
|
|
99
133
|
|
|
134
|
+
// Sync item from props when surface is first mounted or remounted via updateInitProps
|
|
135
|
+
const prevPropsItemRef = React.useRef(props.item);
|
|
136
|
+
if (props.item !== prevPropsItemRef.current) {
|
|
137
|
+
prevPropsItemRef.current = props.item;
|
|
138
|
+
// Synchronous state update during render — React restarts with new state
|
|
139
|
+
// before committing, so stale frame is never painted.
|
|
140
|
+
setItem(props.item ? deserializeContentItem(props.item) : null);
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
const sid = props.surfaceId;
|
|
101
144
|
|
|
102
145
|
// Initialize state from surface properties (set once in configure()).
|
|
@@ -141,6 +184,24 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
141
184
|
|
|
142
185
|
// --- Event subscriptions (filtered by surfaceId) ---
|
|
143
186
|
|
|
187
|
+
// Item changed via event (not updateInitProps) — triggers React DIFF
|
|
188
|
+
// instead of full remount. Uses DeviceEventEmitter because this event
|
|
189
|
+
// is not in the TurboModule spec (avoids native codegen changes).
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!sid) return;
|
|
192
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
193
|
+
const sub = DeviceEventEmitter.addListener('onOverlayItemChanged', (e: { surfaceId: string; item: string }) => {
|
|
194
|
+
if (e.surfaceId !== sid) return;
|
|
195
|
+
const newItem = e.item ? deserializeContentItem(e.item) : null;
|
|
196
|
+
if (newItem) setItem(newItem);
|
|
197
|
+
// Reset playback state for the new item
|
|
198
|
+
setIsActive(false);
|
|
199
|
+
setPlayerState('idle' as PlayerState);
|
|
200
|
+
setTime({ current: 0, duration: 0, buffered: 0 });
|
|
201
|
+
});
|
|
202
|
+
return () => sub.remove();
|
|
203
|
+
}, [sid]);
|
|
204
|
+
|
|
144
205
|
useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
|
|
145
206
|
'onOverlayActiveChanged', sid,
|
|
146
207
|
(e) => setIsActive(e.isActive),
|
|
@@ -175,7 +236,16 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
175
236
|
'onOverlayFeedScrollPhaseChanged', sid,
|
|
176
237
|
(e) => {
|
|
177
238
|
try {
|
|
178
|
-
|
|
239
|
+
const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
|
|
240
|
+
// Stabilize object reference: only update state if the phase actually
|
|
241
|
+
// changed. Prevents unnecessary re-renders of memo'd overlay components
|
|
242
|
+
// that receive feedScrollPhase as a prop (new JSON.parse object !== old).
|
|
243
|
+
setFeedScrollPhase(prev => {
|
|
244
|
+
if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) {
|
|
245
|
+
return prev;
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
});
|
|
179
249
|
} catch {
|
|
180
250
|
setFeedScrollPhase(null);
|
|
181
251
|
}
|
|
@@ -187,19 +257,56 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
187
257
|
(e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
|
|
188
258
|
);
|
|
189
259
|
|
|
190
|
-
|
|
260
|
+
// Batched full-state sync — replaces 7 individual events on swipe settle.
|
|
261
|
+
// All setState calls within one handler = one React render (auto-batched).
|
|
262
|
+
useOverlayEvent<{
|
|
263
|
+
surfaceId: string;
|
|
264
|
+
isActive: boolean;
|
|
265
|
+
playerState: string;
|
|
266
|
+
isMuted: boolean;
|
|
267
|
+
playbackRate: number;
|
|
268
|
+
captionsEnabled: boolean;
|
|
269
|
+
activeCue: string | null;
|
|
270
|
+
feedScrollPhase: string | null;
|
|
271
|
+
}>('onOverlayFullState', sid, (e) => {
|
|
272
|
+
setIsActive(e.isActive);
|
|
273
|
+
setPlayerState(e.playerState as PlayerState);
|
|
274
|
+
setIsMuted(e.isMuted);
|
|
275
|
+
setPlaybackRate(e.playbackRate);
|
|
276
|
+
setCaptionsEnabled(e.captionsEnabled);
|
|
277
|
+
try {
|
|
278
|
+
setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null);
|
|
279
|
+
} catch {
|
|
280
|
+
setActiveCue(null);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
|
|
284
|
+
setFeedScrollPhase(prev => {
|
|
285
|
+
if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) return prev;
|
|
286
|
+
return next;
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
setFeedScrollPhase(null);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!item) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
191
296
|
|
|
192
297
|
return (
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
298
|
+
<OverlayErrorBoundary surfaceId={sid} overlayName={props.overlayName}>
|
|
299
|
+
<Component
|
|
300
|
+
item={item}
|
|
301
|
+
isActive={isActive}
|
|
302
|
+
playerState={playerState}
|
|
303
|
+
time={time}
|
|
304
|
+
isMuted={isMuted}
|
|
305
|
+
playbackRate={playbackRate}
|
|
306
|
+
captionsEnabled={captionsEnabled}
|
|
307
|
+
activeCue={activeCue}
|
|
308
|
+
feedScrollPhase={feedScrollPhase}
|
|
309
|
+
/>
|
|
310
|
+
</OverlayErrorBoundary>
|
|
204
311
|
);
|
|
205
312
|
}
|
package/src/ShortKitPlayer.tsx
CHANGED
|
@@ -17,11 +17,13 @@ import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
|
|
|
17
17
|
export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
18
18
|
const { config, contentItem, active, style } = props;
|
|
19
19
|
|
|
20
|
+
const clickAction = config?.clickAction ?? 'feed';
|
|
21
|
+
|
|
20
22
|
const serializedConfig = useMemo(() => {
|
|
21
23
|
const cfg = config ?? {};
|
|
22
24
|
return JSON.stringify({
|
|
23
25
|
cornerRadius: cfg.cornerRadius ?? 12,
|
|
24
|
-
clickAction:
|
|
26
|
+
clickAction: clickAction,
|
|
25
27
|
autoplay: cfg.autoplay ?? true,
|
|
26
28
|
loop: cfg.loop ?? true,
|
|
27
29
|
muteOnStart: cfg.muteOnStart ?? true,
|
|
@@ -31,20 +33,40 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
|
31
33
|
: { type: 'custom' }
|
|
32
34
|
: 'none',
|
|
33
35
|
});
|
|
34
|
-
}, [config]);
|
|
36
|
+
}, [config, clickAction]);
|
|
35
37
|
|
|
36
38
|
const serializedItem = useMemo(() => {
|
|
37
39
|
if (!contentItem) return undefined;
|
|
38
40
|
return JSON.stringify(contentItem);
|
|
39
41
|
}, [contentItem]);
|
|
40
42
|
|
|
43
|
+
// When clickAction is "none", the native view should not participate in
|
|
44
|
+
// Fabric's JS-side hit testing. Without this, Fabric on Android sees the
|
|
45
|
+
// native component as the touch target and never routes the touch to a
|
|
46
|
+
// parent Pressable — even though the native view returns false from
|
|
47
|
+
// dispatchTouchEvent. iOS doesn't need this because UIKit's responder
|
|
48
|
+
// chain bubbles unhandled touches automatically.
|
|
49
|
+
//
|
|
50
|
+
// "box-none" on the container means the container View itself is
|
|
51
|
+
// transparent to touches (passes through to parent), but children can
|
|
52
|
+
// still receive touches if needed. "none" on the native view means it
|
|
53
|
+
// is fully invisible to hit testing.
|
|
54
|
+
const nativeTouchPassthrough = clickAction === 'none';
|
|
55
|
+
|
|
41
56
|
return (
|
|
42
|
-
<View
|
|
57
|
+
<View
|
|
58
|
+
style={[styles.container, style]}
|
|
59
|
+
pointerEvents={nativeTouchPassthrough ? 'box-none' : 'auto'}
|
|
60
|
+
onStartShouldSetResponder={() => {
|
|
61
|
+
return false;
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
43
64
|
<ShortKitPlayerView
|
|
44
65
|
style={styles.player}
|
|
45
66
|
config={serializedConfig}
|
|
46
67
|
contentItem={serializedItem}
|
|
47
68
|
active={active}
|
|
69
|
+
pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
|
|
48
70
|
/>
|
|
49
71
|
</View>
|
|
50
72
|
);
|
package/src/ShortKitProvider.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ContentItem,
|
|
9
9
|
FeedConfig,
|
|
10
10
|
FeedFilter,
|
|
11
|
+
FeedInput,
|
|
11
12
|
PlayerTime,
|
|
12
13
|
PlayerState,
|
|
13
14
|
CaptionTrack,
|
|
@@ -420,10 +421,11 @@ export function ShortKitProvider({
|
|
|
420
421
|
}
|
|
421
422
|
}, []);
|
|
422
423
|
|
|
423
|
-
const preloadFeedCmd = useCallback(async (config?: Partial<FeedConfig
|
|
424
|
+
const preloadFeedCmd = useCallback(async (config?: Partial<FeedConfig>, items?: FeedInput[]): Promise<string> => {
|
|
424
425
|
if (!NativeShortKitModule) return '';
|
|
425
426
|
const configJSON = config ? serializeFeedConfig(config as FeedConfig) : '{}';
|
|
426
|
-
|
|
427
|
+
const itemsJSON = items ? JSON.stringify(items) : null;
|
|
428
|
+
return NativeShortKitModule.preloadFeed(configJSON, itemsJSON);
|
|
427
429
|
}, []);
|
|
428
430
|
|
|
429
431
|
// -----------------------------------------------------------------------
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { AppRegistry } from 'react-native';
|
|
3
|
+
import type { VideoCarouselOverlayProps, VideoCarouselItem, ContentItem, PlayerTime, PlayerState } from './types';
|
|
4
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
5
|
+
|
|
6
|
+
const _videoCarouselRegistry = new Map<string, React.ComponentType<VideoCarouselOverlayProps>>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register a named video carousel overlay component for rendering inside feed cells.
|
|
10
|
+
* Called by ShortKitFeed on mount via useLayoutEffect.
|
|
11
|
+
* Idempotent — registering the same name twice is a no-op.
|
|
12
|
+
*/
|
|
13
|
+
export function registerVideoCarouselOverlayComponent(
|
|
14
|
+
name: string,
|
|
15
|
+
component: React.ComponentType<VideoCarouselOverlayProps>,
|
|
16
|
+
) {
|
|
17
|
+
if (_videoCarouselRegistry.has(name)) return;
|
|
18
|
+
_videoCarouselRegistry.set(name, component);
|
|
19
|
+
|
|
20
|
+
const moduleName = `ShortKitVideoCarouselOverlay_${name}`;
|
|
21
|
+
AppRegistry.registerComponent(moduleName, () => {
|
|
22
|
+
return function NamedVideoCarouselSurface(props: RawProps) {
|
|
23
|
+
return <VideoCarouselSurfaceInner {...props} overlayName={name} />;
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Subscribe to a named native event, filtered by surfaceId. */
|
|
29
|
+
function useOverlayEvent<T extends { surfaceId: string }>(
|
|
30
|
+
eventName: string,
|
|
31
|
+
surfaceId: string | undefined,
|
|
32
|
+
handler: (event: T) => void,
|
|
33
|
+
) {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!surfaceId) return;
|
|
36
|
+
|
|
37
|
+
let sub: { remove: () => void } | undefined;
|
|
38
|
+
const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
|
|
39
|
+
if (typeof emitter === 'function') {
|
|
40
|
+
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
41
|
+
if (e.surfaceId !== surfaceId) return;
|
|
42
|
+
handler(e);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return () => sub?.remove();
|
|
47
|
+
}, [surfaceId]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface RawProps {
|
|
51
|
+
surfaceId?: string;
|
|
52
|
+
carouselItem?: string;
|
|
53
|
+
activeVideo?: string;
|
|
54
|
+
activeVideoIndex?: number;
|
|
55
|
+
isActive?: boolean;
|
|
56
|
+
playerState?: string;
|
|
57
|
+
isMuted?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface InnerProps extends RawProps {
|
|
61
|
+
overlayName: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function VideoCarouselSurfaceInner(props: InnerProps) {
|
|
65
|
+
const Component = _videoCarouselRegistry.get(props.overlayName);
|
|
66
|
+
if (!Component) return null;
|
|
67
|
+
|
|
68
|
+
const sid = props.surfaceId;
|
|
69
|
+
|
|
70
|
+
// Playback state — initialized from surface props, updated via events
|
|
71
|
+
const [isActive, setIsActive] = useState(props.isActive ?? false);
|
|
72
|
+
const [playerState, setPlayerState] = useState<PlayerState>((props.playerState as PlayerState) ?? 'idle');
|
|
73
|
+
const [isMuted, setIsMuted] = useState(props.isMuted ?? true);
|
|
74
|
+
const [time, setTime] = useState<PlayerTime>({ current: 0, duration: 0, buffered: 0 });
|
|
75
|
+
|
|
76
|
+
// Batched full-state sync
|
|
77
|
+
useOverlayEvent<{
|
|
78
|
+
surfaceId: string;
|
|
79
|
+
isActive: boolean;
|
|
80
|
+
playerState: string;
|
|
81
|
+
isMuted: boolean;
|
|
82
|
+
playbackRate: number;
|
|
83
|
+
captionsEnabled: boolean;
|
|
84
|
+
activeCue: string | null;
|
|
85
|
+
feedScrollPhase: string | null;
|
|
86
|
+
}>('onOverlayFullState', sid, (e) => {
|
|
87
|
+
setIsActive(e.isActive);
|
|
88
|
+
setPlayerState(e.playerState as PlayerState);
|
|
89
|
+
setIsMuted(e.isMuted);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Individual state updates
|
|
93
|
+
useOverlayEvent<{ surfaceId: string; playerState: string }>(
|
|
94
|
+
'onOverlayPlayerStateChanged', sid,
|
|
95
|
+
(e) => setPlayerState(e.playerState as PlayerState),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
useOverlayEvent<{ surfaceId: string; isMuted: boolean }>(
|
|
99
|
+
'onOverlayMutedChanged', sid,
|
|
100
|
+
(e) => setIsMuted(e.isMuted),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Time updates (250ms coalesced)
|
|
104
|
+
useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
|
|
105
|
+
'onOverlayTimeUpdate', sid,
|
|
106
|
+
(e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Carousel item — from surface props (set once in configure())
|
|
110
|
+
const carouselItem: VideoCarouselItem | null = useMemo(() => {
|
|
111
|
+
if (!props.carouselItem) return null;
|
|
112
|
+
try { return JSON.parse(props.carouselItem); } catch { return null; }
|
|
113
|
+
}, [props.carouselItem]);
|
|
114
|
+
|
|
115
|
+
// Active video — initialized from surface props, updated via events on swipe
|
|
116
|
+
const initialActiveVideo: ContentItem | null = useMemo(() => {
|
|
117
|
+
if (!props.activeVideo) return null;
|
|
118
|
+
try { return JSON.parse(props.activeVideo); } catch { return null; }
|
|
119
|
+
}, [props.activeVideo]);
|
|
120
|
+
|
|
121
|
+
const [activeVideo, setActiveVideo] = useState<ContentItem | null>(initialActiveVideo);
|
|
122
|
+
const [activeVideoIndex, setActiveVideoIndex] = useState(props.activeVideoIndex ?? 0);
|
|
123
|
+
|
|
124
|
+
// Sync from props when surface is reconfigured (new carousel item)
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
setActiveVideo(initialActiveVideo);
|
|
127
|
+
setActiveVideoIndex(props.activeVideoIndex ?? 0);
|
|
128
|
+
}, [initialActiveVideo]);
|
|
129
|
+
|
|
130
|
+
// Active video changes via event (no Fabric remount)
|
|
131
|
+
useOverlayEvent<{ surfaceId: string; activeVideo: string; activeVideoIndex: number }>(
|
|
132
|
+
'onVideoCarouselActiveVideoChanged', sid,
|
|
133
|
+
(e) => {
|
|
134
|
+
try {
|
|
135
|
+
setActiveVideo(JSON.parse(e.activeVideo));
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore parse errors
|
|
138
|
+
}
|
|
139
|
+
setActiveVideoIndex(e.activeVideoIndex);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!carouselItem || !activeVideo) return null;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Component
|
|
147
|
+
carouselItem={carouselItem}
|
|
148
|
+
activeVideo={activeVideo}
|
|
149
|
+
activeVideoIndex={activeVideoIndex}
|
|
150
|
+
isActive={isActive}
|
|
151
|
+
time={time}
|
|
152
|
+
playerState={playerState}
|
|
153
|
+
isMuted={isMuted}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|