@shortkitsdk/react-native 0.2.6 → 0.2.12
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 +17 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +570 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -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 +444 -0
- package/ios/SKFabricSurfaceWrapper.h +18 -0
- package/ios/SKFabricSurfaceWrapper.mm +57 -0
- package/ios/ShortKitBridge.swift +220 -63
- package/ios/ShortKitFeedView.swift +82 -228
- package/ios/ShortKitFeedViewManager.mm +3 -2
- package/ios/ShortKitModule.mm +69 -37
- 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 +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3683 -1249
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
- 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 +56 -15
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3683 -1249
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
- 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 +56 -15
- 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 +6 -24
- package/src/ShortKitFeed.tsx +124 -41
- package/src/ShortKitLoadingSurface.tsx +24 -0
- package/src/ShortKitOverlaySurface.tsx +313 -0
- package/src/ShortKitPlayer.tsx +30 -9
- package/src/ShortKitProvider.tsx +28 -285
- package/src/index.ts +9 -3
- package/src/serialization.ts +20 -39
- package/src/specs/NativeShortKitModule.ts +74 -45
- package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
- package/src/types.ts +84 -16
- package/src/useShortKit.ts +1 -3
- package/src/useShortKitPlayer.ts +7 -7
- 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 -219
- package/ios/ShortKitOverlayBridge.swift +0 -111
- package/src/CarouselOverlayManager.tsx +0 -70
- package/src/OverlayManager.tsx +0 -87
- package/src/useShortKitCarousel.ts +0 -29
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { AppRegistry, View, Text } 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
|
+
const SK_OVERLAY_TAG = '[ShortKit:OverlaySurface]';
|
|
8
|
+
|
|
9
|
+
// Named registry — supports different overlay components per feed
|
|
10
|
+
const _overlayRegistry = new Map<string, React.ComponentType<OverlayProps>>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register a named overlay component for rendering inside feed cells.
|
|
14
|
+
* Called by ShortKitFeed on mount via useLayoutEffect.
|
|
15
|
+
* Idempotent — registering the same name twice is a no-op.
|
|
16
|
+
*/
|
|
17
|
+
export function registerOverlayComponent(
|
|
18
|
+
name: string,
|
|
19
|
+
component: React.ComponentType<OverlayProps>,
|
|
20
|
+
) {
|
|
21
|
+
if (_overlayRegistry.has(name)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
_overlayRegistry.set(name, component);
|
|
25
|
+
|
|
26
|
+
const moduleName = `ShortKitOverlay_${name}`;
|
|
27
|
+
AppRegistry.registerComponent(moduleName, () => {
|
|
28
|
+
return function NamedOverlaySurface(props: RawOverlaySurfaceProps) {
|
|
29
|
+
return <ShortKitOverlaySurfaceInner {...props} overlayName={name} />;
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Error boundary that catches crashes inside the overlay component.
|
|
36
|
+
* Without this, a crash in an isolated React surface is silent on Android.
|
|
37
|
+
*/
|
|
38
|
+
class OverlayErrorBoundary extends React.Component<
|
|
39
|
+
{ surfaceId?: string; overlayName: string; children: React.ReactNode },
|
|
40
|
+
{ error: Error | null }
|
|
41
|
+
> {
|
|
42
|
+
state: { error: Error | null } = { error: null };
|
|
43
|
+
|
|
44
|
+
static getDerivedStateFromError(error: Error) {
|
|
45
|
+
return { error };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
49
|
+
console.error(
|
|
50
|
+
`${SK_OVERLAY_TAG} CRASH in overlay '${this.props.overlayName}' (surfaceId=${this.props.surfaceId}):`,
|
|
51
|
+
error.message,
|
|
52
|
+
'\nComponent stack:',
|
|
53
|
+
info.componentStack,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
if (this.state.error) {
|
|
59
|
+
// Render a visible red indicator in __DEV__ so it's obvious
|
|
60
|
+
if (__DEV__) {
|
|
61
|
+
return (
|
|
62
|
+
<View style={{ flex: 1, backgroundColor: 'rgba(255,0,0,0.3)', justifyContent: 'center', alignItems: 'center', padding: 20 }}>
|
|
63
|
+
<Text style={{ color: 'white', fontSize: 14, textAlign: 'center' }}>
|
|
64
|
+
{`Overlay '${this.props.overlayName}' crashed:\n${this.state.error.message}`}
|
|
65
|
+
</Text>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return this.props.children;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Raw props received from native appProperties (set once per item in configure()). */
|
|
76
|
+
interface RawOverlaySurfaceProps {
|
|
77
|
+
surfaceId?: string;
|
|
78
|
+
item?: string;
|
|
79
|
+
isActive?: boolean;
|
|
80
|
+
playerState?: string;
|
|
81
|
+
isMuted?: boolean;
|
|
82
|
+
playbackRate?: number;
|
|
83
|
+
captionsEnabled?: boolean;
|
|
84
|
+
activeCue?: string;
|
|
85
|
+
feedScrollPhase?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface InnerProps extends RawOverlaySurfaceProps {
|
|
89
|
+
overlayName: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to a native overlay event, filtered by surfaceId.
|
|
94
|
+
*
|
|
95
|
+
* Both iOS and Android (new arch) emit events through the codegen
|
|
96
|
+
* EventEmitter path (mEventEmitterCallback / JSI direct channel).
|
|
97
|
+
* We subscribe via the codegen emitter method on the TurboModule.
|
|
98
|
+
*/
|
|
99
|
+
function useOverlayEvent<T extends { surfaceId: string }>(
|
|
100
|
+
eventName: string,
|
|
101
|
+
surfaceId: string | undefined,
|
|
102
|
+
handler: (event: T) => void,
|
|
103
|
+
) {
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!surfaceId) return;
|
|
106
|
+
|
|
107
|
+
let sub: { remove: () => void } | undefined;
|
|
108
|
+
|
|
109
|
+
// Both platforms: events come through the codegen EventEmitterCallback.
|
|
110
|
+
// Access the codegen emitter method by name on the TurboModule.
|
|
111
|
+
const emitter = NativeShortKitModule?.[eventName as keyof typeof NativeShortKitModule];
|
|
112
|
+
if (typeof emitter === 'function') {
|
|
113
|
+
sub = (emitter as (cb: (e: T) => void) => { remove: () => void })((e: T) => {
|
|
114
|
+
if (e.surfaceId !== surfaceId) return;
|
|
115
|
+
handler(e);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return () => sub?.remove();
|
|
120
|
+
}, [surfaceId]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ShortKitOverlaySurfaceInner(props: InnerProps) {
|
|
124
|
+
const Component = _overlayRegistry.get(props.overlayName);
|
|
125
|
+
if (!Component) {
|
|
126
|
+
console.error(`${SK_OVERLAY_TAG} ShortKitOverlaySurfaceInner: Component NOT FOUND in registry for '${props.overlayName}'. Registered names: [${Array.from(_overlayRegistry.keys()).join(', ')}]`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const [item, setItem] = useState(() =>
|
|
131
|
+
props.item ? deserializeContentItem(props.item) : null,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Sync item from props when surface is first mounted or remounted via updateInitProps
|
|
135
|
+
const prevPropsItemRef = React.useRef(props.item);
|
|
136
|
+
if (props.item !== prevPropsItemRef.current) {
|
|
137
|
+
prevPropsItemRef.current = props.item;
|
|
138
|
+
// Synchronous state update during render — React restarts with new state
|
|
139
|
+
// before committing, so stale frame is never painted.
|
|
140
|
+
setItem(props.item ? deserializeContentItem(props.item) : null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sid = props.surfaceId;
|
|
144
|
+
|
|
145
|
+
// Initialize state from surface properties (set once in configure()).
|
|
146
|
+
// All subsequent updates arrive via events filtered by surfaceId.
|
|
147
|
+
const [isActive, setIsActive] = useState(props.isActive ?? false);
|
|
148
|
+
const [playerState, setPlayerState] = useState<PlayerState>(
|
|
149
|
+
(props.playerState ?? 'idle') as PlayerState,
|
|
150
|
+
);
|
|
151
|
+
const [isMuted, setIsMuted] = useState(props.isMuted ?? true);
|
|
152
|
+
const [playbackRate, setPlaybackRate] = useState(props.playbackRate ?? 1);
|
|
153
|
+
const [captionsEnabled, setCaptionsEnabled] = useState(
|
|
154
|
+
props.captionsEnabled ?? false,
|
|
155
|
+
);
|
|
156
|
+
const [time, setTime] = useState<PlayerTime>({
|
|
157
|
+
current: 0,
|
|
158
|
+
duration: 0,
|
|
159
|
+
buffered: 0,
|
|
160
|
+
});
|
|
161
|
+
const [activeCue, setActiveCue] = useState<OverlayProps['activeCue']>(
|
|
162
|
+
props.activeCue ? JSON.parse(props.activeCue) : null,
|
|
163
|
+
);
|
|
164
|
+
const [feedScrollPhase, setFeedScrollPhase] =
|
|
165
|
+
useState<FeedScrollPhase | null>(
|
|
166
|
+
props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Reset state when the item changes (cell reuse). useState initial values
|
|
170
|
+
// only apply on mount, so on re-render with new props we must sync manually.
|
|
171
|
+
// This prevents stale progress bar / player state flashing from the previous cell.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
setIsActive(props.isActive ?? false);
|
|
174
|
+
setPlayerState((props.playerState ?? 'idle') as PlayerState);
|
|
175
|
+
setTime({ current: 0, duration: 0, buffered: 0 });
|
|
176
|
+
setIsMuted(props.isMuted ?? true);
|
|
177
|
+
setPlaybackRate(props.playbackRate ?? 1);
|
|
178
|
+
setCaptionsEnabled(props.captionsEnabled ?? false);
|
|
179
|
+
setActiveCue(props.activeCue ? JSON.parse(props.activeCue) : null);
|
|
180
|
+
setFeedScrollPhase(
|
|
181
|
+
props.feedScrollPhase ? JSON.parse(props.feedScrollPhase) : null,
|
|
182
|
+
);
|
|
183
|
+
}, [props.item]);
|
|
184
|
+
|
|
185
|
+
// --- Event subscriptions (filtered by surfaceId) ---
|
|
186
|
+
|
|
187
|
+
// Item changed via event (not updateInitProps) — triggers React DIFF
|
|
188
|
+
// instead of full remount. Uses DeviceEventEmitter because this event
|
|
189
|
+
// is not in the TurboModule spec (avoids native codegen changes).
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!sid) return;
|
|
192
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
193
|
+
const sub = DeviceEventEmitter.addListener('onOverlayItemChanged', (e: { surfaceId: string; item: string }) => {
|
|
194
|
+
if (e.surfaceId !== sid) return;
|
|
195
|
+
const newItem = e.item ? deserializeContentItem(e.item) : null;
|
|
196
|
+
if (newItem) setItem(newItem);
|
|
197
|
+
// Reset playback state for the new item
|
|
198
|
+
setIsActive(false);
|
|
199
|
+
setPlayerState('idle' as PlayerState);
|
|
200
|
+
setTime({ current: 0, duration: 0, buffered: 0 });
|
|
201
|
+
});
|
|
202
|
+
return () => sub.remove();
|
|
203
|
+
}, [sid]);
|
|
204
|
+
|
|
205
|
+
useOverlayEvent<{ surfaceId: string; isActive: boolean }>(
|
|
206
|
+
'onOverlayActiveChanged', sid,
|
|
207
|
+
(e) => setIsActive(e.isActive),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
useOverlayEvent<{ surfaceId: string; playerState: string }>(
|
|
211
|
+
'onOverlayPlayerStateChanged', sid,
|
|
212
|
+
(e) => setPlayerState(e.playerState as PlayerState),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
useOverlayEvent<{ surfaceId: string; isMuted: boolean }>(
|
|
216
|
+
'onOverlayMutedChanged', sid,
|
|
217
|
+
(e) => setIsMuted(e.isMuted),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
useOverlayEvent<{ surfaceId: string; playbackRate: number }>(
|
|
221
|
+
'onOverlayPlaybackRateChanged', sid,
|
|
222
|
+
(e) => setPlaybackRate(e.playbackRate),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
useOverlayEvent<{ surfaceId: string; captionsEnabled: boolean }>(
|
|
226
|
+
'onOverlayCaptionsEnabledChanged', sid,
|
|
227
|
+
(e) => setCaptionsEnabled(e.captionsEnabled),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
useOverlayEvent<{ surfaceId: string; activeCue: string | null }>(
|
|
231
|
+
'onOverlayActiveCueChanged', sid,
|
|
232
|
+
(e) => setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
useOverlayEvent<{ surfaceId: string; feedScrollPhase: string | null }>(
|
|
236
|
+
'onOverlayFeedScrollPhaseChanged', sid,
|
|
237
|
+
(e) => {
|
|
238
|
+
try {
|
|
239
|
+
const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
|
|
240
|
+
// Stabilize object reference: only update state if the phase actually
|
|
241
|
+
// changed. Prevents unnecessary re-renders of memo'd overlay components
|
|
242
|
+
// that receive feedScrollPhase as a prop (new JSON.parse object !== old).
|
|
243
|
+
setFeedScrollPhase(prev => {
|
|
244
|
+
if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) {
|
|
245
|
+
return prev;
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
});
|
|
249
|
+
} catch {
|
|
250
|
+
setFeedScrollPhase(null);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// EXPERIMENT: time updates disabled to measure impact on swipe perf
|
|
256
|
+
// useOverlayEvent<{ surfaceId: string; current: number; duration: number; buffered: number }>(
|
|
257
|
+
// 'onOverlayTimeUpdate', sid,
|
|
258
|
+
// (e) => setTime({ current: e.current, duration: e.duration, buffered: e.buffered }),
|
|
259
|
+
// );
|
|
260
|
+
|
|
261
|
+
// Batched full-state sync — replaces 7 individual events on swipe settle.
|
|
262
|
+
// All setState calls within one handler = one React render (auto-batched).
|
|
263
|
+
useOverlayEvent<{
|
|
264
|
+
surfaceId: string;
|
|
265
|
+
isActive: boolean;
|
|
266
|
+
playerState: string;
|
|
267
|
+
isMuted: boolean;
|
|
268
|
+
playbackRate: number;
|
|
269
|
+
captionsEnabled: boolean;
|
|
270
|
+
activeCue: string | null;
|
|
271
|
+
feedScrollPhase: string | null;
|
|
272
|
+
}>('onOverlayFullState', sid, (e) => {
|
|
273
|
+
setIsActive(e.isActive);
|
|
274
|
+
setPlayerState(e.playerState as PlayerState);
|
|
275
|
+
setIsMuted(e.isMuted);
|
|
276
|
+
setPlaybackRate(e.playbackRate);
|
|
277
|
+
setCaptionsEnabled(e.captionsEnabled);
|
|
278
|
+
try {
|
|
279
|
+
setActiveCue(e.activeCue ? JSON.parse(e.activeCue) : null);
|
|
280
|
+
} catch {
|
|
281
|
+
setActiveCue(null);
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const next = e.feedScrollPhase ? JSON.parse(e.feedScrollPhase) : null;
|
|
285
|
+
setFeedScrollPhase(prev => {
|
|
286
|
+
if (prev?.phase === next?.phase && prev?.fromId === next?.fromId) return prev;
|
|
287
|
+
return next;
|
|
288
|
+
});
|
|
289
|
+
} catch {
|
|
290
|
+
setFeedScrollPhase(null);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!item) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<OverlayErrorBoundary surfaceId={sid} overlayName={props.overlayName}>
|
|
300
|
+
<Component
|
|
301
|
+
item={item}
|
|
302
|
+
isActive={isActive}
|
|
303
|
+
playerState={playerState}
|
|
304
|
+
time={time}
|
|
305
|
+
isMuted={isMuted}
|
|
306
|
+
playbackRate={playbackRate}
|
|
307
|
+
captionsEnabled={captionsEnabled}
|
|
308
|
+
activeCue={activeCue}
|
|
309
|
+
feedScrollPhase={feedScrollPhase}
|
|
310
|
+
/>
|
|
311
|
+
</OverlayErrorBoundary>
|
|
312
|
+
);
|
|
313
|
+
}
|
package/src/ShortKitPlayer.tsx
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
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
|
|
17
|
-
if (!context) {
|
|
18
|
-
throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
|
|
19
|
-
}
|
|
20
|
+
const clickAction = config?.clickAction ?? 'feed';
|
|
20
21
|
|
|
21
22
|
const serializedConfig = useMemo(() => {
|
|
22
23
|
const cfg = config ?? {};
|
|
23
24
|
return JSON.stringify({
|
|
24
25
|
cornerRadius: cfg.cornerRadius ?? 12,
|
|
25
|
-
clickAction:
|
|
26
|
+
clickAction: clickAction,
|
|
26
27
|
autoplay: cfg.autoplay ?? true,
|
|
27
28
|
loop: cfg.loop ?? true,
|
|
28
29
|
muteOnStart: cfg.muteOnStart ?? true,
|
|
@@ -32,20 +33,40 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
|
32
33
|
: { type: 'custom' }
|
|
33
34
|
: 'none',
|
|
34
35
|
});
|
|
35
|
-
}, [config]);
|
|
36
|
+
}, [config, clickAction]);
|
|
36
37
|
|
|
37
38
|
const serializedItem = useMemo(() => {
|
|
38
39
|
if (!contentItem) return undefined;
|
|
39
40
|
return JSON.stringify(contentItem);
|
|
40
41
|
}, [contentItem]);
|
|
41
42
|
|
|
43
|
+
// When clickAction is "none", the native view should not participate in
|
|
44
|
+
// Fabric's JS-side hit testing. Without this, Fabric on Android sees the
|
|
45
|
+
// native component as the touch target and never routes the touch to a
|
|
46
|
+
// parent Pressable — even though the native view returns false from
|
|
47
|
+
// dispatchTouchEvent. iOS doesn't need this because UIKit's responder
|
|
48
|
+
// chain bubbles unhandled touches automatically.
|
|
49
|
+
//
|
|
50
|
+
// "box-none" on the container means the container View itself is
|
|
51
|
+
// transparent to touches (passes through to parent), but children can
|
|
52
|
+
// still receive touches if needed. "none" on the native view means it
|
|
53
|
+
// is fully invisible to hit testing.
|
|
54
|
+
const nativeTouchPassthrough = clickAction === 'none';
|
|
55
|
+
|
|
42
56
|
return (
|
|
43
|
-
<View
|
|
57
|
+
<View
|
|
58
|
+
style={[styles.container, style]}
|
|
59
|
+
pointerEvents={nativeTouchPassthrough ? 'box-none' : 'auto'}
|
|
60
|
+
onStartShouldSetResponder={() => {
|
|
61
|
+
return false;
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
44
64
|
<ShortKitPlayerView
|
|
45
65
|
style={styles.player}
|
|
46
66
|
config={serializedConfig}
|
|
47
67
|
contentItem={serializedItem}
|
|
48
68
|
active={active}
|
|
69
|
+
pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
|
|
49
70
|
/>
|
|
50
71
|
</View>
|
|
51
72
|
);
|