@shortkitsdk/react-native 0.2.31 → 0.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/libs/shortkit-release.aar +0 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +26 -5
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +19 -5
- package/ios/FeedMaskHostView.swift +190 -0
- package/ios/ShortKitBridge.swift +111 -3
- package/ios/ShortKitModule.mm +5 -1
- package/ios/ShortKitPlayerNativeView.swift +31 -0
- package/ios/ShortKitPlayerNativeViewManager.mm +1 -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 +3060 -259
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +84 -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 +84 -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 +3060 -259
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +84 -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 +84 -7
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +3060 -259
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +84 -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 +84 -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/ios/ShortKitWidgetNativeView.swift +153 -6
- package/ios/ShortKitWidgetNativeViewManager.mm +2 -0
- package/package.json +1 -1
- package/src/ShortKitCommands.ts +12 -2
- package/src/ShortKitContext.ts +7 -1
- package/src/ShortKitFeedMaskSurface.tsx +132 -0
- package/src/ShortKitPlayer.tsx +15 -2
- package/src/ShortKitWidget.tsx +16 -2
- package/src/index.ts +8 -1
- package/src/serialization.ts +15 -0
- package/src/specs/NativeShortKitModule.ts +2 -1
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +7 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +15 -0
- package/src/types.ts +99 -0
|
@@ -31,11 +31,40 @@ import ShortKitSDK
|
|
|
31
31
|
/// one is mounted simultaneously.
|
|
32
32
|
@objc public var widgetId: String?
|
|
33
33
|
|
|
34
|
+
/// Mirrors `ShortKitWidgetViewController.active`. Hosts use this to
|
|
35
|
+
/// release the widget's pool tile when it shouldn't be playing
|
|
36
|
+
/// (e.g. inactive tab, scrolled off-screen). Defaults to `true` so
|
|
37
|
+
/// existing host integrations behave identically to before.
|
|
38
|
+
@objc public var active: Bool = true {
|
|
39
|
+
didSet {
|
|
40
|
+
guard active != oldValue else { return }
|
|
41
|
+
widgetVC?.active = active
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// JSON-serialized `FeedInput[]` used to seed the expanded feed when
|
|
46
|
+
/// a card is tapped and `clickAction == .feed`. See
|
|
47
|
+
/// `ShortKitWidgetProps.feedItems` in the JS layer for full
|
|
48
|
+
/// semantics. The parsed list is forwarded to the SDK via
|
|
49
|
+
/// `ShortKitWidgetViewController.setFeedItems(_:)`.
|
|
50
|
+
@objc public var feedItems: String? {
|
|
51
|
+
didSet {
|
|
52
|
+
guard feedItems != oldValue else { return }
|
|
53
|
+
applyFeedItems()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
// MARK: - Child VC
|
|
35
58
|
|
|
36
59
|
private var widgetVC: ShortKitWidgetViewController?
|
|
37
60
|
private var parsedConfig: WidgetConfig?
|
|
38
61
|
private var parsedItems: [WidgetInput] = []
|
|
62
|
+
private var parsedFeedItems: [FeedInput] = []
|
|
63
|
+
/// Parsed feedMask mode pulled out of the serialized config JSON in
|
|
64
|
+
/// `applyConfig()`. Stored separately because the SDK's
|
|
65
|
+
/// `ShortKitWidgetViewController.feedMask` is a top-level property,
|
|
66
|
+
/// not part of `WidgetConfig`.
|
|
67
|
+
private var parsedFeedMask: FeedMaskMode = .none
|
|
39
68
|
|
|
40
69
|
// MARK: - Lifecycle
|
|
41
70
|
|
|
@@ -49,7 +78,17 @@ import ShortKitSDK
|
|
|
49
78
|
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
50
79
|
super.willMove(toWindow: newWindow)
|
|
51
80
|
if newWindow == nil {
|
|
52
|
-
|
|
81
|
+
// The host UIView is leaving the window — typically a
|
|
82
|
+
// transient detach driven by `react-native-screens` when
|
|
83
|
+
// a tab blurs or a screen is pushed on top. We must NOT
|
|
84
|
+
// destroy the widget VC here; doing so causes the user
|
|
85
|
+
// to see the host's background through the widget on
|
|
86
|
+
// return, plus a fresh full reload of cards / players /
|
|
87
|
+
// HLS streams. Suspend instead: detach from the parent
|
|
88
|
+
// VC hierarchy + pause playback, but keep the VC and
|
|
89
|
+
// all its loaded state alive so we can re-attach in
|
|
90
|
+
// `didMoveToWindow` instantly.
|
|
91
|
+
suspendWidgetVC()
|
|
53
92
|
}
|
|
54
93
|
}
|
|
55
94
|
|
|
@@ -58,18 +97,62 @@ import ShortKitSDK
|
|
|
58
97
|
widgetVC?.view.frame = bounds
|
|
59
98
|
}
|
|
60
99
|
|
|
100
|
+
deinit {
|
|
101
|
+
// Final teardown when React truly unmounts the bridge UIView.
|
|
102
|
+
// The instance is going away, so it's safe to release the
|
|
103
|
+
// child VC's parent-chain references here. Without this, a
|
|
104
|
+
// suspended widget VC would leak (its parent VC still holds
|
|
105
|
+
// it as a child until the parent itself is deallocated).
|
|
106
|
+
destroyWidgetVC()
|
|
107
|
+
}
|
|
108
|
+
|
|
61
109
|
// MARK: - VC Containment
|
|
62
110
|
|
|
63
111
|
private func embedWidgetVCIfNeeded() {
|
|
64
|
-
guard widgetVC == nil else { return }
|
|
65
|
-
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
66
112
|
guard let parentVC = findParentViewController() else { return }
|
|
67
113
|
|
|
114
|
+
// Re-attach a suspended VC. All loaded state — items, player
|
|
115
|
+
// pool checkouts, collection-view layout, scroll position —
|
|
116
|
+
// is preserved across a suspend/resume cycle, so the user
|
|
117
|
+
// sees the same widget reappear in place rather than a fresh
|
|
118
|
+
// load. Mirrors `ShortKitPlayerNativeView`.
|
|
119
|
+
if let vc = widgetVC {
|
|
120
|
+
parentVC.addChild(vc)
|
|
121
|
+
vc.view.frame = bounds
|
|
122
|
+
addSubview(vc.view)
|
|
123
|
+
vc.didMove(toParent: parentVC)
|
|
124
|
+
// Re-sync the active prop. While suspended we forced the
|
|
125
|
+
// VC inactive; restore whatever the JS prop currently says.
|
|
126
|
+
vc.active = active
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// First mount — create a fresh VC.
|
|
131
|
+
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
132
|
+
|
|
68
133
|
let widgetConfig = parsedConfig ?? WidgetConfig()
|
|
69
134
|
|
|
70
135
|
// Pass items at construction so the VC never races the server fetch —
|
|
71
136
|
// analogous to the feed's `feedItems` prop wiring.
|
|
72
137
|
let vc = ShortKitWidgetViewController(shortKit: sdk, config: widgetConfig, items: parsedItems)
|
|
138
|
+
// Sync the active prop to the freshly-created VC. Without this, a
|
|
139
|
+
// widget that mounts already with active=false (e.g. on an
|
|
140
|
+
// unfocused tab) would still go through its initial-load path and
|
|
141
|
+
// start rotating before the prop change reaches it.
|
|
142
|
+
vc.active = active
|
|
143
|
+
// Forward the optional expanded-feed seed list. Set before view
|
|
144
|
+
// load so it's available the first time `openFeed()` runs —
|
|
145
|
+
// same ordering rationale as the player's bridge.
|
|
146
|
+
if !parsedFeedItems.isEmpty {
|
|
147
|
+
vc.setFeedItems(parsedFeedItems)
|
|
148
|
+
}
|
|
149
|
+
// Forward the optional host-supplied feed mask. The SDK's
|
|
150
|
+
// `feedMask` lives as a top-level VC property (not in
|
|
151
|
+
// WidgetConfig), so it has to be set after construction. The
|
|
152
|
+
// factory closure inside the mode is invoked by the SDK each
|
|
153
|
+
// time `openFeed()` fires — so each expansion gets its own
|
|
154
|
+
// FeedMaskHostView instance.
|
|
155
|
+
vc.feedMask = parsedFeedMask
|
|
73
156
|
self.widgetVC = vc
|
|
74
157
|
|
|
75
158
|
// Wire host-handleable card-tap callback. When the JS wrapper has
|
|
@@ -96,7 +179,25 @@ import ShortKitSDK
|
|
|
96
179
|
vc.didMove(toParent: parentVC)
|
|
97
180
|
}
|
|
98
181
|
|
|
99
|
-
|
|
182
|
+
/// Detach the widget VC from the parent VC + view hierarchy
|
|
183
|
+
/// without destroying it. Called when the bridge UIView leaves
|
|
184
|
+
/// the window (typically a `react-native-screens` tab blur).
|
|
185
|
+
/// All of the widget's loaded state is preserved so the
|
|
186
|
+
/// re-attach in `embedWidgetVCIfNeeded` can be instant. Pause
|
|
187
|
+
/// playback so the SDK pool tiles aren't held while suspended.
|
|
188
|
+
private func suspendWidgetVC() {
|
|
189
|
+
guard let vc = widgetVC else { return }
|
|
190
|
+
vc.active = false
|
|
191
|
+
vc.willMove(toParent: nil)
|
|
192
|
+
vc.view.removeFromSuperview()
|
|
193
|
+
vc.removeFromParent()
|
|
194
|
+
// Keep widgetVC reference — re-attached in embedWidgetVCIfNeeded.
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Full teardown — called from `deinit` when React unmounts the
|
|
198
|
+
/// native view, and from `applyConfig` when the config string
|
|
199
|
+
/// changes and we need to rebuild the VC from scratch.
|
|
200
|
+
private func destroyWidgetVC() {
|
|
100
201
|
guard let vc = widgetVC else { return }
|
|
101
202
|
vc.willMove(toParent: nil)
|
|
102
203
|
vc.view.removeFromSuperview()
|
|
@@ -109,12 +210,37 @@ import ShortKitSDK
|
|
|
109
210
|
private func applyConfig() {
|
|
110
211
|
guard let json = config else { return }
|
|
111
212
|
parsedConfig = Self.parseWidgetConfig(json)
|
|
213
|
+
// FeedMask is stored as a sibling field in the serialized
|
|
214
|
+
// config JSON; extract it here so it's available when the VC
|
|
215
|
+
// gets (re)constructed in embedWidgetVCIfNeeded.
|
|
216
|
+
parsedFeedMask = Self.extractFeedMask(json)
|
|
112
217
|
if widgetVC != nil {
|
|
113
|
-
|
|
218
|
+
destroyWidgetVC()
|
|
114
219
|
embedWidgetVCIfNeeded()
|
|
115
220
|
}
|
|
116
221
|
}
|
|
117
222
|
|
|
223
|
+
/// Pulls the `feedMask` field out of the raw config JSON without
|
|
224
|
+
/// re-parsing the rest of `WidgetConfig`. Returns `.none` if the
|
|
225
|
+
/// JSON is malformed or omits `feedMask`. Mirrors how the
|
|
226
|
+
/// equivalent value is read on `ShortKitPlayerNativeView`.
|
|
227
|
+
private static func extractFeedMask(_ json: String) -> FeedMaskMode {
|
|
228
|
+
guard let data = json.data(using: .utf8),
|
|
229
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
230
|
+
return .none
|
|
231
|
+
}
|
|
232
|
+
// The serializer emits feedMask either as the string "none" or
|
|
233
|
+
// as a {type, name} object — both arrive nested as a sub-JSON
|
|
234
|
+
// value (not stringified again, unlike feedConfig).
|
|
235
|
+
if let mask = obj["feedMask"] {
|
|
236
|
+
if let data = try? JSONSerialization.data(withJSONObject: mask),
|
|
237
|
+
let json = String(data: data, encoding: .utf8) {
|
|
238
|
+
return ShortKitBridge.parseFeedMask(json)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return .none
|
|
242
|
+
}
|
|
243
|
+
|
|
118
244
|
private func applyItems() {
|
|
119
245
|
parsedItems = items.flatMap { Self.parseWidgetInputs($0) } ?? []
|
|
120
246
|
// Post-mount updates: push items through the public API.
|
|
@@ -123,6 +249,18 @@ import ShortKitSDK
|
|
|
123
249
|
}
|
|
124
250
|
}
|
|
125
251
|
|
|
252
|
+
private func applyFeedItems() {
|
|
253
|
+
// Empty / nil JSON ⇒ clear the host seed.
|
|
254
|
+
guard let json = feedItems,
|
|
255
|
+
let items = ShortKitBridge.parseFeedInputs(json) else {
|
|
256
|
+
parsedFeedItems = []
|
|
257
|
+
widgetVC?.setFeedItems([])
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
parsedFeedItems = items
|
|
261
|
+
widgetVC?.setFeedItems(items)
|
|
262
|
+
}
|
|
263
|
+
|
|
126
264
|
// MARK: - Parsing
|
|
127
265
|
|
|
128
266
|
private static func parseWidgetConfig(_ json: String) -> WidgetConfig {
|
|
@@ -138,6 +276,13 @@ import ShortKitSDK
|
|
|
138
276
|
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
139
277
|
let loop = obj["loop"] as? Bool ?? true
|
|
140
278
|
let rotationInterval = obj["rotationInterval"] as? TimeInterval ?? 10000
|
|
279
|
+
let previewDuration = obj["previewDuration"] as? TimeInterval ?? 5
|
|
280
|
+
let playbackMode: WidgetPlaybackMode
|
|
281
|
+
switch obj["playbackMode"] as? String {
|
|
282
|
+
case "allVisibleSimultaneous": playbackMode = .allVisibleSimultaneous
|
|
283
|
+
case "singleVisibleRotating": playbackMode = .singleVisibleRotating
|
|
284
|
+
default: playbackMode = .singleVisibleRotating
|
|
285
|
+
}
|
|
141
286
|
|
|
142
287
|
let clickAction: PlayerClickAction
|
|
143
288
|
switch obj["clickAction"] as? String {
|
|
@@ -180,7 +325,9 @@ import ShortKitSDK
|
|
|
180
325
|
clickAction: clickAction,
|
|
181
326
|
cardOverlay: overlayMode,
|
|
182
327
|
feedConfig: feedConfig,
|
|
183
|
-
filter: filter
|
|
328
|
+
filter: filter,
|
|
329
|
+
playbackMode: playbackMode,
|
|
330
|
+
previewDuration: previewDuration
|
|
184
331
|
)
|
|
185
332
|
}
|
|
186
333
|
|
|
@@ -24,5 +24,7 @@ RCT_EXPORT_MODULE(ShortKitWidgetView)
|
|
|
24
24
|
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
25
25
|
RCT_EXPORT_VIEW_PROPERTY(items, NSString)
|
|
26
26
|
RCT_EXPORT_VIEW_PROPERTY(widgetId, NSString)
|
|
27
|
+
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
|
|
28
|
+
RCT_EXPORT_VIEW_PROPERTY(feedItems, NSString)
|
|
27
29
|
|
|
28
30
|
@end
|
package/package.json
CHANGED
package/src/ShortKitCommands.ts
CHANGED
|
@@ -32,7 +32,17 @@ export const ShortKitCommands = {
|
|
|
32
32
|
NativeShortKitModule?.prefetchStoryboard(playbackId),
|
|
33
33
|
getStoryboardData: (playbackId: string): Promise<string | null> =>
|
|
34
34
|
NativeShortKitModule?.getStoryboardData(playbackId) ?? Promise.resolve(null),
|
|
35
|
-
downloadVideo: (
|
|
36
|
-
|
|
35
|
+
downloadVideo: (
|
|
36
|
+
itemId: string,
|
|
37
|
+
options?: {
|
|
38
|
+
mode?: 'interruptive' | 'nonInterruptive';
|
|
39
|
+
overlayMode?: 'none' | 'static' | 'deterministic';
|
|
40
|
+
}
|
|
41
|
+
): Promise<string> => {
|
|
42
|
+
const mode = options?.mode ?? 'nonInterruptive';
|
|
43
|
+
const overlayMode = options?.overlayMode ?? 'none';
|
|
44
|
+
return NativeShortKitModule?.downloadVideo(itemId, mode, overlayMode)
|
|
45
|
+
?? Promise.reject(new Error('ShortKit not initialized'));
|
|
46
|
+
},
|
|
37
47
|
cancelDownload: () => NativeShortKitModule?.cancelDownload(),
|
|
38
48
|
} as const;
|
package/src/ShortKitContext.ts
CHANGED
|
@@ -55,7 +55,13 @@ export interface ShortKitContextValue {
|
|
|
55
55
|
|
|
56
56
|
// Download
|
|
57
57
|
downloadState: DownloadState;
|
|
58
|
-
downloadVideo: (
|
|
58
|
+
downloadVideo: (
|
|
59
|
+
itemId: string,
|
|
60
|
+
options?: {
|
|
61
|
+
mode?: 'interruptive' | 'nonInterruptive';
|
|
62
|
+
overlayMode?: 'none' | 'static' | 'deterministic';
|
|
63
|
+
}
|
|
64
|
+
) => Promise<string>;
|
|
59
65
|
cancelDownload: () => void;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AppRegistry, View, Text } from 'react-native';
|
|
3
|
+
import type { FeedMaskProps, ContentItem } from './types';
|
|
4
|
+
import { deserializeContentItem } from './serialization';
|
|
5
|
+
|
|
6
|
+
const SK_MASK_TAG = '[ShortKit:FeedMaskSurface]';
|
|
7
|
+
|
|
8
|
+
// Named registry — supports different mask components per host site.
|
|
9
|
+
const _maskRegistry = new Map<string, React.ComponentType<FeedMaskProps>>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a named feed-mask component for rendering around the
|
|
13
|
+
* expanded feed. Called by `<ShortKitPlayer>` / `<ShortKitWidget>` on
|
|
14
|
+
* mount via `useLayoutEffect` whenever `config.feedMask` is set with
|
|
15
|
+
* `type: 'custom'`. Idempotent — registering the same name twice is
|
|
16
|
+
* a no-op.
|
|
17
|
+
*/
|
|
18
|
+
export function registerFeedMaskComponent(
|
|
19
|
+
name: string,
|
|
20
|
+
component: React.ComponentType<FeedMaskProps>,
|
|
21
|
+
) {
|
|
22
|
+
if (_maskRegistry.has(name)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
_maskRegistry.set(name, component);
|
|
26
|
+
|
|
27
|
+
const moduleName = `ShortKitFeedMask_${name}`;
|
|
28
|
+
AppRegistry.registerComponent(moduleName, () => {
|
|
29
|
+
return function NamedFeedMaskSurface(props: RawFeedMaskSurfaceProps) {
|
|
30
|
+
return <ShortKitFeedMaskSurfaceInner {...props} maskName={name} />;
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Error boundary mirrors the overlay-surface boundary so a crash in
|
|
37
|
+
* the mask component doesn't bring down the SDK's modal presentation.
|
|
38
|
+
*/
|
|
39
|
+
class FeedMaskErrorBoundary extends React.Component<
|
|
40
|
+
{ maskName: string; children: React.ReactNode },
|
|
41
|
+
{ error: Error | null }
|
|
42
|
+
> {
|
|
43
|
+
state: { error: Error | null } = { error: null };
|
|
44
|
+
|
|
45
|
+
static getDerivedStateFromError(error: Error) {
|
|
46
|
+
return { error };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
50
|
+
console.error(
|
|
51
|
+
`${SK_MASK_TAG} CRASH in mask '${this.props.maskName}':`,
|
|
52
|
+
error.message,
|
|
53
|
+
'\nComponent stack:',
|
|
54
|
+
info.componentStack,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render() {
|
|
59
|
+
if (this.state.error) {
|
|
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
|
+
{`Mask '${this.props.maskName}' crashed:\n${this.state.error.message}`}
|
|
65
|
+
</Text>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
// Production fallback: render an empty view so the SDK still
|
|
70
|
+
// has a usable feedRegion via the embed slot the host inserts
|
|
71
|
+
// (which they no longer do, since the mask crashed). The SDK's
|
|
72
|
+
// placeholder UIView keeps the feed visible.
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return this.props.children;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Raw props pushed in by the native bridge (`FeedMaskHostView`)
|
|
81
|
+
* via `SKFabricSurfaceWrapper.setProperties`. Item arrives serialized
|
|
82
|
+
* as a JSON string; everything else is a primitive.
|
|
83
|
+
*/
|
|
84
|
+
interface RawFeedMaskSurfaceProps {
|
|
85
|
+
/** Serialized `ContentItem`. JSON-stringified by the bridge. */
|
|
86
|
+
item?: string;
|
|
87
|
+
/** Active item's index in the feed, when known. */
|
|
88
|
+
activeIndex?: number;
|
|
89
|
+
/** Total feed length, when known. */
|
|
90
|
+
totalCount?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface InnerProps extends RawFeedMaskSurfaceProps {
|
|
94
|
+
maskName: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ShortKitFeedMaskSurfaceInner(props: InnerProps) {
|
|
98
|
+
const Component = _maskRegistry.get(props.maskName);
|
|
99
|
+
if (!Component) {
|
|
100
|
+
if (__DEV__) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`${SK_MASK_TAG} mask '${props.maskName}' is not registered — did you set config.feedMask?`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let item: ContentItem | null = null;
|
|
109
|
+
if (props.item) {
|
|
110
|
+
try {
|
|
111
|
+
item = deserializeContentItem(props.item);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.warn(`${SK_MASK_TAG} failed to deserialize item:`, e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// No item to render around → render nothing yet. The SDK will push
|
|
118
|
+
// a configure() with the active item shortly after expansion.
|
|
119
|
+
if (!item) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<FeedMaskErrorBoundary maskName={props.maskName}>
|
|
125
|
+
<Component
|
|
126
|
+
item={item}
|
|
127
|
+
activeIndex={props.activeIndex}
|
|
128
|
+
totalCount={props.totalCount}
|
|
129
|
+
/>
|
|
130
|
+
</FeedMaskErrorBoundary>
|
|
131
|
+
);
|
|
132
|
+
}
|
package/src/ShortKitPlayer.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { serializePlayerConfig } from './serialization';
|
|
|
6
6
|
import { registerOverlayComponent } from './ShortKitOverlaySurface';
|
|
7
7
|
import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
|
|
8
8
|
import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
|
|
9
|
+
import { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Single-video player component. Displays one video with thumbnail fallback
|
|
@@ -19,7 +20,7 @@ import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOv
|
|
|
19
20
|
* app's responsibility.
|
|
20
21
|
*/
|
|
21
22
|
export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
22
|
-
const { config, contentItem, active, style } = props;
|
|
23
|
+
const { config, contentItem, active, feedItems, style } = props;
|
|
23
24
|
|
|
24
25
|
const clickAction = config?.clickAction ?? 'feed';
|
|
25
26
|
|
|
@@ -39,7 +40,10 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
|
39
40
|
registerVideoCarouselOverlayComponent(fc.videoCarouselOverlay.name, fc.videoCarouselOverlay.component);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
if (config?.feedMask && config.feedMask !== 'none') {
|
|
44
|
+
registerFeedMaskComponent(config.feedMask.name, config.feedMask.component);
|
|
45
|
+
}
|
|
46
|
+
}, [config?.overlay, config?.feedConfig, config?.feedMask]);
|
|
43
47
|
|
|
44
48
|
const serializedConfig = useMemo(() => {
|
|
45
49
|
return serializePlayerConfig(config ?? {});
|
|
@@ -50,6 +54,14 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
|
50
54
|
return JSON.stringify(contentItem);
|
|
51
55
|
}, [contentItem]);
|
|
52
56
|
|
|
57
|
+
// Serialize the optional expanded-feed seed list. Empty/undefined ⇒
|
|
58
|
+
// pass `undefined` so the native side falls through to the legacy
|
|
59
|
+
// single-item behavior (preload built from `self.items`).
|
|
60
|
+
const serializedFeedItems = useMemo(() => {
|
|
61
|
+
if (!feedItems || feedItems.length === 0) return undefined;
|
|
62
|
+
return JSON.stringify(feedItems);
|
|
63
|
+
}, [feedItems]);
|
|
64
|
+
|
|
53
65
|
// When clickAction is "none", the native view should not participate in
|
|
54
66
|
// Fabric's JS-side hit testing. Without this, Fabric on Android sees the
|
|
55
67
|
// native component as the touch target and never routes the touch to a
|
|
@@ -76,6 +88,7 @@ export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
|
76
88
|
config={serializedConfig}
|
|
77
89
|
contentItem={serializedItem}
|
|
78
90
|
active={active}
|
|
91
|
+
feedItems={serializedFeedItems}
|
|
79
92
|
pointerEvents={nativeTouchPassthrough ? 'none' : 'auto'}
|
|
80
93
|
/>
|
|
81
94
|
</View>
|
package/src/ShortKitWidget.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { serializeWidgetConfig } from './serialization';
|
|
|
8
8
|
import { registerOverlayComponent } from './ShortKitOverlaySurface';
|
|
9
9
|
import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
|
|
10
10
|
import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
|
|
11
|
+
import { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
|
|
11
12
|
|
|
12
13
|
// Local UUID generator. We don't pull in a runtime dep just for this; a
|
|
13
14
|
// timestamp + random suffix is more than unique enough to disambiguate
|
|
@@ -23,7 +24,7 @@ function generateWidgetId(): string {
|
|
|
23
24
|
* Must be rendered inside a `<ShortKitProvider>`.
|
|
24
25
|
*/
|
|
25
26
|
export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
26
|
-
const { config, items, onCardTap, style } = props;
|
|
27
|
+
const { config, items, onCardTap, active, feedItems, style } = props;
|
|
27
28
|
|
|
28
29
|
const isInitialized = useContext(ShortKitInitContext);
|
|
29
30
|
if (!isInitialized) {
|
|
@@ -69,7 +70,10 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
|
69
70
|
registerVideoCarouselOverlayComponent(fc.videoCarouselOverlay.name, fc.videoCarouselOverlay.component);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
if (config?.feedMask && config.feedMask !== 'none') {
|
|
74
|
+
registerFeedMaskComponent(config.feedMask.name, config.feedMask.component);
|
|
75
|
+
}
|
|
76
|
+
}, [config?.overlay, config?.feedConfig, config?.feedMask]);
|
|
73
77
|
|
|
74
78
|
// Subscribe to the global widget-card-tap event, filtered to this
|
|
75
79
|
// instance's widgetId.
|
|
@@ -93,6 +97,14 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
|
93
97
|
return JSON.stringify(items);
|
|
94
98
|
}, [items]);
|
|
95
99
|
|
|
100
|
+
// Serialize the optional expanded-feed seed list. Empty/undefined ⇒
|
|
101
|
+
// pass `undefined` so the native side falls through to the legacy
|
|
102
|
+
// behavior (preload built from `self.items` — the carousel cards).
|
|
103
|
+
const serializedFeedItems = useMemo(() => {
|
|
104
|
+
if (!feedItems || feedItems.length === 0) return undefined;
|
|
105
|
+
return JSON.stringify(feedItems);
|
|
106
|
+
}, [feedItems]);
|
|
107
|
+
|
|
96
108
|
return (
|
|
97
109
|
<View style={[styles.container, style]}>
|
|
98
110
|
<ShortKitWidgetView
|
|
@@ -100,6 +112,8 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
|
100
112
|
config={serializedConfig}
|
|
101
113
|
items={serializedItems}
|
|
102
114
|
widgetId={widgetId}
|
|
115
|
+
active={active}
|
|
116
|
+
feedItems={serializedFeedItems}
|
|
103
117
|
/>
|
|
104
118
|
</View>
|
|
105
119
|
);
|
package/src/index.ts
CHANGED
|
@@ -52,4 +52,11 @@ export { default as NativeShortKitModule } from './specs/NativeShortKitModule';
|
|
|
52
52
|
export { registerOverlayComponent } from './ShortKitOverlaySurface';
|
|
53
53
|
export { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
|
|
54
54
|
export { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
|
|
55
|
-
export
|
|
55
|
+
export { registerFeedMaskComponent } from './ShortKitFeedMaskSurface';
|
|
56
|
+
export type {
|
|
57
|
+
OverlayProps,
|
|
58
|
+
CarouselOverlayProps,
|
|
59
|
+
VideoCarouselOverlayProps,
|
|
60
|
+
FeedMaskProps,
|
|
61
|
+
FeedMaskConfig,
|
|
62
|
+
} from './types';
|
package/src/serialization.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
FeedConfig,
|
|
3
3
|
ContentItem,
|
|
4
4
|
FeedInput,
|
|
5
|
+
FeedMaskConfig,
|
|
5
6
|
PlayerState,
|
|
6
7
|
PlayerTime,
|
|
7
8
|
WidgetConfig,
|
|
@@ -139,6 +140,16 @@ function serializeOverlay(overlay: OverlayConfig | undefined): string | { type:
|
|
|
139
140
|
return { type: 'custom', name: overlay.name };
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* FeedMask serialization mirrors the overlay path: only the name
|
|
145
|
+
* crosses the bridge — the JS-side registry maps the name back to
|
|
146
|
+
* the React component at mount time inside `FeedMaskHostView`.
|
|
147
|
+
*/
|
|
148
|
+
function serializeFeedMask(mask: FeedMaskConfig | undefined): string | { type: 'custom'; name: string } {
|
|
149
|
+
if (!mask || mask === 'none') return 'none';
|
|
150
|
+
return { type: 'custom', name: mask.name };
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
export function serializeWidgetConfig(config: WidgetConfig): string {
|
|
143
154
|
const cfg = config ?? {};
|
|
144
155
|
return JSON.stringify({
|
|
@@ -153,6 +164,9 @@ export function serializeWidgetConfig(config: WidgetConfig): string {
|
|
|
153
164
|
overlay: serializeOverlay(cfg.overlay),
|
|
154
165
|
filter: cfg.filter ? JSON.stringify(cfg.filter) : undefined,
|
|
155
166
|
feedConfig: cfg.feedConfig ? serializeFeedConfig(cfg.feedConfig) : undefined,
|
|
167
|
+
feedMask: serializeFeedMask(cfg.feedMask),
|
|
168
|
+
playbackMode: cfg.playbackMode ?? 'singleVisibleRotating',
|
|
169
|
+
previewDuration: cfg.previewDuration ?? 5,
|
|
156
170
|
});
|
|
157
171
|
}
|
|
158
172
|
|
|
@@ -166,5 +180,6 @@ export function serializePlayerConfig(config: PlayerConfig): string {
|
|
|
166
180
|
muteOnStart: cfg.muteOnStart ?? true,
|
|
167
181
|
overlay: serializeOverlay(cfg.overlay),
|
|
168
182
|
feedConfig: cfg.feedConfig ? serializeFeedConfig(cfg.feedConfig) : undefined,
|
|
183
|
+
feedMask: serializeFeedMask(cfg.feedMask),
|
|
169
184
|
});
|
|
170
185
|
}
|
|
@@ -140,6 +140,7 @@ type DownloadStartedEvent = Readonly<{
|
|
|
140
140
|
type DownloadProgressEvent = Readonly<{
|
|
141
141
|
itemId: string;
|
|
142
142
|
progress: number;
|
|
143
|
+
phase: 'downloading' | 'compositing' | 'finalizing';
|
|
143
144
|
}>;
|
|
144
145
|
|
|
145
146
|
type DownloadCompletedEvent = Readonly<{
|
|
@@ -306,7 +307,7 @@ export interface Spec extends TurboModule {
|
|
|
306
307
|
getStoryboardData(playbackId: string): Promise<string>;
|
|
307
308
|
|
|
308
309
|
// --- Download ---
|
|
309
|
-
downloadVideo(itemId: string, mode: string): Promise<string>;
|
|
310
|
+
downloadVideo(itemId: string, mode: string, overlayMode: string): Promise<string>;
|
|
310
311
|
cancelDownload(): void;
|
|
311
312
|
|
|
312
313
|
// --- Carousel accessors ---
|
|
@@ -5,6 +5,13 @@ export interface NativeProps extends ViewProps {
|
|
|
5
5
|
config: string;
|
|
6
6
|
contentItem?: string;
|
|
7
7
|
active?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Serialized JSON of the host-provided expanded-feed seed list.
|
|
10
|
+
* See `ShortKitPlayerProps.feedItems` for semantics. Sent as a
|
|
11
|
+
* string to keep the prop primitive (Codegen-friendly) — the
|
|
12
|
+
* native bridge parses + hydrates into `[ContentItem]`.
|
|
13
|
+
*/
|
|
14
|
+
feedItems?: string;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
export default codegenNativeComponent<NativeProps>(
|
|
@@ -10,6 +10,21 @@ export interface NativeProps extends ViewProps {
|
|
|
10
10
|
* `<ShortKitWidget>` when multiple widgets are mounted.
|
|
11
11
|
*/
|
|
12
12
|
widgetId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the widget is currently allowed to play. Defaults to true on
|
|
15
|
+
* the native side. Hosts use this to release the widget's pool tile
|
|
16
|
+
* when it shouldn't be playing (inactive tab, scrolled off-screen,
|
|
17
|
+
* etc.) so other surfaces can claim the tile cleanly. Mirrors
|
|
18
|
+
* `<ShortKitFeed>`'s `active` prop.
|
|
19
|
+
*/
|
|
20
|
+
active?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Serialized JSON of the host-provided expanded-feed seed list.
|
|
23
|
+
* See `ShortKitWidgetProps.feedItems` for semantics. Sent as a
|
|
24
|
+
* string to keep the prop primitive (Codegen-friendly) — the
|
|
25
|
+
* native bridge parses + hydrates into `[FeedInput]`.
|
|
26
|
+
*/
|
|
27
|
+
feedItems?: string;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
export default codegenNativeComponent<NativeProps>(
|