@shortkitsdk/react-native 0.2.36 → 0.2.37

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.
Binary file
@@ -95,6 +95,51 @@ class ShortKitBridge(
95
95
  }
96
96
  }
97
97
 
98
+ /**
99
+ * Serialize a [FeedItem] to a ContentItem-shaped JSON string for the
100
+ * `onFeedTransition` event. For [FeedItem.Content] cells this is the
101
+ * real ContentItem. For every other cell kind (AdSlotItem,
102
+ * ImageCarousel, Survey, VideoCarousel) there is no playable
103
+ * ContentItem, so we synthesize a minimal one whose ONLY meaningful
104
+ * field is `id`.
105
+ *
106
+ * Why: JS hosts use `event.to` as an identity cursor for resume-on-
107
+ * tab-return (`setFeedItems(startAt:)`). Before this synthesis, non-
108
+ * content cells came through as `to=null` and the host's stored
109
+ * resume id stayed pinned to the last regular video — causing the
110
+ * SDK to re-seed the feed at the wrong index on tab return.
111
+ *
112
+ * Scope: this is a bridge-only synthesis. It never flows back into
113
+ * the Android SDK (pool, cache, FeedDataSource, etc. all still see
114
+ * the real FeedItem and treat non-content cells honestly). JS-side
115
+ * consumers should ONLY read `.id` / `.playbackId` from
116
+ * `onFeedTransition` items — deeper fields are placeholders for
117
+ * non-content cells.
118
+ *
119
+ * Mirrors iOS PR #170 ShortKitBridge.swift:770-790.
120
+ */
121
+ fun serializeFeedItemIdentityJSON(feedItem: com.shortkit.sdk.model.FeedItem): String {
122
+ if (feedItem is com.shortkit.sdk.model.FeedItem.Content) {
123
+ return serializeContentItemToJSON(feedItem.item)
124
+ }
125
+ val synthetic = ContentItem(
126
+ id = feedItem.id,
127
+ playbackId = null,
128
+ title = "",
129
+ description = null,
130
+ duration = 0.0,
131
+ streamingUrl = "",
132
+ thumbnailUrl = "",
133
+ captionTracks = emptyList(),
134
+ customMetadata = null,
135
+ author = null,
136
+ articleUrl = null,
137
+ commentCount = null,
138
+ fallbackUrl = null,
139
+ )
140
+ return serializeContentItemToJSON(synthetic)
141
+ }
142
+
98
143
  /**
99
144
  * Serialize a [ContentItem] to a JSON string for bridge transport.
100
145
  */
@@ -687,6 +732,38 @@ class ShortKitBridge(
687
732
  fragment.onVideoCarouselCellTap = { payload ->
688
733
  emitVideoCarouselCellTap(payload.id, payload.index, payload.pageIndex)
689
734
  }
735
+ // Per-feed transition event. Fires once per transition, bound to this
736
+ // fragment. Replaces the global player.feedTransition.collect pattern
737
+ // (which fanned every feed's transitions out to every mounted
738
+ // <ShortKitFeed>, causing cross-feed state pollution). Structural fix:
739
+ // callback literally cannot fire on the wrong feed. Mirrors iOS
740
+ // PR #170 ShortKitBridge.swift:117-143.
741
+ fragment.onFeedTransition = { event ->
742
+ val params = Arguments.createMap().apply {
743
+ putString("feedId", id)
744
+ putString("phase", when (event.phase) {
745
+ FeedTransitionPhase.BEGAN -> "began"
746
+ FeedTransitionPhase.ENDED -> "ended"
747
+ })
748
+ putString("direction", when (event.direction) {
749
+ FeedDirection.FORWARD -> "forward"
750
+ FeedDirection.BACKWARD -> "backward"
751
+ else -> "forward"
752
+ })
753
+ // Serialize from the underlying FeedItem (not event.from/to
754
+ // which are null for non-content cells). See the docstring on
755
+ // serializeFeedItemIdentityJSON for the why — in short, this
756
+ // ensures carousels/ads/surveys come through with a populated
757
+ // id, so JS-side hosts can track feed position for any cell type.
758
+ event.fromFeedItem?.let {
759
+ putString("fromItem", serializeFeedItemIdentityJSON(it))
760
+ }
761
+ event.toFeedItem?.let {
762
+ putString("toItem", serializeFeedItemIdentityJSON(it))
763
+ }
764
+ }
765
+ emitEventOnMain("onFeedTransition", params)
766
+ }
690
767
 
691
768
  // Replay buffered operations after the fragment's view is created.
692
769
  // commitNowAllowingStateLoss triggers onCreate but onViewCreated
@@ -1240,10 +1317,16 @@ class ShortKitBridge(
1240
1317
  }
1241
1318
  }
