@shortkitsdk/react-native 0.2.24 → 0.2.26
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 +151 -0
- package/android/libs/shortkit-release.aar +0 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +19 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +43 -0
- package/ios/ReactCarouselOverlayHost.swift +51 -3
- package/ios/ReactOverlayHost.swift +67 -7
- package/ios/ReactVideoCarouselOverlayHost.swift +181 -19
- package/ios/SKFabricSurfaceWrapper.mm +7 -1
- package/ios/ShortKitBridge.swift +85 -5
- package/ios/ShortKitFeedView.swift +70 -4
- package/ios/ShortKitFeedViewManager.mm +3 -0
- package/ios/ShortKitModule.mm +46 -3
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +5273 -337
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +151 -7
- 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 +151 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +5273 -337
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +151 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +151 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +5273 -337
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +151 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +151 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +38 -10
- package/src/ShortKitCommands.ts +4 -0
- package/src/ShortKitFeed.tsx +65 -10
- package/src/ShortKitOverlaySurface.tsx +59 -23
- package/src/ShortKitProvider.tsx +2 -1
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +51 -5
- package/src/index.ts +2 -0
- package/src/serialization.ts +37 -1
- package/src/specs/NativeShortKitModule.ts +68 -3
- package/src/specs/ShortKitFeedViewNativeComponent.ts +11 -0
- package/src/types.ts +85 -3
- package/src/useShortKitCarousel.ts +80 -0
|
@@ -10,39 +10,39 @@
|
|
|
10
10
|
</data>
|
|
11
11
|
<key>Info.plist</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
Iq8WNAyualDO11/gg6nPxoT/uLo=
|
|
14
14
|
</data>
|
|
15
15
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
|
|
16
16
|
<data>
|
|
17
|
-
|
|
17
|
+
Gzr/etd10eddI2LBjZqt4DbbwAo=
|
|
18
18
|
</data>
|
|
19
19
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
|
|
20
20
|
<data>
|
|
21
|
-
|
|
21
|
+
SY4J8Ap5bvNmlGr2eHB/mIEc9/4=
|
|
22
22
|
</data>
|
|
23
23
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
|
|
24
24
|
<data>
|
|
25
|
-
|
|
25
|
+
Jifa8RGekCm+oLvj1vexV+qHBxg=
|
|
26
26
|
</data>
|
|
27
27
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
|
|
28
28
|
<data>
|
|
29
|
-
|
|
29
|
+
SY4J8Ap5bvNmlGr2eHB/mIEc9/4=
|
|
30
30
|
</data>
|
|
31
31
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
|
|
32
32
|
<data>
|
|
33
|
-
|
|
33
|
+
Gzr/etd10eddI2LBjZqt4DbbwAo=
|
|
34
34
|
</data>
|
|
35
35
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
|
|
36
36
|
<data>
|
|
37
|
-
|
|
37
|
+
RSB+lYIOsHV+EHcf6nw6updgwoE=
|
|
38
38
|
</data>
|
|
39
39
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
|
|
40
40
|
<data>
|
|
41
|
-
|
|
41
|
+
4SaiyfqnnCuP3NHBedJ86U5WUvk=
|
|
42
42
|
</data>
|
|
43
43
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
|
|
44
44
|
<data>
|
|
45
|
-
|
|
45
|
+
RSB+lYIOsHV+EHcf6nw6updgwoE=
|
|
46
46
|
</data>
|
|
47
47
|
<key>Modules/module.modulemap</key>
|
|
48
48
|
<data>
|
|
@@ -66,56 +66,56 @@
|
|
|
66
66
|
<dict>
|
|
67
67
|
<key>hash2</key>
|
|
68
68
|
<data>
|
|
69
|
-
|
|
69
|
+
isVaXgRh+uHcpvhnLI/Xw6vyUa6t68QJDw+sElYsnNw=
|
|
70
70
|
</data>
|
|
71
71
|
</dict>
|
|
72
72
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
|
|
73
73
|
<dict>
|
|
74
74
|
<key>hash2</key>
|
|
75
75
|
<data>
|
|
76
|
-
|
|
76
|
+
1/jxH+/lRA97UGj3ZA78cxyVLWeDthh0KFAHyutYAGs=
|
|
77
77
|
</data>
|
|
78
78
|
</dict>
|
|
79
79
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
|
|
80
80
|
<dict>
|
|
81
81
|
<key>hash2</key>
|
|
82
82
|
<data>
|
|
83
|
-
|
|
83
|
+
1ZNQBox7b3AwcLy9/eOdX9J9kLPO6aiDoKklf4b+uDw=
|
|
84
84
|
</data>
|
|
85
85
|
</dict>
|
|
86
86
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
|
|
87
87
|
<dict>
|
|
88
88
|
<key>hash2</key>
|
|
89
89
|
<data>
|
|
90
|
-
|
|
90
|
+
1/jxH+/lRA97UGj3ZA78cxyVLWeDthh0KFAHyutYAGs=
|
|
91
91
|
</data>
|
|
92
92
|
</dict>
|
|
93
93
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
|
|
94
94
|
<dict>
|
|
95
95
|
<key>hash2</key>
|
|
96
96
|
<data>
|
|
97
|
-
|
|
97
|
+
isVaXgRh+uHcpvhnLI/Xw6vyUa6t68QJDw+sElYsnNw=
|
|
98
98
|
</data>
|
|
99
99
|
</dict>
|
|
100
100
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
|
|
101
101
|
<dict>
|
|
102
102
|
<key>hash2</key>
|
|
103
103
|
<data>
|
|
104
|
-
|
|
104
|
+
LcZQmnn2zALd1t49D1Ky9DkTE/qLtiA/Y8X7G8X0tpA=
|
|
105
105
|
</data>
|
|
106
106
|
</dict>
|
|
107
107
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
|
|
108
108
|
<dict>
|
|
109
109
|
<key>hash2</key>
|
|
110
110
|
<data>
|
|
111
|
-
|
|
111
|
+
rFL1rnzbA6I/ynZT99TT87vcqERbX/oj+lvvjIITisM=
|
|
112
112
|
</data>
|
|
113
113
|
</dict>
|
|
114
114
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
|
|
115
115
|
<dict>
|
|
116
116
|
<key>hash2</key>
|
|
117
117
|
<data>
|
|
118
|
-
|
|
118
|
+
LcZQmnn2zALd1t49D1Ky9DkTE/qLtiA/Y8X7G8X0tpA=
|
|
119
119
|
</data>
|
|
120
120
|
</dict>
|
|
121
121
|
<key>Modules/module.modulemap</key>
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
1
|
+
import React, { useRef, useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { AppRegistry } from 'react-native';
|
|
3
3
|
import type { CarouselOverlayProps, ImageCarouselItem } from './types';
|
|
4
4
|
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
@@ -31,6 +31,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
31
31
|
surfaceId: string | undefined,
|
|
32
32
|
handler: (event: T) => void,
|
|
33
33
|
) {
|
|
34
|
+
// Ref-based handler capture so the subscription always calls the latest
|
|
35
|
+
// closure. Effect depends on [eventName, surfaceId] only — re-subscribing
|
|
36
|
+
// on every render would churn native subscriptions.
|
|
37
|
+
const handlerRef = useRef(handler);
|
|
38
|
+
handlerRef.current = handler;
|
|
39
|
+
|
|
34
40
|
useEffect(() => {
|
|
35
41
|
if (!surfaceId) return;
|
|
36
42
|
|
|
@@ -39,12 +45,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
39
45
|
if (typeof emitter === 'function') {
|
|
40
46
|
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
41
47
|
if (e.surfaceId !== surfaceId) return;
|
|
42
|
-
|
|
48
|
+
handlerRef.current(e);
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
return () => sub?.remove();
|
|
47
|
-
}, [surfaceId]);
|
|
53
|
+
}, [eventName, surfaceId]);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
interface RawCarouselSurfaceProps {
|
|
@@ -87,21 +93,43 @@ function CarouselSurfaceInner(props: InnerProps) {
|
|
|
87
93
|
(e) => setActiveImageIndex(e.activeImageIndex),
|
|
88
94
|
);
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
96
|
+
// Initial item — from surface props (set once on first mount).
|
|
97
|
+
const initialItem: ImageCarouselItem | null = useMemo(() => {
|
|
98
|
+
if (!props.item) return null;
|
|
99
|
+
try { return JSON.parse(props.item); } catch { return null; }
|
|
100
|
+
}, [props.item]);
|
|
101
|
+
|
|
102
|
+
// Subsequent item changes arrive via event — React diff instead of a full
|
|
103
|
+
// Fabric remount on cell reuse. Payload also resets isActive/activeImageIndex.
|
|
104
|
+
const [eventItem, setEventItem] = useState<ImageCarouselItem | null>(null);
|
|
105
|
+
useOverlayEvent<{
|
|
106
|
+
surfaceId: string;
|
|
107
|
+
item: string;
|
|
108
|
+
isActive: boolean;
|
|
109
|
+
activeImageIndex: number;
|
|
110
|
+
}>('onCarouselItemChanged', sid, (e) => {
|
|
94
111
|
try {
|
|
95
|
-
|
|
112
|
+
setEventItem(JSON.parse(e.item));
|
|
96
113
|
} catch {
|
|
97
|
-
|
|
114
|
+
setEventItem(null);
|
|
98
115
|
}
|
|
99
|
-
|
|
116
|
+
setIsActive(e.isActive);
|
|
117
|
+
setActiveImageIndex(e.activeImageIndex);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Prefer event-sourced item (set after first item change) over the initial
|
|
121
|
+
// props-sourced value. Initial mount still uses props; all subsequent
|
|
122
|
+
// transitions are event-driven.
|
|
123
|
+
const item = eventItem ?? initialItem;
|
|
100
124
|
|
|
101
125
|
if (!item) return null;
|
|
102
126
|
|
|
103
127
|
return (
|
|
128
|
+
// key={item.id} preserves "fresh mount per item" for the user's component
|
|
129
|
+
// — their local state resets cleanly across items — while the SDK wrapper
|
|
130
|
+
// above stays mounted and keeps its bridge event subscriptions alive.
|
|
104
131
|
<Component
|
|
132
|
+
key={item.id}
|
|
105
133
|
item={item}
|
|
106
134
|
isActive={isActive}
|
|
107
135
|
activeImageIndex={activeImageIndex}
|
package/src/ShortKitCommands.ts
CHANGED
|
@@ -14,6 +14,10 @@ export const ShortKitCommands = {
|
|
|
14
14
|
seekAndPlay: (seconds: number) => NativeShortKitModule?.seekAndPlay(seconds),
|
|
15
15
|
skipToNext: () => NativeShortKitModule?.skipToNext(),
|
|
16
16
|
skipToPrevious: () => NativeShortKitModule?.skipToPrevious(),
|
|
17
|
+
carouselNext: (): boolean => NativeShortKitModule?.carouselNext() ?? false,
|
|
18
|
+
carouselPrevious: (): boolean => NativeShortKitModule?.carouselPrevious() ?? false,
|
|
19
|
+
carouselSetActiveIndex: (index: number): boolean =>
|
|
20
|
+
NativeShortKitModule?.carouselSetActiveIndex(index) ?? false,
|
|
17
21
|
setMuted: (muted: boolean) => NativeShortKitModule?.setMuted(muted),
|
|
18
22
|
setPlaybackRate: (rate: number) => NativeShortKitModule?.setPlaybackRate(rate),
|
|
19
23
|
setCaptionsEnabled: (enabled: boolean) =>
|
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -31,8 +31,11 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
31
31
|
const {
|
|
32
32
|
config,
|
|
33
33
|
preloadId,
|
|
34
|
+
feedItems,
|
|
35
|
+
active,
|
|
34
36
|
style,
|
|
35
37
|
startAtItemId,
|
|
38
|
+
seedThumbnailUrl,
|
|
36
39
|
onLoop,
|
|
37
40
|
onFeedTransition,
|
|
38
41
|
onFormatChange,
|
|
@@ -42,6 +45,8 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
42
45
|
onDidFetchContentItems,
|
|
43
46
|
onRemainingContentCountChange,
|
|
44
47
|
onFeedReady,
|
|
48
|
+
onCarouselActiveVideoCompleted,
|
|
49
|
+
onVideoCarouselCellTap,
|
|
45
50
|
} = props;
|
|
46
51
|
|
|
47
52
|
const isInitialized = useContext(ShortKitInitContext);
|
|
@@ -49,21 +54,21 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
49
54
|
throw new Error('ShortKitFeed must be used within a ShortKitProvider');
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
// ⚡ TEMP — remove after verifying. Should only log on mount, not on every swipe/time update.
|
|
53
|
-
const renderCount = useRef(0);
|
|
54
|
-
renderCount.current++;
|
|
55
|
-
if (renderCount.current <= 3 || renderCount.current % 100 === 0) {
|
|
56
|
-
console.log(`[SK:Feed] render #${renderCount.current}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
57
|
// Stable feed instance ID — survives re-renders and React fast refresh
|
|
60
58
|
const feedIdRef = useRef(generateFeedId());
|
|
61
59
|
const feedId = feedIdRef.current;
|
|
62
60
|
|
|
63
61
|
// Expose per-feed imperative methods on the ref
|
|
64
62
|
useImperativeHandle(ref, () => ({
|
|
65
|
-
setFeedItems: (items: FeedInput[]) => {
|
|
66
|
-
NativeShortKitModule?.setFeedItems(
|
|
63
|
+
setFeedItems: (items: FeedInput[], options?: { startAt?: string }) => {
|
|
64
|
+
NativeShortKitModule?.setFeedItems(
|
|
65
|
+
feedId,
|
|
66
|
+
serializeFeedInputs(items),
|
|
67
|
+
options?.startAt ?? null,
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
scrollToItem: (id: string, options?: { animated?: boolean }) => {
|
|
71
|
+
NativeShortKitModule?.scrollFeedToItem(feedId, id, options?.animated ?? false);
|
|
67
72
|
},
|
|
68
73
|
appendFeedItems: (items: FeedInput[]) => {
|
|
69
74
|
NativeShortKitModule?.appendFeedItems(feedId, serializeFeedInputs(items));
|
|
@@ -100,6 +105,23 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
100
105
|
return () => subscription.remove();
|
|
101
106
|
}, [feedId, onFeedReady]);
|
|
102
107
|
|
|
108
|
+
// Subscribe to per-feed video-carousel cell-tap events
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!NativeShortKitModule || !onVideoCarouselCellTap) return;
|
|
111
|
+
|
|
112
|
+
const subscription = NativeShortKitModule.onVideoCarouselCellTap((event) => {
|
|
113
|
+
if (event.feedId === feedId) {
|
|
114
|
+
onVideoCarouselCellTap({
|
|
115
|
+
id: event.id,
|
|
116
|
+
index: event.index,
|
|
117
|
+
pageIndex: event.pageIndex,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return () => subscription.remove();
|
|
123
|
+
}, [feedId, onVideoCarouselCellTap]);
|
|
124
|
+
|
|
103
125
|
// Register overlay components before native view mounts.
|
|
104
126
|
// useLayoutEffect fires after commit but before paint — before
|
|
105
127
|
// didMoveToWindow/onAttachedToWindow on the native view.
|
|
@@ -120,6 +142,13 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
120
142
|
[config],
|
|
121
143
|
);
|
|
122
144
|
|
|
145
|
+
// Serialize feedItems once — only the initial value matters (mount-time
|
|
146
|
+
// prop). Post-mount item updates use the imperative ref methods.
|
|
147
|
+
const feedItemsJSON = useMemo(
|
|
148
|
+
() => (feedItems && feedItems.length > 0 ? serializeFeedInputs(feedItems) : undefined),
|
|
149
|
+
[feedItems],
|
|
150
|
+
);
|
|
151
|
+
|
|
123
152
|
// ---------------------------------------------------------------------------
|
|
124
153
|
// Subscribe to feed-level events and forward to callback props
|
|
125
154
|
// ---------------------------------------------------------------------------
|
|
@@ -181,7 +210,10 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
181
210
|
|
|
182
211
|
if (onRefreshStateChanged) {
|
|
183
212
|
subscriptions.push(
|
|
184
|
-
NativeShortKitModule.
|
|
213
|
+
NativeShortKitModule.onRefreshStateChangedPerFeed((event) => {
|
|
214
|
+
// Filter to this feed instance — prevents other feeds' refresh
|
|
215
|
+
// events from triggering this consumer's refresh handler.
|
|
216
|
+
if (event.feedId !== feedId && event.feedId !== '') return;
|
|
185
217
|
const state: ShortKitRefreshState =
|
|
186
218
|
event.status === 'pulling'
|
|
187
219
|
? { status: 'pulling', progress: event.progress }
|
|
@@ -204,6 +236,25 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
204
236
|
);
|
|
205
237
|
}
|
|
206
238
|
|
|
239
|
+
if (onCarouselActiveVideoCompleted) {
|
|
240
|
+
subscriptions.push(
|
|
241
|
+
NativeShortKitModule.onCarouselActiveVideoCompleted((event) => {
|
|
242
|
+
try {
|
|
243
|
+
onCarouselActiveVideoCompleted({
|
|
244
|
+
surfaceId: event.surfaceId,
|
|
245
|
+
contentItem: JSON.parse(event.contentItem),
|
|
246
|
+
indexInCarousel: event.indexInCarousel,
|
|
247
|
+
carouselItem: JSON.parse(event.carouselItem),
|
|
248
|
+
wasLast: event.wasLast,
|
|
249
|
+
willAutoAdvance: event.willAutoAdvance,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.warn('[ShortKit] failed to parse onCarouselActiveVideoCompleted', err);
|
|
253
|
+
}
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
207
258
|
return () => {
|
|
208
259
|
for (const sub of subscriptions) {
|
|
209
260
|
sub.remove();
|
|
@@ -217,6 +268,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
217
268
|
onDismiss,
|
|
218
269
|
onRefreshStateChanged,
|
|
219
270
|
onDidFetchContentItems,
|
|
271
|
+
onCarouselActiveVideoCompleted,
|
|
220
272
|
]);
|
|
221
273
|
|
|
222
274
|
// ---------------------------------------------------------------------------
|
|
@@ -230,6 +282,9 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
230
282
|
feedId={feedId}
|
|
231
283
|
startAtItemId={startAtItemId}
|
|
232
284
|
preloadId={preloadId}
|
|
285
|
+
seedThumbnailUrl={seedThumbnailUrl}
|
|
286
|
+
feedItemsJSON={feedItemsJSON}
|
|
287
|
+
active={active}
|
|
233
288
|
/>
|
|
234
289
|
</View>
|
|
235
290
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { AppRegistry, View, Text } from 'react-native';
|
|
3
3
|
import type { OverlayProps, PlayerState, PlayerTime, FeedScrollPhase } from './types';
|
|
4
4
|
import { deserializeContentItem } from './serialization';
|
|
@@ -101,6 +101,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
101
101
|
surfaceId: string | undefined,
|
|
102
102
|
handler: (event: T) => void,
|
|
103
103
|
) {
|
|
104
|
+
// Stash the latest handler in a ref so the subscription below always calls
|
|
105
|
+
// the current closure. The effect deliberately depends on [eventName, surfaceId]
|
|
106
|
+
// only — re-subscribing on every render would churn native subscriptions.
|
|
107
|
+
const handlerRef = useRef(handler);
|
|
108
|
+
handlerRef.current = handler;
|
|
109
|
+
|
|
104
110
|
useEffect(() => {
|
|
105
111
|
if (!surfaceId) return;
|
|
106
112
|
|
|
@@ -112,12 +118,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
112
118
|
if (typeof emitter === 'function') {
|
|
113
119
|
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
114
120
|
if (e.surfaceId !== surfaceId) return;
|
|
115
|
-
|
|
121
|
+
handlerRef.current(e);
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
return () => sub?.remove();
|
|
120
|
-
}, [surfaceId]);
|
|
126
|
+
}, [eventName, surfaceId]);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
@@ -131,12 +137,12 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
131
137
|
props.item ? deserializeContentItem(props.item) : null,
|
|
132
138
|
);
|
|
133
139
|
|
|
134
|
-
// Sync item from props when surface is first mounted or remounted via updateInitProps
|
|
140
|
+
// Sync item from props when surface is first mounted or remounted via updateInitProps.
|
|
141
|
+
// Synchronous setState during render — React restarts with new state before
|
|
142
|
+
// committing, so the stale frame is never painted.
|
|
135
143
|
const prevPropsItemRef = React.useRef(props.item);
|
|
136
144
|
if (props.item !== prevPropsItemRef.current) {
|
|
137
145
|
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
146
|
setItem(props.item ? deserializeContentItem(props.item) : null);
|
|
141
147
|
}
|
|
142
148
|
|
|
@@ -188,23 +194,45 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
188
194
|
|
|
189
195
|
// --- Event subscriptions (filtered by surfaceId) ---
|
|
190
196
|
|
|
191
|
-
// Item changed via event
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
// Item changed via event — triggers React DIFF instead of a full Fabric
|
|
198
|
+
// remount. Now a first-class codegen event (see NativeShortKitModule.ts),
|
|
199
|
+
// which routes through the JSI EventEmitterCallback on both platforms.
|
|
200
|
+
// Payload carries full initial state to match what pushInitialProperties()
|
|
201
|
+
// would have set, avoiding any stale-state window before activatePlayback().
|
|
202
|
+
useOverlayEvent<{
|
|
203
|
+
surfaceId: string;
|
|
204
|
+
item: string;
|
|
205
|
+
isActive: boolean;
|
|
206
|
+
playerState: string;
|
|
207
|
+
isMuted: boolean;
|
|
208
|
+
playbackRate: number;
|
|
209
|
+
captionsEnabled: boolean;
|
|
210
|
+
activeCue: string | null;
|
|
211
|
+
feedScrollPhase: string | null;
|
|
212
|
+
}>('onOverlayItemChanged', sid, (e) => {
|
|
213
|
+
const newItem = e.item ? deserializeContentItem(e.item) : null;
|
|
214
|
+
if (newItem) setItem(newItem);
|
|
215
|
+
// Apply ALL initial state — matches the old setProperties() behavior.
|
|
216
|
+
setIsActive(e.isActive);
|
|
217
|
+
setPlayerState(e.playerState as PlayerState);
|
|
218
|
+
setIsMuted(e.isMuted);
|
|
219
|
+
setPlaybackRate(e.playbackRate);
|
|
220
|
+
setCaptionsEnabled(e.captionsEnabled);
|
|
221
|
+
setTime({ current: 0, duration: 0, buffered: 0 });
|
|
222
|
+
const rawCue = e.activeCue ?? null;
|
|
223
|
+
lastActiveCueJsonRef.current = rawCue;
|
|
224
|
+
try {
|
|
225
|
+
setActiveCue(rawCue ? JSON.parse(rawCue) : null);
|
|
226
|
+
} catch {
|
|
227
|
+
setActiveCue(null);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
|
|
231
|
+
setFeedScrollPhase(next);
|
|
232
|
+
} catch {
|
|
233
|
+
setFeedScrollPhase(null);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
208
236
|
|
|
209
237
|
useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
|
|
210
238
|
'onOverlayActiveChanged', sid,
|
|
@@ -310,7 +338,15 @@ function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
|
310
338
|
|
|
311
339
|
return (
|
|
312
340
|
<OverlayErrorBoundary surfaceId={sid} overlayName={props.overlayName}>
|
|
341
|
+
{/*
|
|
342
|
+
key={item.id} preserves the legacy "fresh mount per item" contract
|
|
343
|
+
for the user's overlay component (their local useState/useRef reset
|
|
344
|
+
cleanly across items). The SDK wrapper above stays mounted, so
|
|
345
|
+
bridge event subscriptions and SDK-side state survive cell reuse
|
|
346
|
+
instead of churning on every swipe.
|
|
347
|
+
*/}
|
|
313
348
|
<Component
|
|
349
|
+
key={item.id}
|
|
314
350
|
item={item}
|
|
315
351
|
isActive={isActive}
|
|
316
352
|
playerState={playerState}
|
package/src/ShortKitProvider.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from './types';
|
|
20
20
|
import {
|
|
21
21
|
serializeFeedConfig,
|
|
22
|
+
serializeFeedInputs,
|
|
22
23
|
deserializePlayerState,
|
|
23
24
|
deserializeContentItem,
|
|
24
25
|
deserializePlayerTime,
|
|
@@ -466,7 +467,7 @@ export function ShortKitProvider({
|
|
|
466
467
|
const preloadFeedCmd = useCallback(async (config?: Partial<FeedConfig>, items?: FeedInput[]): Promise<string> => {
|
|
467
468
|
if (!NativeShortKitModule) return '';
|
|
468
469
|
const configJSON = config ? serializeFeedConfig(config as FeedConfig) : '{}';
|
|
469
|
-
const itemsJSON = items ?
|
|
470
|
+
const itemsJSON = items ? serializeFeedInputs(items) : null;
|
|
470
471
|
return NativeShortKitModule.preloadFeed(configJSON, itemsJSON);
|
|
471
472
|
}, []);
|
|
472
473
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
1
|
+
import React, { useRef, useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { AppRegistry } from 'react-native';
|
|
3
3
|
import type { VideoCarouselOverlayProps, VideoCarouselItem, ContentItem, PlayerTime, PlayerState } from './types';
|
|
4
4
|
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
@@ -31,6 +31,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
31
31
|
surfaceId: string | undefined,
|
|
32
32
|
handler: (event: T) => void,
|
|
33
33
|
) {
|
|
34
|
+
// Ref-based handler capture so the subscription always calls the latest
|
|
35
|
+
// closure. Effect depends on [eventName, surfaceId] only — re-subscribing
|
|
36
|
+
// on every render would churn native subscriptions.
|
|
37
|
+
const handlerRef = useRef(handler);
|
|
38
|
+
handlerRef.current = handler;
|
|
39
|
+
|
|
34
40
|
useEffect(() => {
|
|
35
41
|
if (!surfaceId) return;
|
|
36
42
|
|
|
@@ -39,12 +45,12 @@ function useOverlayEvent<T extends { surfaceId: string }>(
|
|
|
39
45
|
if (typeof emitter === 'function') {
|
|
40
46
|
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
41
47
|
if (e.surfaceId !== surfaceId) return;
|
|
42
|
-
|
|
48
|
+
handlerRef.current(e);
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
return () => sub?.remove();
|
|
47
|
-
}, [surfaceId]);
|
|
53
|
+
}, [eventName, surfaceId]);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
interface RawProps {
|
|
@@ -140,11 +146,51 @@ function VideoCarouselSurfaceInner(props: InnerProps) {
|
|
|
140
146
|
},
|
|
141
147
|
);
|
|
142
148
|
|
|
143
|
-
|
|
149
|
+
// Carousel item itself changes via event — React diff instead of a full
|
|
150
|
+
// Fabric remount on cell reuse. Payload carries full initial state to match
|
|
151
|
+
// the old setProperties() path.
|
|
152
|
+
const [eventCarouselItem, setEventCarouselItem] = useState<VideoCarouselItem | null>(null);
|
|
153
|
+
useOverlayEvent<{
|
|
154
|
+
surfaceId: string;
|
|
155
|
+
carouselItem: string;
|
|
156
|
+
activeVideo: string;
|
|
157
|
+
activeVideoIndex: number;
|
|
158
|
+
isActive: boolean;
|
|
159
|
+
playerState: string;
|
|
160
|
+
isMuted: boolean;
|
|
161
|
+
}>('onVideoCarouselItemChanged', sid, (e) => {
|
|
162
|
+
try {
|
|
163
|
+
setEventCarouselItem(JSON.parse(e.carouselItem));
|
|
164
|
+
} catch {
|
|
165
|
+
setEventCarouselItem(null);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
setActiveVideo(e.activeVideo ? JSON.parse(e.activeVideo) : null);
|
|
169
|
+
} catch {
|
|
170
|
+
setActiveVideo(null);
|
|
171
|
+
}
|
|
172
|
+
setActiveVideoIndex(e.activeVideoIndex);
|
|
173
|
+
setIsActive(e.isActive);
|
|
174
|
+
setPlayerState(e.playerState as PlayerState);
|
|
175
|
+
setIsMuted(e.isMuted);
|
|
176
|
+
setTime({ current: 0, duration: 0, buffered: 0 });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Prefer the event-sourced carousel item (set after first item change) over
|
|
180
|
+
// the initial props-sourced value. The initial mount still goes through
|
|
181
|
+
// props, but all subsequent item transitions are event-driven.
|
|
182
|
+
const effectiveCarouselItem = eventCarouselItem ?? carouselItem;
|
|
183
|
+
|
|
184
|
+
if (!effectiveCarouselItem || !activeVideo) return null;
|
|
144
185
|
|
|
145
186
|
return (
|
|
187
|
+
// key={effectiveCarouselItem.id} preserves the legacy "fresh mount per
|
|
188
|
+
// carousel item" contract for the user's component — their local state
|
|
189
|
+
// resets cleanly across items — while the SDK wrapper above stays mounted
|
|
190
|
+
// and keeps its bridge event subscriptions alive.
|
|
146
191
|
<Component
|
|
147
|
-
|
|
192
|
+
key={effectiveCarouselItem.id}
|
|
193
|
+
carouselItem={effectiveCarouselItem}
|
|
148
194
|
activeVideo={activeVideo}
|
|
149
195
|
activeVideoIndex={activeVideoIndex}
|
|
150
196
|
isActive={isActive}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { ShortKitPlayer } from './ShortKitPlayer';
|
|
|
4
4
|
export { ShortKitWidget } from './ShortKitWidget';
|
|
5
5
|
export { useShortKitPlayer } from './useShortKitPlayer';
|
|
6
6
|
export { useShortKit } from './useShortKit';
|
|
7
|
+
export { useShortKitCarousel } from './useShortKitCarousel';
|
|
7
8
|
export type {
|
|
8
9
|
FeedConfig,
|
|
9
10
|
FeedHeight,
|
|
@@ -40,6 +41,7 @@ export type {
|
|
|
40
41
|
ShortKitPlayerProps,
|
|
41
42
|
ShortKitWidgetProps,
|
|
42
43
|
ShortKitPlayerState,
|
|
44
|
+
ShortKitCarouselState,
|
|
43
45
|
ShortKitRefreshState,
|
|
44
46
|
StoryboardData,
|
|
45
47
|
StoryboardTile,
|
package/src/serialization.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
FeedInput,
|
|
5
5
|
PlayerState,
|
|
6
6
|
PlayerTime,
|
|
7
|
+
VideoCarouselVideoInput,
|
|
7
8
|
} from './types';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -88,9 +89,44 @@ export function deserializePlayerTime(event: {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Serialize a single VideoCarouselVideoInput slide for the bridge.
|
|
94
|
+
* Always emits `origin` (defaults to 'other') for a consistent native shape.
|
|
95
|
+
*/
|
|
96
|
+
function serializeVideoCarouselSlide(
|
|
97
|
+
slide: VideoCarouselVideoInput,
|
|
98
|
+
): Record<string, unknown> {
|
|
99
|
+
return {
|
|
100
|
+
playbackId: slide.playbackId,
|
|
101
|
+
origin: slide.origin ?? 'other',
|
|
102
|
+
...(slide.fallbackUrl != null ? { fallbackUrl: slide.fallbackUrl } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
/**
|
|
92
107
|
* Serialize FeedInput[] to a JSON string for the bridge.
|
|
108
|
+
* For video items, `origin` is included and defaults to 'other' when omitted.
|
|
93
109
|
*/
|
|
94
110
|
export function serializeFeedInputs(items: FeedInput[]): string {
|
|
95
|
-
|
|
111
|
+
const serialized = items.map((input) => {
|
|
112
|
+
if (input.type === 'video') {
|
|
113
|
+
return {
|
|
114
|
+
type: 'video',
|
|
115
|
+
playbackId: input.playbackId,
|
|
116
|
+
origin: input.origin ?? 'other',
|
|
117
|
+
...(input.fallbackUrl != null ? { fallbackUrl: input.fallbackUrl } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (input.type === 'videoCarousel') {
|
|
121
|
+
return {
|
|
122
|
+
type: 'videoCarousel',
|
|
123
|
+
item: {
|
|
124
|
+
...input.item,
|
|
125
|
+
videos: input.item.videos.map(serializeVideoCarouselSlide),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return input;
|
|
130
|
+
});
|
|
131
|
+
return JSON.stringify(serialized);
|
|
96
132
|
}
|