@shortkitsdk/react-native 0.2.0 → 0.2.1
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/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +117 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
- package/ios/ShortKitBridge.swift +88 -1
- package/ios/ShortKitModule.mm +22 -0
- package/ios/ShortKitPlayerNativeView.swift +186 -0
- package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
- package/ios/ShortKitWidgetNativeView.swift +168 -0
- package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
- package/package.json +1 -1
- package/src/ShortKitContext.ts +5 -0
- package/src/ShortKitFeed.tsx +10 -0
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +43 -0
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +12 -0
- package/src/serialization.ts +10 -0
- package/src/specs/NativeShortKitModule.ts +18 -0
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +78 -0
- package/src/useShortKit.ts +5 -1
package/ios/ShortKitModule.mm
CHANGED
|
@@ -54,6 +54,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
54
54
|
@"onFeedTransition",
|
|
55
55
|
@"onFormatChange",
|
|
56
56
|
@"onPrefetchedAheadCountChanged",
|
|
57
|
+
@"onRemainingContentCountChanged",
|
|
57
58
|
@"onError",
|
|
58
59
|
@"onShareTapped",
|
|
59
60
|
@"onSurveyResponse",
|
|
@@ -64,6 +65,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
64
65
|
@"onOverlayRestore",
|
|
65
66
|
@"onOverlayTap",
|
|
66
67
|
@"onOverlayDoubleTap",
|
|
68
|
+
@"onContentTapped",
|
|
67
69
|
];
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -102,6 +104,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
102
104
|
|
|
103
105
|
RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
104
106
|
config:(NSString *)config
|
|
107
|
+
embedId:(NSString *)embedId
|
|
105
108
|
clientAppName:(NSString *)clientAppName
|
|
106
109
|
clientAppVersion:(NSString *)clientAppVersion
|
|
107
110
|
customDimensions:(NSString *)customDimensions) {
|
|
@@ -110,6 +113,7 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
|
110
113
|
|
|
111
114
|
_shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
|
|
112
115
|
config:config
|
|
116
|
+
embedId:embedId
|
|
113
117
|
clientAppName:clientAppName
|
|
114
118
|
clientAppVersion:clientAppVersion
|
|
115
119
|
customDimensions:customDimensions
|
|
@@ -187,6 +191,24 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
|
|
|
187
191
|
[_shortKitBridge setMaxBitrate:bitrate];
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
// MARK: - Custom Feed
|
|
195
|
+
|
|
196
|
+
RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
|
|
197
|
+
[_shortKitBridge setFeedItems:items];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
|
|
201
|
+
[_shortKitBridge appendFeedItems:items];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
|
|
205
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
206
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
207
|
+
[_shortKitBridge fetchContent:limit completion:^(NSString *json) {
|
|
208
|
+
resolve(json);
|
|
209
|
+
}];
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
// MARK: - New Architecture (TurboModule)
|
|
191
213
|
|
|
192
214
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// Fabric native view wrapping `ShortKitPlayerViewController` for use as
|
|
5
|
+
/// a single-video player in React Native.
|
|
6
|
+
///
|
|
7
|
+
/// Props (set by RCTViewManager):
|
|
8
|
+
/// - `config`: JSON string with PlayerConfig values
|
|
9
|
+
/// - `contentItem`: JSON string with ContentItem data
|
|
10
|
+
@objc public class ShortKitPlayerNativeView: UIView {
|
|
11
|
+
|
|
12
|
+
// MARK: - Props
|
|
13
|
+
|
|
14
|
+
@objc public var config: String? {
|
|
15
|
+
didSet {
|
|
16
|
+
guard config != oldValue else { return }
|
|
17
|
+
applyConfig()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc public var contentItem: String? {
|
|
22
|
+
didSet {
|
|
23
|
+
guard contentItem != oldValue else { return }
|
|
24
|
+
applyContentItem()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public var active: Bool = true {
|
|
29
|
+
didSet {
|
|
30
|
+
guard active != oldValue else { return }
|
|
31
|
+
applyActive()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Child VC
|
|
36
|
+
|
|
37
|
+
private var playerVC: ShortKitPlayerViewController?
|
|
38
|
+
private var parsedConfig: PlayerConfig?
|
|
39
|
+
|
|
40
|
+
// MARK: - Lifecycle
|
|
41
|
+
|
|
42
|
+
public override func didMoveToWindow() {
|
|
43
|
+
super.didMoveToWindow()
|
|
44
|
+
if window != nil {
|
|
45
|
+
embedPlayerVCIfNeeded()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
50
|
+
super.willMove(toWindow: newWindow)
|
|
51
|
+
if newWindow == nil {
|
|
52
|
+
removePlayerVC()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override func layoutSubviews() {
|
|
57
|
+
super.layoutSubviews()
|
|
58
|
+
playerVC?.view.frame = bounds
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MARK: - VC Containment
|
|
62
|
+
|
|
63
|
+
private func embedPlayerVCIfNeeded() {
|
|
64
|
+
guard playerVC == nil else { return }
|
|
65
|
+
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
66
|
+
guard let parentVC = findParentViewController() else { return }
|
|
67
|
+
|
|
68
|
+
let playerConfig = parsedConfig ?? PlayerConfig()
|
|
69
|
+
|
|
70
|
+
let vc = ShortKitPlayerViewController(shortKit: sdk, config: playerConfig)
|
|
71
|
+
self.playerVC = vc
|
|
72
|
+
|
|
73
|
+
// Inject content BEFORE the view loads so loadData() skips its own
|
|
74
|
+
// fetch. Accessing vc.view triggers viewDidLoad, so configure first.
|
|
75
|
+
if let json = contentItem, let item = Self.parseContentItem(json) {
|
|
76
|
+
vc.configure(with: item)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parentVC.addChild(vc)
|
|
80
|
+
vc.view.frame = bounds
|
|
81
|
+
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
82
|
+
addSubview(vc.view)
|
|
83
|
+
vc.didMove(toParent: parentVC)
|
|
84
|
+
|
|
85
|
+
// Activate based on the `active` prop (defaults to true)
|
|
86
|
+
if active {
|
|
87
|
+
vc.activate()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func removePlayerVC() {
|
|
92
|
+
guard let vc = playerVC else { return }
|
|
93
|
+
vc.deactivate()
|
|
94
|
+
vc.willMove(toParent: nil)
|
|
95
|
+
vc.view.removeFromSuperview()
|
|
96
|
+
vc.removeFromParent()
|
|
97
|
+
self.playerVC = nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// MARK: - Prop Application
|
|
101
|
+
|
|
102
|
+
private func applyConfig() {
|
|
103
|
+
guard let json = config else { return }
|
|
104
|
+
parsedConfig = Self.parsePlayerConfig(json)
|
|
105
|
+
// Config changes require re-embedding
|
|
106
|
+
if playerVC != nil {
|
|
107
|
+
removePlayerVC()
|
|
108
|
+
embedPlayerVCIfNeeded()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func applyContentItem() {
|
|
113
|
+
guard let json = contentItem, let item = Self.parseContentItem(json) else { return }
|
|
114
|
+
playerVC?.configure(with: item)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func applyActive() {
|
|
118
|
+
guard let vc = playerVC else { return }
|
|
119
|
+
if active {
|
|
120
|
+
vc.activate()
|
|
121
|
+
} else {
|
|
122
|
+
vc.deactivate()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Parsing
|
|
127
|
+
|
|
128
|
+
private static func parsePlayerConfig(_ json: String) -> PlayerConfig {
|
|
129
|
+
guard let data = json.data(using: .utf8),
|
|
130
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
131
|
+
return PlayerConfig()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
|
|
135
|
+
let autoplay = obj["autoplay"] as? Bool ?? true
|
|
136
|
+
let loop = obj["loop"] as? Bool ?? true
|
|
137
|
+
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
138
|
+
|
|
139
|
+
let clickAction: PlayerClickAction
|
|
140
|
+
switch obj["clickAction"] as? String {
|
|
141
|
+
case "feed": clickAction = .feed
|
|
142
|
+
case "mute": clickAction = .mute
|
|
143
|
+
case "none": clickAction = .none
|
|
144
|
+
default: clickAction = .feed
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let overlayMode: VideoOverlayMode
|
|
148
|
+
if let overlayObj = obj["overlay"] as? [String: Any],
|
|
149
|
+
overlayObj["type"] as? String == "custom" {
|
|
150
|
+
overlayMode = .custom { @Sendable in
|
|
151
|
+
let overlay = ShortKitOverlayBridge()
|
|
152
|
+
overlay.bridge = ShortKitBridge.shared
|
|
153
|
+
return overlay
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
overlayMode = .none
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return PlayerConfig(
|
|
160
|
+
cornerRadius: cornerRadius,
|
|
161
|
+
clickAction: clickAction,
|
|
162
|
+
autoplay: autoplay,
|
|
163
|
+
loop: loop,
|
|
164
|
+
muteOnStart: muteOnStart,
|
|
165
|
+
videoOverlay: overlayMode
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func parseContentItem(_ json: String) -> ContentItem? {
|
|
170
|
+
guard let data = json.data(using: .utf8) else { return nil }
|
|
171
|
+
return try? JSONDecoder().decode(ContentItem.self, from: data)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MARK: - Helpers
|
|
175
|
+
|
|
176
|
+
private func findParentViewController() -> UIViewController? {
|
|
177
|
+
var responder: UIResponder? = self
|
|
178
|
+
while let next = responder?.next {
|
|
179
|
+
if let vc = next as? UIViewController {
|
|
180
|
+
return vc
|
|
181
|
+
}
|
|
182
|
+
responder = next
|
|
183
|
+
}
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
|
|
3
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
4
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
5
|
+
#else
|
|
6
|
+
#import "ShortKitReactNative-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@interface ShortKitPlayerViewManager : RCTViewManager
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation ShortKitPlayerViewManager
|
|
13
|
+
|
|
14
|
+
RCT_EXPORT_MODULE(ShortKitPlayerView)
|
|
15
|
+
|
|
16
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
17
|
+
return YES;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
- (UIView *)view {
|
|
21
|
+
return [[ShortKitPlayerNativeView alloc] init];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(contentItem, NSString)
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
|
|
27
|
+
|
|
28
|
+
@end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// Fabric native view wrapping `ShortKitWidgetViewController` for use as
|
|
5
|
+
/// a horizontal video carousel in React Native.
|
|
6
|
+
///
|
|
7
|
+
/// Props (set by RCTViewManager):
|
|
8
|
+
/// - `config`: JSON string with WidgetConfig values
|
|
9
|
+
/// - `items`: JSON string with ContentItem array
|
|
10
|
+
@objc public class ShortKitWidgetNativeView: UIView {
|
|
11
|
+
|
|
12
|
+
// MARK: - Props
|
|
13
|
+
|
|
14
|
+
@objc public var config: String? {
|
|
15
|
+
didSet {
|
|
16
|
+
guard config != oldValue else { return }
|
|
17
|
+
applyConfig()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc public var items: String? {
|
|
22
|
+
didSet {
|
|
23
|
+
guard items != oldValue else { return }
|
|
24
|
+
applyItems()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MARK: - Child VC
|
|
29
|
+
|
|
30
|
+
private var widgetVC: ShortKitWidgetViewController?
|
|
31
|
+
private var parsedConfig: WidgetConfig?
|
|
32
|
+
|
|
33
|
+
// MARK: - Lifecycle
|
|
34
|
+
|
|
35
|
+
public override func didMoveToWindow() {
|
|
36
|
+
super.didMoveToWindow()
|
|
37
|
+
if window != nil {
|
|
38
|
+
embedWidgetVCIfNeeded()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
43
|
+
super.willMove(toWindow: newWindow)
|
|
44
|
+
if newWindow == nil {
|
|
45
|
+
removeWidgetVC()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public override func layoutSubviews() {
|
|
50
|
+
super.layoutSubviews()
|
|
51
|
+
widgetVC?.view.frame = bounds
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - VC Containment
|
|
55
|
+
|
|
56
|
+
private func embedWidgetVCIfNeeded() {
|
|
57
|
+
guard widgetVC == nil else { return }
|
|
58
|
+
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
59
|
+
guard let parentVC = findParentViewController() else { return }
|
|
60
|
+
|
|
61
|
+
let widgetConfig = parsedConfig ?? WidgetConfig()
|
|
62
|
+
|
|
63
|
+
let vc = ShortKitWidgetViewController(shortKit: sdk, config: widgetConfig)
|
|
64
|
+
self.widgetVC = vc
|
|
65
|
+
|
|
66
|
+
parentVC.addChild(vc)
|
|
67
|
+
vc.view.frame = bounds
|
|
68
|
+
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
69
|
+
addSubview(vc.view)
|
|
70
|
+
vc.didMove(toParent: parentVC)
|
|
71
|
+
|
|
72
|
+
// If items were already set, apply them
|
|
73
|
+
if let json = items, let contentItems = Self.parseContentItems(json) {
|
|
74
|
+
vc.configure(with: contentItems)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func removeWidgetVC() {
|
|
79
|
+
guard let vc = widgetVC else { return }
|
|
80
|
+
vc.willMove(toParent: nil)
|
|
81
|
+
vc.view.removeFromSuperview()
|
|
82
|
+
vc.removeFromParent()
|
|
83
|
+
self.widgetVC = nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Prop Application
|
|
87
|
+
|
|
88
|
+
private func applyConfig() {
|
|
89
|
+
guard let json = config else { return }
|
|
90
|
+
parsedConfig = Self.parseWidgetConfig(json)
|
|
91
|
+
if widgetVC != nil {
|
|
92
|
+
removeWidgetVC()
|
|
93
|
+
embedWidgetVCIfNeeded()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func applyItems() {
|
|
98
|
+
guard let json = items, let contentItems = Self.parseContentItems(json) else { return }
|
|
99
|
+
widgetVC?.configure(with: contentItems)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - Parsing
|
|
103
|
+
|
|
104
|
+
private static func parseWidgetConfig(_ json: String) -> WidgetConfig {
|
|
105
|
+
guard let data = json.data(using: .utf8),
|
|
106
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
107
|
+
return WidgetConfig()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cardCount = obj["cardCount"] as? Int ?? 3
|
|
111
|
+
let cardSpacing = obj["cardSpacing"] as? CGFloat ?? 8
|
|
112
|
+
let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
|
|
113
|
+
let autoplay = obj["autoplay"] as? Bool ?? true
|
|
114
|
+
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
115
|
+
let loop = obj["loop"] as? Bool ?? true
|
|
116
|
+
let rotationInterval = obj["rotationInterval"] as? TimeInterval ?? 10000
|
|
117
|
+
|
|
118
|
+
let clickAction: PlayerClickAction
|
|
119
|
+
switch obj["clickAction"] as? String {
|
|
120
|
+
case "feed": clickAction = .feed
|
|
121
|
+
case "mute": clickAction = .mute
|
|
122
|
+
case "none": clickAction = .none
|
|
123
|
+
default: clickAction = .feed
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let overlayMode: VideoOverlayMode
|
|
127
|
+
if let overlayObj = obj["overlay"] as? [String: Any],
|
|
128
|
+
overlayObj["type"] as? String == "custom" {
|
|
129
|
+
overlayMode = .custom { @Sendable in
|
|
130
|
+
let overlay = ShortKitOverlayBridge()
|
|
131
|
+
overlay.bridge = ShortKitBridge.shared
|
|
132
|
+
return overlay
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
overlayMode = .none
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return WidgetConfig(
|
|
139
|
+
cardCount: cardCount,
|
|
140
|
+
cardSpacing: cardSpacing,
|
|
141
|
+
cornerRadius: cornerRadius,
|
|
142
|
+
autoplay: autoplay,
|
|
143
|
+
muteOnStart: muteOnStart,
|
|
144
|
+
loop: loop,
|
|
145
|
+
rotationInterval: rotationInterval / 1000.0, // JS sends ms, iOS expects seconds
|
|
146
|
+
clickAction: clickAction,
|
|
147
|
+
cardOverlay: overlayMode
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static func parseContentItems(_ json: String) -> [ContentItem]? {
|
|
152
|
+
guard let data = json.data(using: .utf8) else { return nil }
|
|
153
|
+
return try? JSONDecoder().decode([ContentItem].self, from: data)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// MARK: - Helpers
|
|
157
|
+
|
|
158
|
+
private func findParentViewController() -> UIViewController? {
|
|
159
|
+
var responder: UIResponder? = self
|
|
160
|
+
while let next = responder?.next {
|
|
161
|
+
if let vc = next as? UIViewController {
|
|
162
|
+
return vc
|
|
163
|
+
}
|
|
164
|
+
responder = next
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
|
|
3
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
4
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
5
|
+
#else
|
|
6
|
+
#import "ShortKitReactNative-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@interface ShortKitWidgetViewManager : RCTViewManager
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation ShortKitWidgetViewManager
|
|
13
|
+
|
|
14
|
+
RCT_EXPORT_MODULE(ShortKitWidgetView)
|
|
15
|
+
|
|
16
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
17
|
+
return YES;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
- (UIView *)view {
|
|
21
|
+
return [[ShortKitWidgetNativeView alloc] init];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(items, NSString)
|
|
26
|
+
|
|
27
|
+
@end
|
package/package.json
CHANGED
package/src/ShortKitContext.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
ContentItem,
|
|
4
|
+
CustomFeedItem,
|
|
4
5
|
PlayerTime,
|
|
5
6
|
PlayerState,
|
|
6
7
|
CaptionTrack,
|
|
@@ -20,6 +21,7 @@ export interface ShortKitContextValue {
|
|
|
20
21
|
activeCaptionTrack: CaptionTrack | null;
|
|
21
22
|
activeCue: { text: string; startTime: number; endTime: number } | null;
|
|
22
23
|
prefetchedAheadCount: number;
|
|
24
|
+
remainingContentCount: number;
|
|
23
25
|
isActive: boolean;
|
|
24
26
|
isTransitioning: boolean;
|
|
25
27
|
lastOverlayTap: number;
|
|
@@ -42,6 +44,9 @@ export interface ShortKitContextValue {
|
|
|
42
44
|
// SDK operations
|
|
43
45
|
setUserId: (id: string) => void;
|
|
44
46
|
clearUserId: () => void;
|
|
47
|
+
setFeedItems: (items: CustomFeedItem[]) => void;
|
|
48
|
+
appendFeedItems: (items: CustomFeedItem[]) => void;
|
|
49
|
+
fetchContent: (limit?: number) => Promise<ContentItem[]>;
|
|
45
50
|
|
|
46
51
|
// Internal — used by ShortKitFeed to render custom overlays
|
|
47
52
|
/** @internal */
|
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -29,6 +29,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
29
29
|
onLoop,
|
|
30
30
|
onFeedTransition,
|
|
31
31
|
onFormatChange,
|
|
32
|
+
onContentTapped,
|
|
32
33
|
} = props;
|
|
33
34
|
|
|
34
35
|
const context = useContext(ShortKitContext);
|
|
@@ -109,6 +110,14 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
109
110
|
);
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
if (onContentTapped) {
|
|
114
|
+
subscriptions.push(
|
|
115
|
+
NativeShortKitModule.onContentTapped((event) => {
|
|
116
|
+
onContentTapped(event.contentId, event.index);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
return () => {
|
|
113
122
|
for (const sub of subscriptions) {
|
|
114
123
|
sub.remove();
|
|
@@ -121,6 +130,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
121
130
|
onLoop,
|
|
122
131
|
onFeedTransition,
|
|
123
132
|
onFormatChange,
|
|
133
|
+
onContentTapped,
|
|
124
134
|
]);
|
|
125
135
|
|
|
126
136
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { ShortKitPlayerProps } from './types';
|
|
4
|
+
import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
|
|
5
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Single-video player component. Displays one video with thumbnail fallback
|
|
9
|
+
* and optional overlay. Wraps a native Fabric view.
|
|
10
|
+
*
|
|
11
|
+
* Must be rendered inside a `<ShortKitProvider>`.
|
|
12
|
+
*/
|
|
13
|
+
export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
14
|
+
const { config, contentItem, active, style } = props;
|
|
15
|
+
|
|
16
|
+
const context = useContext(ShortKitContext);
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const serializedConfig = useMemo(() => {
|
|
22
|
+
const cfg = config ?? {};
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
cornerRadius: cfg.cornerRadius ?? 12,
|
|
25
|
+
clickAction: cfg.clickAction ?? 'feed',
|
|
26
|
+
autoplay: cfg.autoplay ?? true,
|
|
27
|
+
loop: cfg.loop ?? true,
|
|
28
|
+
muteOnStart: cfg.muteOnStart ?? true,
|
|
29
|
+
overlay: cfg.overlay
|
|
30
|
+
? typeof cfg.overlay === 'string'
|
|
31
|
+
? cfg.overlay
|
|
32
|
+
: { type: 'custom' }
|
|
33
|
+
: 'none',
|
|
34
|
+
});
|
|
35
|
+
}, [config]);
|
|
36
|
+
|
|
37
|
+
const serializedItem = useMemo(() => {
|
|
38
|
+
if (!contentItem) return undefined;
|
|
39
|
+
return JSON.stringify(contentItem);
|
|
40
|
+
}, [contentItem]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={[styles.container, style]}>
|
|
44
|
+
<ShortKitPlayerView
|
|
45
|
+
style={styles.player}
|
|
46
|
+
config={serializedConfig}
|
|
47
|
+
contentItem={serializedItem}
|
|
48
|
+
active={active}
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
container: {
|
|
56
|
+
overflow: 'hidden',
|
|
57
|
+
},
|
|
58
|
+
player: {
|
|
59
|
+
flex: 1,
|
|
60
|
+
},
|
|
61
|
+
});
|