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