1242
1319
 
1243
- // Did loop
1320
+ // Did loop — tagged with active surface's feedId. player.didLoop is a
1321
+ // singleton flow on the shared ShortKitPlayer (only one item plays at
1322
+ // a time). The event semantically belongs to whichever feed owns the
1323
+ // active surface; tag with that feedId so JS consumers bound to
1324
+ // specific feeds can filter. Empty string means no active surface
1325
+ // (treat as unknown/global). Mirrors iOS bridge :615-631.
1244
1326
  newScope.launch {
1245
1327
  player.didLoop.collect { event ->
1246
1328
  val params = Arguments.createMap().apply {
1329
+ putString("feedId", activeSurfaceFeedId())
1247
1330
  putString("contentId", event.contentId)
1248
1331
  putInt("loopCount", event.loopCount)
1249
1332
  }
@@ -1251,34 +1334,26 @@ class ShortKitBridge(
1251
1334
  }
1252
1335
  }
1253
1336
 
1254
- // Feed transition
1255
- newScope.launch {
1256
- player.feedTransition.collect { event ->
1257
- val params = Arguments.createMap().apply {
1258
- putString("phase", when (event.phase) {
1259
- FeedTransitionPhase.BEGAN -> "began"
1260
- FeedTransitionPhase.ENDED -> "ended"
1261
- })
1262
- putString("direction", when (event.direction) {
1263
- FeedDirection.FORWARD -> "forward"
1264
- FeedDirection.BACKWARD -> "backward"
1265
- else -> "forward"
1266
- })
1267
- if (event.from != null) {
1268
- putString("fromItem", serializeContentItemToJSON(event.from!!))
1269
- }
1270
- if (event.to != null) {
1271
- putString("toItem", serializeContentItemToJSON(event.to!!))
1272
- }
1273
- }
1274
- emitEvent.invoke("onFeedTransition", params)
1275
- }
1276
- }
1277
-
1278
- // Format change
1337
+ // NOTE: the global `player.feedTransition.collect` subscription used
1338
+ // to live here. It's been removed — feed transitions are now emitted
1339
+ // to RN via the per-fragment `fragment.onFeedTransition` closure
1340
+ // wired in `registerFeedFragment` above. This eliminates the cross-
1341
+ // feed routing bug where every mounted <ShortKitFeed> consumer
1342
+ // received every feed's transitions. Mirrors iOS PR #170
1343
+ // ShortKitBridge.swift:633-642.
1344
+ //
1345
+ // `player.sendFeedTransition` is still called by ShortKitFeedFragment
1346
+ // for backward compatibility with native Kotlin SDK consumers that
1347
+ // subscribe to `ShortKitPlayer.feedTransition` directly.
1348
+
1349
+ // Format change — tagged with active surface's feedId. Same reasoning
1350
+ // as onDidLoop: fires from shared player singleton, but semantically
1351
+ // belongs to whichever feed owns the active surface. Mirrors iOS
1352
+ // bridge :671-688.
1279
1353
  newScope.launch {
1280
1354
  player.formatChange.collect { event ->
1281
1355
  val params = Arguments.createMap().apply {
1356
+ putString("feedId", activeSurfaceFeedId())
1282
1357
  putString("contentId", event.contentId)
1283
1358
  putDouble("fromBitrate", event.fromBitrate.toDouble())
1284
1359
  putDouble("toBitrate", event.toBitrate.toDouble())
@@ -148,11 +148,12 @@ import ShortKitSDK
148
148
  "playerState": "idle",
149
149
  "isMuted": cachedIsMuted,
150
150
  ]
151
- if let firstVideo = item.videos.first,
152
- let videoData = try? JSONEncoder().encode(firstVideo),
151
+ let initialIndex = item.clampedInitialPageIndex
152
+ if item.videos.indices.contains(initialIndex),
153
+ let videoData = try? JSONEncoder().encode(item.videos[initialIndex]),
153
154
  let videoJSON = String(data: videoData, encoding: .utf8) {
154
155
  props["activeVideo"] = videoJSON
155
- props["activeVideoIndex"] = 0
156
+ props["activeVideoIndex"] = initialIndex
156
157
  }
157
158
  return props
158
159
  }
@@ -160,16 +161,17 @@ import ShortKitSDK
160
161
  /// Emit onVideoCarouselItemChanged with full initial state for the new item.
161
162
  /// Replaces setProperties() on cell reuse, avoiding a full Fabric root remount.
162
163
  private func emitItemChanged(item: VideoCarouselItem, json: String) {
164
+ let initialIndex = item.clampedInitialPageIndex
163
165
  var body: [String: Any] = [
164
166
  "surfaceId": surfaceId,
165
167
  "carouselItem": json,
166
168
  "isActive": false,
167
169
  "playerState": "idle",
168
170
  "isMuted": cachedIsMuted,
169
- "activeVideoIndex": 0,
171
+ "activeVideoIndex": initialIndex,
170
172
  ]
171
- if let firstVideo = item.videos.first,
172
- let videoData = try? JSONEncoder().encode(firstVideo),
173
+ if item.videos.indices.contains(initialIndex),
174
+ let videoData = try? JSONEncoder().encode(item.videos[initialIndex]),
173
175
  let videoJSON = String(data: videoData, encoding: .utf8) {
174
176
  body["activeVideo"] = videoJSON
175
177
  } else {
@@ -11,9 +11,9 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>0.2.36</string>
14
+ <string>0.2.37</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.36</string>
16
+ <string>0.2.37</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.36\""
53520
+ "value": "\"0.2.37\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -10,11 +10,11 @@
10
10
  </data>
11
11
  <key>Info.plist</key>
12
12
  <data>
13
- WQrFujyFbopBKGGWbEKoC1woXeM=
13
+ Rmlks05t/ZAWDJyrlYCLZI2hLt4=
14
14
  </data>
15
15
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json</key>
16
16
  <data>
17
- mwN55YBG0lFbYyqh8rFLlhydUhg=
17
+ o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
18
18
  </data>
19
19
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
20
20
  <data>
@@ -50,7 +50,7 @@
50
50
  <dict>
51
51
  <key>hash2</key>
52
52
  <data>
53
- lPV80dTyQRi3R5Al7WxpvZYLMH1fLtDMRPj8kH++RCs=
53
+ GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
54
54
  </data>
55
55
  </dict>
56
56
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
@@ -11,9 +11,9 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>0.2.36</string>
14
+ <string>0.2.37</string>
15
15
  <key>CFBundleShortVersionString</key>
16
- <string>0.2.36</string>
16
+ <string>0.2.37</string>
17
17
  <key>MinimumOSVersion</key>
18
18
  <string>16.0</string>
19
19
  </dict>
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.36\""
53520
+ "value": "\"0.2.37\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -53517,7 +53517,7 @@
53517
53517
  "kind": "StringLiteral",
53518
53518
  "offset": 154,
53519
53519
  "length": 8,
53520
- "value": "\"0.2.36\""
53520
+ "value": "\"0.2.37\""
53521
53521
  },
53522
53522
  {
53523
53523
  "filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
@@ -10,11 +10,11 @@
10
10
  </data>
11
11
  <key>Info.plist</key>
12
12
  <data>
13
- WQrFujyFbopBKGGWbEKoC1woXeM=
13
+ Rmlks05t/ZAWDJyrlYCLZI2hLt4=
14
14
  </data>
15
15
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
16
16
  <data>
17
- mwN55YBG0lFbYyqh8rFLlhydUhg=
17
+ o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
18
18
  </data>
19
19
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
20
20
  <data>
@@ -30,7 +30,7 @@
30
30
  </data>
31
31
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
32
32
  <data>
33
- mwN55YBG0lFbYyqh8rFLlhydUhg=
33
+ o4q5PMYrrSkfEuV3X5DKLlP/Ic8=
34
34
  </data>
35
35
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
36
36
  <data>
@@ -66,7 +66,7 @@
66
66
  <dict>
67
67
  <key>hash2</key>
68
68
  <data>
69
- lPV80dTyQRi3R5Al7WxpvZYLMH1fLtDMRPj8kH++RCs=
69
+ GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
70
70
  </data>
71
71
  </dict>
72
72
  <key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
@@ -94,7 +94,7 @@
94
94
  <dict>
95
95
  <key>hash2</key>
96
96
  <data>
97
- lPV80dTyQRi3R5Al7WxpvZYLMH1fLtDMRPj8kH++RCs=
97
+ GPD6LqlhW6OfAta/UbVY9JS6SMSbk402F6pveuST4rY=
98
98
  </data>
99
99
  </dict>
100
100
  <key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/react-native",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "ShortKit React Native SDK — short-form video feed",
5
5
  "react-native": "src/index",
6
6
  "source": "src/index",