@shortkitsdk/react-native 0.2.24 → 0.2.25
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 +43 -1
- package/ios/ShortKitFeedView.swift +20 -0
- package/ios/ShortKitFeedViewManager.mm +1 -0
- package/ios/ShortKitModule.mm +33 -0
- package/ios/ShortKitSDK.xcframework/Info.plist +5 -5
- 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 +3590 -382
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +104 -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 +104 -5
- 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 +3590 -382
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +104 -5
- 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 +104 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +3590 -382
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +104 -5
- 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 +104 -5
- 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 +23 -7
- package/src/ShortKitOverlaySurface.tsx +59 -23
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +51 -5
- package/src/index.ts +2 -0
- package/src/serialization.ts +37 -1
- package/src/specs/NativeShortKitModule.ts +51 -2
- package/src/specs/ShortKitFeedViewNativeComponent.ts +8 -0
- package/src/types.ts +27 -1
- package/src/useShortKitCarousel.ts +80 -0
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -33,6 +33,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
33
33
|
preloadId,
|
|
34
34
|
style,
|
|
35
35
|
startAtItemId,
|
|
36
|
+
seedThumbnailUrl,
|
|
36
37
|
onLoop,
|
|
37
38
|
onFeedTransition,
|
|
38
39
|
onFormatChange,
|
|
@@ -42,6 +43,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
42
43
|
onDidFetchContentItems,
|
|
43
44
|
onRemainingContentCountChange,
|
|
44
45
|
onFeedReady,
|
|
46
|
+
onCarouselActiveVideoCompleted,
|
|
45
47
|
} = props;
|
|
46
48
|
|
|
47
49
|
const isInitialized = useContext(ShortKitInitContext);
|
|
@@ -49,13 +51,6 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
49
51
|
throw new Error('ShortKitFeed must be used within a ShortKitProvider');
|
|
50
52
|
}
|
|
51
53
|
|
|
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
54
|
// Stable feed instance ID — survives re-renders and React fast refresh
|
|
60
55
|
const feedIdRef = useRef(generateFeedId());
|
|
61
56
|
const feedId = feedIdRef.current;
|
|
@@ -204,6 +199,25 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
204
199
|
);
|
|
205
200
|
}
|
|
206
201
|
|
|
202
|
+
if (onCarouselActiveVideoCompleted) {
|
|
203
|
+
subscriptions.push(
|
|
204
|
+
NativeShortKitModule.onCarouselActiveVideoCompleted((event) => {
|
|
205
|
+
try {
|
|
206
|
+
onCarouselActiveVideoCompleted({
|
|
207
|
+
surfaceId: event.surfaceId,
|
|
208
|
+
contentItem: JSON.parse(event.contentItem),
|
|
209
|
+
indexInCarousel: event.indexInCarousel,
|
|
210
|
+
carouselItem: JSON.parse(event.carouselItem),
|
|
211
|
+
wasLast: event.wasLast,
|
|
212
|
+
willAutoAdvance: event.willAutoAdvance,
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.warn('[ShortKit] failed to parse onCarouselActiveVideoCompleted', err);
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
207
221
|
return () => {
|
|
208
222
|
for (const sub of subscriptions) {
|
|
209
223
|
sub.remove();
|
|
@@ -217,6 +231,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
217
231
|
onDismiss,
|
|
218
232
|
onRefreshStateChanged,
|
|
219
233
|
onDidFetchContentItems,
|
|
234
|
+
onCarouselActiveVideoCompleted,
|
|
220
235
|
]);
|
|
221
236
|
|
|
222
237
|
// ---------------------------------------------------------------------------
|
|
@@ -230,6 +245,7 @@ export const ShortKitFeed = forwardRef<ShortKitFeedHandle, ShortKitFeedProps>(
|
|
|
230
245
|
feedId={feedId}
|
|
231
246
|
startAtItemId={startAtItemId}
|
|
232
247
|
preloadId={preloadId}
|
|
248
|
+
seedThumbnailUrl={seedThumbnailUrl}
|
|
233
249
|
/>
|
|
234
250
|
</View>
|
|
235
251
|
);
|
|
@@ -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}
|
|
@@ -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
|
}
|
|
@@ -177,12 +177,38 @@ type CarouselActiveImageEvent = Readonly<{
|
|
|
177
177
|
activeImageIndex: Int32;
|
|
178
178
|
}>;
|
|
179
179
|
|
|
180
|
+
type CarouselItemChangedEvent = Readonly<{
|
|
181
|
+
surfaceId: string;
|
|
182
|
+
item: string; // JSON-serialized ImageCarouselItem (with local file:// URLs where cached)
|
|
183
|
+
isActive: boolean;
|
|
184
|
+
activeImageIndex: Int32;
|
|
185
|
+
}>;
|
|
186
|
+
|
|
180
187
|
type VideoCarouselActiveVideoEvent = Readonly<{
|
|
181
188
|
surfaceId: string;
|
|
182
189
|
activeVideo: string;
|
|
183
190
|
activeVideoIndex: Int32;
|
|
184
191
|
}>;
|
|
185
192
|
|
|
193
|
+
type VideoCarouselItemChangedEvent = Readonly<{
|
|
194
|
+
surfaceId: string;
|
|
195
|
+
carouselItem: string; // JSON-serialized VideoCarouselItem
|
|
196
|
+
activeVideo: string | null; // JSON-serialized ContentItem; null when item has no videos
|
|
197
|
+
activeVideoIndex: Int32;
|
|
198
|
+
isActive: boolean;
|
|
199
|
+
playerState: string;
|
|
200
|
+
isMuted: boolean;
|
|
201
|
+
}>;
|
|
202
|
+
|
|
203
|
+
type CarouselActiveVideoCompletedEvent = Readonly<{
|
|
204
|
+
surfaceId: string;
|
|
205
|
+
contentItem: string; // JSON-serialized ContentItem
|
|
206
|
+
indexInCarousel: Int32;
|
|
207
|
+
carouselItem: string; // JSON-serialized VideoCarouselItem
|
|
208
|
+
wasLast: boolean;
|
|
209
|
+
willAutoAdvance: boolean;
|
|
210
|
+
}>;
|
|
211
|
+
|
|
186
212
|
type OverlayFullStateEvent = Readonly<{
|
|
187
213
|
surfaceId: string;
|
|
188
214
|
isActive: boolean;
|
|
@@ -190,8 +216,20 @@ type OverlayFullStateEvent = Readonly<{
|
|
|
190
216
|
isMuted: boolean;
|
|
191
217
|
playbackRate: Double;
|
|
192
218
|
captionsEnabled: boolean;
|
|
193
|
-
activeCue: string;
|
|
194
|
-
feedScrollPhase: string;
|
|
219
|
+
activeCue: string | null;
|
|
220
|
+
feedScrollPhase: string | null;
|
|
221
|
+
}>;
|
|
222
|
+
|
|
223
|
+
type OverlayItemChangedEvent = Readonly<{
|
|
224
|
+
surfaceId: string;
|
|
225
|
+
item: string; // JSON-serialized ContentItem
|
|
226
|
+
isActive: boolean;
|
|
227
|
+
playerState: string;
|
|
228
|
+
isMuted: boolean;
|
|
229
|
+
playbackRate: Double;
|
|
230
|
+
captionsEnabled: boolean;
|
|
231
|
+
activeCue: string | null;
|
|
232
|
+
feedScrollPhase: string | null;
|
|
195
233
|
}>;
|
|
196
234
|
|
|
197
235
|
export interface Spec extends TurboModule {
|
|
@@ -217,6 +255,9 @@ export interface Spec extends TurboModule {
|
|
|
217
255
|
seekAndPlay(seconds: Double): void;
|
|
218
256
|
skipToNext(): void;
|
|
219
257
|
skipToPrevious(): void;
|
|
258
|
+
carouselNext(): boolean;
|
|
259
|
+
carouselPrevious(): boolean;
|
|
260
|
+
carouselSetActiveIndex(index: Int32): boolean;
|
|
220
261
|
setMuted(muted: boolean): void;
|
|
221
262
|
setPlaybackRate(rate: Double): void;
|
|
222
263
|
setCaptionsEnabled(enabled: boolean): void;
|
|
@@ -239,6 +280,10 @@ export interface Spec extends TurboModule {
|
|
|
239
280
|
downloadVideo(itemId: string, mode: string): Promise<string>;
|
|
240
281
|
cancelDownload(): void;
|
|
241
282
|
|
|
283
|
+
// --- Carousel accessors ---
|
|
284
|
+
getCarouselActiveIndex(): Int32;
|
|
285
|
+
getCarouselVideoCount(): Int32;
|
|
286
|
+
|
|
242
287
|
// --- Event emitters ---
|
|
243
288
|
readonly onPlayerStateChanged: EventEmitter<PlayerStateEvent>;
|
|
244
289
|
readonly onCurrentItemChanged: EventEmitter<CurrentItemEvent>;
|
|
@@ -270,8 +315,12 @@ export interface Spec extends TurboModule {
|
|
|
270
315
|
readonly onOverlayFeedScrollPhaseChanged: EventEmitter<OverlayFeedScrollPhaseEvent>;
|
|
271
316
|
readonly onOverlayTimeUpdate: EventEmitter<OverlayTimeUpdateEvent>;
|
|
272
317
|
readonly onOverlayFullState: EventEmitter<OverlayFullStateEvent>;
|
|
318
|
+
readonly onOverlayItemChanged: EventEmitter<OverlayItemChangedEvent>;
|
|
273
319
|
readonly onCarouselActiveImageChanged: EventEmitter<CarouselActiveImageEvent>;
|
|
320
|
+
readonly onCarouselItemChanged: EventEmitter<CarouselItemChangedEvent>;
|
|
274
321
|
readonly onVideoCarouselActiveVideoChanged: EventEmitter<VideoCarouselActiveVideoEvent>;
|
|
322
|
+
readonly onVideoCarouselItemChanged: EventEmitter<VideoCarouselItemChangedEvent>;
|
|
323
|
+
readonly onCarouselActiveVideoCompleted: EventEmitter<CarouselActiveVideoCompletedEvent>;
|
|
275
324
|
|
|
276
325
|
// --- Download events ---
|
|
277
326
|
readonly onDownloadStarted: EventEmitter<DownloadStartedEvent>;
|
|
@@ -6,6 +6,14 @@ export interface NativeProps extends ViewProps {
|
|
|
6
6
|
feedId?: string;
|
|
7
7
|
startAtItemId?: string;
|
|
8
8
|
preloadId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* URL of a thumbnail already rendered/cached on the host side (e.g. the
|
|
11
|
+
* grid tile the user tapped to open this feed). When expo-image or FastImage
|
|
12
|
+
* is used on iOS, SDK fetches the decoded UIImage from SDWebImage's memory
|
|
13
|
+
* cache and paints it onto the first cell with zero network and zero
|
|
14
|
+
* latency. Falls back to network fetch for non-SDWebImage clients.
|
|
15
|
+
*/
|
|
16
|
+
seedThumbnailUrl?: string;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export default codegenNativeComponent<NativeProps>(
|
package/src/types.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface VideoCarouselItem {
|
|
|
113
113
|
articleUrl?: string;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export type ContentOrigin = 'ios_upload' | 'other';
|
|
117
|
+
|
|
116
118
|
/**
|
|
117
119
|
* Per-slide input for a VideoCarouselInput. Mirrors the `{ type: 'video' }`
|
|
118
120
|
* FeedInput variant — host apps pass a playback ID and the SDK constructs
|
|
@@ -120,6 +122,7 @@ export interface VideoCarouselItem {
|
|
|
120
122
|
*/
|
|
121
123
|
export interface VideoCarouselVideoInput {
|
|
122
124
|
playbackId: string;
|
|
125
|
+
origin?: ContentOrigin;
|
|
123
126
|
fallbackUrl?: string;
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -142,7 +145,7 @@ export interface VideoCarouselInput {
|
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
export type FeedInput =
|
|
145
|
-
| { type: 'video'; playbackId: string; fallbackUrl?: string }
|
|
148
|
+
| { type: 'video'; playbackId: string; origin?: ContentOrigin; fallbackUrl?: string }
|
|
146
149
|
| { type: 'imageCarousel'; item: ImageCarouselItem }
|
|
147
150
|
| { type: 'videoCarousel'; item: VideoCarouselInput };
|
|
148
151
|
|
|
@@ -300,6 +303,17 @@ export interface ShortKitFeedProps {
|
|
|
300
303
|
style?: ViewStyle;
|
|
301
304
|
/** Item ID to scroll to on initial load. */
|
|
302
305
|
startAtItemId?: string;
|
|
306
|
+
/**
|
|
307
|
+
* URL of the thumbnail already displayed in your UI (e.g. the grid tile
|
|
308
|
+
* the user just tapped). If your app uses expo-image or FastImage, the SDK
|
|
309
|
+
* will extract the already-decoded UIImage from SDWebImage's memory cache
|
|
310
|
+
* and paint it on the first feed cell with zero network overhead — closing
|
|
311
|
+
* the black-frame gap before video playback begins.
|
|
312
|
+
*
|
|
313
|
+
* Falls back silently (no extra network beyond the existing fallback) for
|
|
314
|
+
* apps on React Native's core <Image> or other image libraries.
|
|
315
|
+
*/
|
|
316
|
+
seedThumbnailUrl?: string;
|
|
303
317
|
onLoop?: (event: LoopEvent) => void;
|
|
304
318
|
onFeedTransition?: (event: FeedTransitionEvent) => void;
|
|
305
319
|
onFormatChange?: (event: FormatChangeEvent) => void;
|
|
@@ -316,6 +330,15 @@ export interface ShortKitFeedProps {
|
|
|
316
330
|
/** Called once when this feed has loaded content and assigned a player
|
|
317
331
|
* to the first cell. Use to dismiss a splash screen or loading overlay. */
|
|
318
332
|
onFeedReady?: () => void;
|
|
333
|
+
/** Called when the active video in a video carousel completes playback. */
|
|
334
|
+
onCarouselActiveVideoCompleted?: (event: {
|
|
335
|
+
surfaceId: string;
|
|
336
|
+
contentItem: ContentItem;
|
|
337
|
+
indexInCarousel: number;
|
|
338
|
+
carouselItem: VideoCarouselItem;
|
|
339
|
+
wasLast: boolean;
|
|
340
|
+
willAutoAdvance: boolean;
|
|
341
|
+
}) => void;
|
|
319
342
|
}
|
|
320
343
|
|
|
321
344
|
/**
|
|
@@ -382,6 +405,9 @@ export interface ShortKitWidgetProps {
|
|
|
382
405
|
|
|
383
406
|
// --- Hook Return Types ---
|
|
384
407
|
|
|
408
|
+
export type { ShortKitCarouselState } from './useShortKitCarousel';
|
|
409
|
+
|
|
410
|
+
|
|
385
411
|
export interface ShortKitPlayerState {
|
|
386
412
|
playerState: PlayerState;
|
|
387
413
|
currentItem: ContentItem | null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
3
|
+
import { ShortKitCommands } from './ShortKitCommands';
|
|
4
|
+
import type { VideoCarouselItem } from './types';
|
|
5
|
+
|
|
6
|
+
export interface ShortKitCarouselState {
|
|
7
|
+
/** Index of the currently active video in the carousel, or null if no carousel is active. */
|
|
8
|
+
activeIndex: number | null;
|
|
9
|
+
/** Number of videos in the active carousel item, or 0 if none. */
|
|
10
|
+
videoCount: number;
|
|
11
|
+
/** The active carousel item, or null if no carousel is active. */
|
|
12
|
+
activeCarouselItem: VideoCarouselItem | null;
|
|
13
|
+
/** Advance to the next video in the carousel. Returns true if the command was dispatched. */
|
|
14
|
+
next: () => boolean;
|
|
15
|
+
/** Go back to the previous video in the carousel. Returns true if the command was dispatched. */
|
|
16
|
+
previous: () => boolean;
|
|
17
|
+
/** Jump to a specific index in the carousel. Returns true if the command was dispatched. */
|
|
18
|
+
setActiveIndex: (index: number) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to access video carousel state and commands.
|
|
23
|
+
*
|
|
24
|
+
* Subscribes to `onVideoCarouselItemChanged` and
|
|
25
|
+
* `onVideoCarouselActiveVideoChanged` from the native TurboModule and surfaces
|
|
26
|
+
* carousel state as reactive values. The imperative commands (`next`,
|
|
27
|
+
* `previous`, `setActiveIndex`) delegate to `ShortKitCommands`.
|
|
28
|
+
*
|
|
29
|
+
* Does not require a `<ShortKitProvider>` — it subscribes directly to native
|
|
30
|
+
* events and is suitable for use inside overlay components.
|
|
31
|
+
*/
|
|
32
|
+
export function useShortKitCarousel(): ShortKitCarouselState {
|
|
33
|
+
const [activeIndex, setActiveIndexState] = useState<number | null>(() => {
|
|
34
|
+
const v = NativeShortKitModule?.getCarouselActiveIndex?.() ?? -1;
|
|
35
|
+
return v < 0 ? null : v;
|
|
36
|
+
});
|
|
37
|
+
const [activeCarouselItem, setActiveCarouselItem] = useState<VideoCarouselItem | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!NativeShortKitModule) return;
|
|
41
|
+
const subItem = NativeShortKitModule.onVideoCarouselItemChanged((e: any) => {
|
|
42
|
+
try {
|
|
43
|
+
const item = e.carouselItem
|
|
44
|
+
? (JSON.parse(e.carouselItem) as VideoCarouselItem)
|
|
45
|
+
: null;
|
|
46
|
+
setActiveCarouselItem(item);
|
|
47
|
+
setActiveIndexState(
|
|
48
|
+
typeof e.activeVideoIndex === 'number' ? e.activeVideoIndex : null,
|
|
49
|
+
);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore parse errors
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const subIdx = NativeShortKitModule.onVideoCarouselActiveVideoChanged((e: any) => {
|
|
55
|
+
setActiveIndexState(
|
|
56
|
+
typeof e.activeVideoIndex === 'number' ? e.activeVideoIndex : null,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
subItem?.remove?.();
|
|
61
|
+
subIdx?.remove?.();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const next = useCallback(() => ShortKitCommands.carouselNext(), []);
|
|
66
|
+
const previous = useCallback(() => ShortKitCommands.carouselPrevious(), []);
|
|
67
|
+
const setActiveIndex = useCallback(
|
|
68
|
+
(i: number) => ShortKitCommands.carouselSetActiveIndex(i),
|
|
69
|
+
[],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
activeIndex,
|
|
74
|
+
videoCount: activeCarouselItem?.videos?.length ?? 0,
|
|
75
|
+
activeCarouselItem,
|
|
76
|
+
next,
|
|
77
|
+
previous,
|
|
78
|
+
setActiveIndex,
|
|
79
|
+
};
|
|
80
|
+
}
|