@xhub-reels/sdk 0.2.11 → 0.2.13
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/README.md +91 -13
- package/dist/index.cjs +767 -57
- package/dist/index.d.cts +222 -4
- package/dist/index.d.ts +222 -4
- package/dist/index.js +763 -59
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ var vanilla = require('zustand/vanilla');
|
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
var Hls = require('hls.js');
|
|
7
|
+
var reactDom = require('react-dom');
|
|
7
8
|
|
|
8
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
10
|
|
|
@@ -106,6 +107,7 @@ var PlayerEngine = class {
|
|
|
106
107
|
this.circuitResetTimer = null;
|
|
107
108
|
this.watchTimeInterval = null;
|
|
108
109
|
this.lastStatus = "idle" /* IDLE */;
|
|
110
|
+
this._destroyed = false;
|
|
109
111
|
this.config = { ...DEFAULT_PLAYER_CONFIG, ...config };
|
|
110
112
|
this.analytics = analytics;
|
|
111
113
|
this.logger = logger;
|
|
@@ -315,8 +317,13 @@ var PlayerEngine = class {
|
|
|
315
317
|
this.lastStatus = "idle" /* IDLE */;
|
|
316
318
|
}
|
|
317
319
|
destroy() {
|
|
320
|
+
if (this._destroyed) return;
|
|
321
|
+
this._destroyed = true;
|
|
318
322
|
this.stopWatchTime();
|
|
319
|
-
if (this.circuitResetTimer)
|
|
323
|
+
if (this.circuitResetTimer) {
|
|
324
|
+
clearTimeout(this.circuitResetTimer);
|
|
325
|
+
this.circuitResetTimer = null;
|
|
326
|
+
}
|
|
320
327
|
this.listeners.clear();
|
|
321
328
|
}
|
|
322
329
|
// ═══════════════════════════════════════════
|
|
@@ -419,6 +426,8 @@ var FeedManager = class {
|
|
|
419
426
|
this.inFlightRequests = /* @__PURE__ */ new Map();
|
|
420
427
|
/** LRU tracking: itemId → lastAccessTime */
|
|
421
428
|
this.accessOrder = /* @__PURE__ */ new Map();
|
|
429
|
+
/** Idempotency guard for destroy() */
|
|
430
|
+
this._destroyed = false;
|
|
422
431
|
/** Prefetch cache — instance-scoped (not static) */
|
|
423
432
|
this.prefetchCache = null;
|
|
424
433
|
this.config = { ...DEFAULT_FEED_CONFIG, ...config };
|
|
@@ -444,6 +453,13 @@ var FeedManager = class {
|
|
|
444
453
|
// ═══════════════════════════════════════════
|
|
445
454
|
// PUBLIC API — Prefetch
|
|
446
455
|
// ═══════════════════════════════════════════
|
|
456
|
+
setInitialItems(items) {
|
|
457
|
+
this.prefetchCache = {
|
|
458
|
+
items,
|
|
459
|
+
nextCursor: null,
|
|
460
|
+
timestamp: Date.now()
|
|
461
|
+
};
|
|
462
|
+
}
|
|
447
463
|
async prefetch(ttlMs) {
|
|
448
464
|
if (this.prefetchCache) {
|
|
449
465
|
const ttl = ttlMs ?? this.config.staleTTL;
|
|
@@ -524,7 +540,10 @@ var FeedManager = class {
|
|
|
524
540
|
// PUBLIC API — Lifecycle
|
|
525
541
|
// ═══════════════════════════════════════════
|
|
526
542
|
destroy() {
|
|
543
|
+
if (this._destroyed) return;
|
|
544
|
+
this._destroyed = true;
|
|
527
545
|
this.abortController?.abort();
|
|
546
|
+
this.abortController = null;
|
|
528
547
|
this.inFlightRequests.clear();
|
|
529
548
|
this.accessOrder.clear();
|
|
530
549
|
this.prefetchCache = null;
|
|
@@ -593,7 +612,13 @@ var FeedManager = class {
|
|
|
593
612
|
applyItems(incoming, _nextCursor, append) {
|
|
594
613
|
const { itemsById, displayOrder } = this.store.getState();
|
|
595
614
|
const nextById = new Map(itemsById);
|
|
615
|
+
const seenInBatch = /* @__PURE__ */ new Set();
|
|
616
|
+
const deduped = [];
|
|
596
617
|
for (const item of incoming) {
|
|
618
|
+
if (!seenInBatch.has(item.id)) {
|
|
619
|
+
seenInBatch.add(item.id);
|
|
620
|
+
deduped.push(item);
|
|
621
|
+
}
|
|
597
622
|
nextById.set(item.id, item);
|
|
598
623
|
this.accessOrder.set(item.id, Date.now());
|
|
599
624
|
}
|
|
@@ -601,14 +626,14 @@ var FeedManager = class {
|
|
|
601
626
|
if (append) {
|
|
602
627
|
const existingIds = new Set(displayOrder);
|
|
603
628
|
const newIds = [];
|
|
604
|
-
for (const item of
|
|
629
|
+
for (const item of deduped) {
|
|
605
630
|
if (!existingIds.has(item.id)) {
|
|
606
631
|
newIds.push(item.id);
|
|
607
632
|
}
|
|
608
633
|
}
|
|
609
634
|
nextOrder = [...displayOrder, ...newIds];
|
|
610
635
|
} else {
|
|
611
|
-
nextOrder =
|
|
636
|
+
nextOrder = deduped.map((item) => item.id);
|
|
612
637
|
}
|
|
613
638
|
if (nextById.size > this.config.maxCacheSize) {
|
|
614
639
|
this.evictLRU(nextById, nextOrder);
|
|
@@ -650,6 +675,8 @@ var OptimisticManager = class {
|
|
|
650
675
|
this.likeDebounceTimers = /* @__PURE__ */ new Map();
|
|
651
676
|
/** Pending like direction: contentId → final intended state */
|
|
652
677
|
this.pendingLikeState = /* @__PURE__ */ new Map();
|
|
678
|
+
/** Idempotency guard for destroy() */
|
|
679
|
+
this._destroyed = false;
|
|
653
680
|
this.interaction = interaction;
|
|
654
681
|
this.logger = logger;
|
|
655
682
|
this.store = vanilla.createStore(createInitialState3);
|
|
@@ -740,6 +767,8 @@ var OptimisticManager = class {
|
|
|
740
767
|
// PUBLIC API — Lifecycle
|
|
741
768
|
// ═══════════════════════════════════════════
|
|
742
769
|
destroy() {
|
|
770
|
+
if (this._destroyed) return;
|
|
771
|
+
this._destroyed = true;
|
|
743
772
|
for (const timer of this.likeDebounceTimers.values()) {
|
|
744
773
|
clearTimeout(timer);
|
|
745
774
|
}
|
|
@@ -761,6 +790,7 @@ var DEFAULT_RESOURCE_CONFIG = {
|
|
|
761
790
|
var ResourceGovernor = class {
|
|
762
791
|
constructor(config = {}, videoLoader, network, logger) {
|
|
763
792
|
this.focusDebounceTimer = null;
|
|
793
|
+
this._destroyed = false;
|
|
764
794
|
this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
|
|
765
795
|
this.videoLoader = videoLoader;
|
|
766
796
|
this.network = network;
|
|
@@ -791,11 +821,19 @@ var ResourceGovernor = class {
|
|
|
791
821
|
this.logger?.debug("[ResourceGovernor] Activated");
|
|
792
822
|
}
|
|
793
823
|
deactivate() {
|
|
824
|
+
if (this._destroyed) return;
|
|
825
|
+
if (!this.store.getState().isActive) return;
|
|
794
826
|
this.networkUnsubscribe?.();
|
|
795
|
-
|
|
827
|
+
this.networkUnsubscribe = void 0;
|
|
828
|
+
if (this.focusDebounceTimer) {
|
|
829
|
+
clearTimeout(this.focusDebounceTimer);
|
|
830
|
+
this.focusDebounceTimer = null;
|
|
831
|
+
}
|
|
796
832
|
this.store.setState({ isActive: false });
|
|
797
833
|
}
|
|
798
834
|
destroy() {
|
|
835
|
+
if (this._destroyed) return;
|
|
836
|
+
this._destroyed = true;
|
|
799
837
|
this.deactivate();
|
|
800
838
|
this.videoLoader?.clearAll();
|
|
801
839
|
}
|
|
@@ -919,6 +957,153 @@ var ResourceGovernor = class {
|
|
|
919
957
|
);
|
|
920
958
|
}
|
|
921
959
|
};
|
|
960
|
+
var DEFAULT_NAVIGATION_CONFIG = {
|
|
961
|
+
phaseTimeoutMs: 1200
|
|
962
|
+
};
|
|
963
|
+
var VALID_NAV_TRANSITIONS = {
|
|
964
|
+
closed: ["opening"],
|
|
965
|
+
// Allow opening → closing so a fast user can dismiss mid-open.
|
|
966
|
+
opening: ["open", "closing", "closed"],
|
|
967
|
+
open: ["closing"],
|
|
968
|
+
// Allow closing → opening so re-opening during the exit animation works.
|
|
969
|
+
closing: ["closed", "opening"]
|
|
970
|
+
};
|
|
971
|
+
function isValidNavTransition(from, to) {
|
|
972
|
+
if (from === to) return false;
|
|
973
|
+
return VALID_NAV_TRANSITIONS[from].includes(to);
|
|
974
|
+
}
|
|
975
|
+
var NavigationManager = class {
|
|
976
|
+
constructor(config = {}, logger) {
|
|
977
|
+
/** Fallback timer guarding against a missing animationend report. */
|
|
978
|
+
this.phaseTimer = null;
|
|
979
|
+
this._destroyed = false;
|
|
980
|
+
this.config = { ...DEFAULT_NAVIGATION_CONFIG, ...config };
|
|
981
|
+
this.logger = logger;
|
|
982
|
+
this.store = vanilla.createStore(() => ({
|
|
983
|
+
phase: "closed",
|
|
984
|
+
openIndex: null
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
// ═══════════════════════════════════════════
|
|
988
|
+
// PUBLIC API — Intent
|
|
989
|
+
// ═══════════════════════════════════════════
|
|
990
|
+
/**
|
|
991
|
+
* Open the reels viewer at `index`. Safe to call from a thumbnail click
|
|
992
|
+
* handler — it transitions into `opening`, at which point ReelsModal mounts
|
|
993
|
+
* <ReelsFeed> hidden and begins prewarming.
|
|
994
|
+
*
|
|
995
|
+
* Calling open() while already open/opening just retargets the index
|
|
996
|
+
* (e.g. user taps a different thumbnail before the animation settles).
|
|
997
|
+
*/
|
|
998
|
+
open(index) {
|
|
999
|
+
if (this._destroyed) return;
|
|
1000
|
+
const { phase, openIndex } = this.store.getState();
|
|
1001
|
+
if ((phase === "open" || phase === "opening") && index === openIndex) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (phase === "open" || phase === "opening") {
|
|
1005
|
+
this.store.setState({ openIndex: index });
|
|
1006
|
+
this.logger?.debug(`[NavigationManager] retarget openIndex=${index}`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (!this.transition("opening")) return;
|
|
1010
|
+
this.store.setState({ openIndex: index });
|
|
1011
|
+
this.logger?.debug(`[NavigationManager] open(${index}) \u2192 opening`);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Begin closing the viewer. ReelsModal animates out then calls
|
|
1015
|
+
* `setPhase('closed')`. Idempotent if already closing/closed.
|
|
1016
|
+
*/
|
|
1017
|
+
close() {
|
|
1018
|
+
if (this._destroyed) return;
|
|
1019
|
+
const { phase } = this.store.getState();
|
|
1020
|
+
if (phase === "closed" || phase === "closing") return;
|
|
1021
|
+
this.transition("closing");
|
|
1022
|
+
this.logger?.debug("[NavigationManager] close() \u2192 closing");
|
|
1023
|
+
}
|
|
1024
|
+
// ═══════════════════════════════════════════
|
|
1025
|
+
// PUBLIC API — Phase reporting (called by ReelsModal)
|
|
1026
|
+
// ═══════════════════════════════════════════
|
|
1027
|
+
/**
|
|
1028
|
+
* Report an animation-driven phase change from the component layer.
|
|
1029
|
+
* Invalid transitions are dropped. When reaching `closed`, openIndex is
|
|
1030
|
+
* cleared so <ReelsFeed> unmounts.
|
|
1031
|
+
*/
|
|
1032
|
+
setPhase(next) {
|
|
1033
|
+
if (this._destroyed) return;
|
|
1034
|
+
this.transition(next);
|
|
1035
|
+
}
|
|
1036
|
+
// ═══════════════════════════════════════════
|
|
1037
|
+
// PUBLIC API — Queries
|
|
1038
|
+
// ═══════════════════════════════════════════
|
|
1039
|
+
getPhase() {
|
|
1040
|
+
return this.store.getState().phase;
|
|
1041
|
+
}
|
|
1042
|
+
getOpenIndex() {
|
|
1043
|
+
return this.store.getState().openIndex;
|
|
1044
|
+
}
|
|
1045
|
+
isOpen() {
|
|
1046
|
+
const { phase } = this.store.getState();
|
|
1047
|
+
return phase === "open" || phase === "opening";
|
|
1048
|
+
}
|
|
1049
|
+
/** Whether <ReelsFeed> should be mounted (any non-closed phase). */
|
|
1050
|
+
shouldMount() {
|
|
1051
|
+
return this.store.getState().phase !== "closed";
|
|
1052
|
+
}
|
|
1053
|
+
// ═══════════════════════════════════════════
|
|
1054
|
+
// PUBLIC API — Lifecycle
|
|
1055
|
+
// ═══════════════════════════════════════════
|
|
1056
|
+
destroy() {
|
|
1057
|
+
if (this._destroyed) return;
|
|
1058
|
+
this._destroyed = true;
|
|
1059
|
+
if (this.phaseTimer) {
|
|
1060
|
+
clearTimeout(this.phaseTimer);
|
|
1061
|
+
this.phaseTimer = null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// ═══════════════════════════════════════════
|
|
1065
|
+
// PRIVATE
|
|
1066
|
+
// ═══════════════════════════════════════════
|
|
1067
|
+
/**
|
|
1068
|
+
* Apply a guarded phase transition. Returns true if it was applied.
|
|
1069
|
+
* Manages the fallback timer for transient phases (opening/closing).
|
|
1070
|
+
*/
|
|
1071
|
+
transition(next) {
|
|
1072
|
+
const { phase } = this.store.getState();
|
|
1073
|
+
if (!isValidNavTransition(phase, next)) {
|
|
1074
|
+
this.logger?.debug(
|
|
1075
|
+
`[NavigationManager] dropped invalid transition ${phase} \u2192 ${next}`
|
|
1076
|
+
);
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
this.clearPhaseTimer();
|
|
1080
|
+
if (next === "closed") {
|
|
1081
|
+
this.store.setState({ phase: "closed", openIndex: null });
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
this.store.setState({ phase: next });
|
|
1085
|
+
if (next === "opening" || next === "closing") {
|
|
1086
|
+
const fallbackTarget = next === "opening" ? "open" : "closed";
|
|
1087
|
+
this.phaseTimer = setTimeout(() => {
|
|
1088
|
+
this.phaseTimer = null;
|
|
1089
|
+
const current = this.store.getState().phase;
|
|
1090
|
+
if (current === next) {
|
|
1091
|
+
this.logger?.warn(
|
|
1092
|
+
`[NavigationManager] phase fallback ${next} \u2192 ${fallbackTarget}`
|
|
1093
|
+
);
|
|
1094
|
+
this.transition(fallbackTarget);
|
|
1095
|
+
}
|
|
1096
|
+
}, this.config.phaseTimeoutMs);
|
|
1097
|
+
}
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
clearPhaseTimer() {
|
|
1101
|
+
if (this.phaseTimer) {
|
|
1102
|
+
clearTimeout(this.phaseTimer);
|
|
1103
|
+
this.phaseTimer = null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
922
1107
|
function usePointerGesture(config = {}) {
|
|
923
1108
|
const {
|
|
924
1109
|
axis = "y",
|
|
@@ -1108,7 +1293,9 @@ function useSnapAnimation(config = {}) {
|
|
|
1108
1293
|
}
|
|
1109
1294
|
);
|
|
1110
1295
|
anim.addEventListener("finish", () => {
|
|
1111
|
-
element.
|
|
1296
|
+
if (element.isConnected) {
|
|
1297
|
+
element.style.transform = `translateY(${toY}px)`;
|
|
1298
|
+
}
|
|
1112
1299
|
anim.cancel();
|
|
1113
1300
|
});
|
|
1114
1301
|
animations.push(anim);
|
|
@@ -1136,31 +1323,100 @@ function useSnapAnimation(config = {}) {
|
|
|
1136
1323
|
}, [cancelAnimation]);
|
|
1137
1324
|
return { animateSnap, animateBounceBack, cancelAnimation };
|
|
1138
1325
|
}
|
|
1326
|
+
var noop = () => {
|
|
1327
|
+
};
|
|
1328
|
+
var noopAsync = () => Promise.resolve();
|
|
1329
|
+
var DEFAULT_LOGGER = {
|
|
1330
|
+
debug: noop,
|
|
1331
|
+
info: noop,
|
|
1332
|
+
warn: noop,
|
|
1333
|
+
error: noop
|
|
1334
|
+
};
|
|
1335
|
+
var DEFAULT_ANALYTICS = {
|
|
1336
|
+
trackView: noop,
|
|
1337
|
+
trackLike: noop,
|
|
1338
|
+
trackShare: noop,
|
|
1339
|
+
trackComment: noop,
|
|
1340
|
+
trackError: noop,
|
|
1341
|
+
trackPlaybackEvent: noop
|
|
1342
|
+
};
|
|
1343
|
+
var DEFAULT_INTERACTION = {
|
|
1344
|
+
like: noopAsync,
|
|
1345
|
+
unlike: noopAsync,
|
|
1346
|
+
follow: noopAsync,
|
|
1347
|
+
unfollow: noopAsync,
|
|
1348
|
+
bookmark: noopAsync,
|
|
1349
|
+
unbookmark: noopAsync,
|
|
1350
|
+
share: noopAsync
|
|
1351
|
+
};
|
|
1352
|
+
var DEFAULT_STORAGE = {
|
|
1353
|
+
get: () => null,
|
|
1354
|
+
set: noop,
|
|
1355
|
+
remove: noop,
|
|
1356
|
+
clear: noop
|
|
1357
|
+
};
|
|
1358
|
+
var DEFAULT_NETWORK = {
|
|
1359
|
+
getNetworkType: () => "unknown",
|
|
1360
|
+
isOnline: () => true,
|
|
1361
|
+
onNetworkChange: () => noop
|
|
1362
|
+
};
|
|
1363
|
+
var DEFAULT_VIDEO_LOADER = {
|
|
1364
|
+
preload: (videoId) => Promise.resolve({ videoId, status: "idle" }),
|
|
1365
|
+
cancel: noop,
|
|
1366
|
+
isPreloaded: () => false,
|
|
1367
|
+
getPreloadStatus: () => "idle",
|
|
1368
|
+
clearAll: noop
|
|
1369
|
+
};
|
|
1370
|
+
var DEFAULT_COMMENT = {
|
|
1371
|
+
fetchComments: () => Promise.resolve({ items: [], nextCursor: null, hasMore: false, total: 0 }),
|
|
1372
|
+
postComment: () => Promise.reject(new Error("Comment adapter not configured")),
|
|
1373
|
+
deleteComment: noopAsync,
|
|
1374
|
+
likeComment: noopAsync,
|
|
1375
|
+
unlikeComment: noopAsync
|
|
1376
|
+
};
|
|
1139
1377
|
var SDKContext = react.createContext(null);
|
|
1140
|
-
function ReelsProvider({
|
|
1141
|
-
|
|
1142
|
-
|
|
1378
|
+
function ReelsProvider({
|
|
1379
|
+
children,
|
|
1380
|
+
adapters,
|
|
1381
|
+
initialItems,
|
|
1382
|
+
debug = false,
|
|
1383
|
+
navigationConfig
|
|
1384
|
+
}) {
|
|
1385
|
+
const resolvedAdapters = react.useMemo(() => ({
|
|
1386
|
+
dataSource: adapters.dataSource,
|
|
1387
|
+
interaction: adapters.interaction ?? DEFAULT_INTERACTION,
|
|
1388
|
+
storage: adapters.storage ?? DEFAULT_STORAGE,
|
|
1389
|
+
analytics: adapters.analytics ?? DEFAULT_ANALYTICS,
|
|
1390
|
+
logger: adapters.logger ?? DEFAULT_LOGGER,
|
|
1391
|
+
network: adapters.network ?? DEFAULT_NETWORK,
|
|
1392
|
+
videoLoader: adapters.videoLoader ?? DEFAULT_VIDEO_LOADER,
|
|
1393
|
+
comment: adapters.comment ?? DEFAULT_COMMENT
|
|
1394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1395
|
+
}), [adapters.dataSource]);
|
|
1396
|
+
const logger = resolvedAdapters.logger;
|
|
1397
|
+
const prevInstanceRef = react.useRef(null);
|
|
1143
1398
|
const value = react.useMemo(() => {
|
|
1144
|
-
if (sdkRef.current) {
|
|
1145
|
-
sdkRef.current.feedManager.destroy();
|
|
1146
|
-
sdkRef.current.playerEngine.destroy();
|
|
1147
|
-
sdkRef.current.resourceGovernor.destroy();
|
|
1148
|
-
sdkRef.current.optimisticManager.destroy();
|
|
1149
|
-
}
|
|
1150
1399
|
const feedManager = new FeedManager(adapters.dataSource, {}, logger);
|
|
1400
|
+
if (initialItems && initialItems.length > 0) {
|
|
1401
|
+
feedManager.setInitialItems(initialItems);
|
|
1402
|
+
}
|
|
1151
1403
|
const playerEngine = new PlayerEngine(
|
|
1152
1404
|
{},
|
|
1153
|
-
|
|
1405
|
+
resolvedAdapters.analytics,
|
|
1154
1406
|
logger
|
|
1155
1407
|
);
|
|
1156
1408
|
const resourceGovernor = new ResourceGovernor(
|
|
1157
1409
|
{},
|
|
1158
|
-
|
|
1159
|
-
|
|
1410
|
+
resolvedAdapters.videoLoader,
|
|
1411
|
+
resolvedAdapters.network,
|
|
1160
1412
|
logger
|
|
1161
1413
|
);
|
|
1162
1414
|
const optimisticManager = new OptimisticManager(
|
|
1163
|
-
|
|
1415
|
+
resolvedAdapters.interaction,
|
|
1416
|
+
logger
|
|
1417
|
+
);
|
|
1418
|
+
const navigationManager = new NavigationManager(
|
|
1419
|
+
navigationConfig ?? {},
|
|
1164
1420
|
logger
|
|
1165
1421
|
);
|
|
1166
1422
|
const instance = {
|
|
@@ -1168,15 +1424,27 @@ function ReelsProvider({ children, adapters, debug = false }) {
|
|
|
1168
1424
|
playerEngine,
|
|
1169
1425
|
resourceGovernor,
|
|
1170
1426
|
optimisticManager,
|
|
1171
|
-
|
|
1427
|
+
navigationManager,
|
|
1428
|
+
adapters: resolvedAdapters
|
|
1172
1429
|
};
|
|
1173
|
-
sdkRef.current = instance;
|
|
1174
1430
|
return instance;
|
|
1175
1431
|
}, [adapters.dataSource]);
|
|
1176
1432
|
react.useEffect(() => {
|
|
1177
|
-
|
|
1433
|
+
const prev = prevInstanceRef.current;
|
|
1434
|
+
if (prev && prev !== value) {
|
|
1435
|
+
prev.feedManager.destroy();
|
|
1436
|
+
prev.playerEngine.destroy();
|
|
1437
|
+
prev.resourceGovernor.destroy();
|
|
1438
|
+
prev.optimisticManager.destroy();
|
|
1439
|
+
prev.navigationManager.destroy();
|
|
1440
|
+
}
|
|
1441
|
+
prevInstanceRef.current = value;
|
|
1442
|
+
}, [value]);
|
|
1443
|
+
react.useEffect(() => {
|
|
1444
|
+
const governor = value.resourceGovernor;
|
|
1445
|
+
governor.activate();
|
|
1178
1446
|
return () => {
|
|
1179
|
-
|
|
1447
|
+
governor.deactivate();
|
|
1180
1448
|
};
|
|
1181
1449
|
}, [value.resourceGovernor]);
|
|
1182
1450
|
react.useEffect(() => {
|
|
@@ -1186,10 +1454,11 @@ function ReelsProvider({ children, adapters, debug = false }) {
|
|
|
1186
1454
|
}, [debug, logger]);
|
|
1187
1455
|
react.useEffect(() => {
|
|
1188
1456
|
return () => {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1457
|
+
prevInstanceRef.current?.feedManager.destroy();
|
|
1458
|
+
prevInstanceRef.current?.playerEngine.destroy();
|
|
1459
|
+
prevInstanceRef.current?.resourceGovernor.destroy();
|
|
1460
|
+
prevInstanceRef.current?.optimisticManager.destroy();
|
|
1461
|
+
prevInstanceRef.current?.navigationManager.destroy();
|
|
1193
1462
|
};
|
|
1194
1463
|
}, []);
|
|
1195
1464
|
return /* @__PURE__ */ jsxRuntime.jsx(SDKContext.Provider, { value, children });
|
|
@@ -1336,10 +1605,11 @@ var ACTIVE_HLS_DEFAULTS = {
|
|
|
1336
1605
|
maxMaxBufferLength: 15,
|
|
1337
1606
|
capLevelToPlayerSize: true,
|
|
1338
1607
|
startLevel: 0,
|
|
1339
|
-
abrEwmaDefaultEstimate:
|
|
1608
|
+
abrEwmaDefaultEstimate: 2e6,
|
|
1340
1609
|
lowLatencyMode: false,
|
|
1341
1610
|
backBufferLength: 5,
|
|
1342
|
-
enableWorker: true
|
|
1611
|
+
enableWorker: true,
|
|
1612
|
+
startFragPrefetch: true
|
|
1343
1613
|
};
|
|
1344
1614
|
var HOT_HLS_DEFAULTS = {
|
|
1345
1615
|
maxBufferLength: 2,
|
|
@@ -1692,25 +1962,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
|
1692
1962
|
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1693
1963
|
var _playAheadActive = 0;
|
|
1694
1964
|
var _playAheadQueue = [];
|
|
1965
|
+
var _tokenCounter = 0;
|
|
1966
|
+
var _releasedTokens = /* @__PURE__ */ new Set();
|
|
1695
1967
|
function acquirePlayAhead() {
|
|
1968
|
+
const token = ++_tokenCounter;
|
|
1969
|
+
const makeRelease = () => {
|
|
1970
|
+
let released = false;
|
|
1971
|
+
return () => {
|
|
1972
|
+
if (released) return;
|
|
1973
|
+
released = true;
|
|
1974
|
+
_releasedTokens.add(token);
|
|
1975
|
+
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1976
|
+
const next = _playAheadQueue.shift();
|
|
1977
|
+
if (next) {
|
|
1978
|
+
next();
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
};
|
|
1696
1982
|
if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
|
|
1697
1983
|
_playAheadActive++;
|
|
1698
|
-
return Promise.resolve();
|
|
1984
|
+
return Promise.resolve(makeRelease());
|
|
1699
1985
|
}
|
|
1700
1986
|
return new Promise((resolve) => {
|
|
1701
1987
|
_playAheadQueue.push(() => {
|
|
1702
|
-
|
|
1988
|
+
_playAheadActive++;
|
|
1989
|
+
setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
|
|
1703
1990
|
});
|
|
1704
1991
|
});
|
|
1705
1992
|
}
|
|
1706
|
-
function releasePlayAhead() {
|
|
1707
|
-
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1708
|
-
const next = _playAheadQueue.shift();
|
|
1709
|
-
if (next) {
|
|
1710
|
-
_playAheadActive++;
|
|
1711
|
-
next();
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
1993
|
function VideoSlot({
|
|
1715
1994
|
item,
|
|
1716
1995
|
index,
|
|
@@ -1833,6 +2112,56 @@ function VideoSlotInner({
|
|
|
1833
2112
|
}
|
|
1834
2113
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
1835
2114
|
const isReady = isHlsSource ? hlsReady : mp4Ready;
|
|
2115
|
+
const [capturedPoster, setCapturedPoster] = react.useState(null);
|
|
2116
|
+
react.useEffect(() => {
|
|
2117
|
+
const video = videoRef.current;
|
|
2118
|
+
if (!video || !shouldLoadSrc) return;
|
|
2119
|
+
let cancelled = false;
|
|
2120
|
+
const captureFrame = () => {
|
|
2121
|
+
if (cancelled) return;
|
|
2122
|
+
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
2123
|
+
if (video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2124
|
+
try {
|
|
2125
|
+
const canvas = document.createElement("canvas");
|
|
2126
|
+
canvas.width = video.videoWidth;
|
|
2127
|
+
canvas.height = video.videoHeight;
|
|
2128
|
+
const ctx = canvas.getContext("2d");
|
|
2129
|
+
if (!ctx) return;
|
|
2130
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
2131
|
+
const dataUrl = canvas.toDataURL("image/webp", 0.85);
|
|
2132
|
+
if (!cancelled) {
|
|
2133
|
+
setCapturedPoster(dataUrl);
|
|
2134
|
+
}
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
|
|
2139
|
+
captureFrame();
|
|
2140
|
+
} else {
|
|
2141
|
+
video.addEventListener("loadeddata", captureFrame, { once: true });
|
|
2142
|
+
}
|
|
2143
|
+
return () => {
|
|
2144
|
+
cancelled = true;
|
|
2145
|
+
video.removeEventListener("loadeddata", captureFrame);
|
|
2146
|
+
};
|
|
2147
|
+
}, [src, shouldLoadSrc]);
|
|
2148
|
+
react.useEffect(() => {
|
|
2149
|
+
setCapturedPoster(null);
|
|
2150
|
+
}, [src]);
|
|
2151
|
+
const [isVideoPlaying, setIsVideoPlaying] = react.useState(false);
|
|
2152
|
+
react.useEffect(() => {
|
|
2153
|
+
const video = videoRef.current;
|
|
2154
|
+
if (!video || !isActive) {
|
|
2155
|
+
setIsVideoPlaying(false);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
const onPlaying = () => setIsVideoPlaying(true);
|
|
2159
|
+
video.addEventListener("playing", onPlaying);
|
|
2160
|
+
return () => {
|
|
2161
|
+
video.removeEventListener("playing", onPlaying);
|
|
2162
|
+
setIsVideoPlaying(false);
|
|
2163
|
+
};
|
|
2164
|
+
}, [isActive]);
|
|
1836
2165
|
const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
|
|
1837
2166
|
react.useEffect(() => {
|
|
1838
2167
|
const video = videoRef.current;
|
|
@@ -1842,36 +2171,43 @@ function VideoSlotInner({
|
|
|
1842
2171
|
const prevMuted = video.muted;
|
|
1843
2172
|
video.muted = true;
|
|
1844
2173
|
let cancelled = false;
|
|
2174
|
+
let release = null;
|
|
1845
2175
|
const doPlayAhead = async () => {
|
|
1846
|
-
await acquirePlayAhead();
|
|
2176
|
+
release = await acquirePlayAhead();
|
|
2177
|
+
if (cancelled) {
|
|
2178
|
+
release();
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
1847
2181
|
try {
|
|
1848
2182
|
await video.play();
|
|
1849
2183
|
if (cancelled) {
|
|
1850
2184
|
video.pause();
|
|
1851
|
-
|
|
2185
|
+
release();
|
|
1852
2186
|
return;
|
|
1853
2187
|
}
|
|
1854
2188
|
const pauseAfterDecode = () => {
|
|
1855
2189
|
video.pause();
|
|
1856
2190
|
video.currentTime = 0;
|
|
1857
2191
|
video.muted = prevMuted;
|
|
1858
|
-
|
|
2192
|
+
release?.();
|
|
1859
2193
|
if (!cancelled) {
|
|
1860
2194
|
setHasPlayedAhead(true);
|
|
1861
2195
|
}
|
|
1862
2196
|
};
|
|
1863
2197
|
setTimeout(pauseAfterDecode, 50);
|
|
1864
2198
|
} catch {
|
|
1865
|
-
|
|
2199
|
+
release?.();
|
|
1866
2200
|
}
|
|
1867
2201
|
};
|
|
1868
2202
|
doPlayAhead();
|
|
1869
2203
|
return () => {
|
|
1870
2204
|
cancelled = true;
|
|
2205
|
+
release?.();
|
|
1871
2206
|
};
|
|
1872
2207
|
}, [isActive, isReady, hasPlayedAhead]);
|
|
1873
2208
|
react.useEffect(() => {
|
|
1874
2209
|
setHasPlayedAhead(false);
|
|
2210
|
+
setIsVideoPlaying(false);
|
|
1875
2211
|
}, [src]);
|
|
1876
2212
|
const wasActiveRef = react.useRef(false);
|
|
1877
2213
|
const [isManuallyPaused, setIsManuallyPaused] = react.useState(false);
|
|
@@ -1948,7 +2284,7 @@ function VideoSlotInner({
|
|
|
1948
2284
|
if (!video) return;
|
|
1949
2285
|
video.muted = isActive ? isMuted : true;
|
|
1950
2286
|
}, [isMuted, isActive]);
|
|
1951
|
-
const showPosterOverlay = !isReady && !hasPlayedAhead;
|
|
2287
|
+
const showPosterOverlay = isActive ? !isVideoPlaying : !isReady && !hasPlayedAhead;
|
|
1952
2288
|
const isPreDecoded = hasPlayedAhead;
|
|
1953
2289
|
const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
|
|
1954
2290
|
const muteIndicatorTimer = react.useRef(null);
|
|
@@ -2066,23 +2402,23 @@ function VideoSlotInner({
|
|
|
2066
2402
|
height: "100%",
|
|
2067
2403
|
objectFit: "cover",
|
|
2068
2404
|
// Hide video until ready to avoid black frame flash.
|
|
2069
|
-
// When pre-decoded, skip transition — first frame is already on canvas.
|
|
2405
|
+
// When pre-decoded or active, skip transition — first frame is already on canvas or playing.
|
|
2070
2406
|
opacity: showPosterOverlay ? 0 : 1,
|
|
2071
|
-
transition: isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2407
|
+
transition: isActive ? "none" : isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2072
2408
|
}
|
|
2073
2409
|
}
|
|
2074
2410
|
),
|
|
2075
|
-
item.poster && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2411
|
+
(capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2076
2412
|
"div",
|
|
2077
2413
|
{
|
|
2078
2414
|
style: {
|
|
2079
2415
|
position: "absolute",
|
|
2080
2416
|
inset: 0,
|
|
2081
|
-
backgroundImage: `url(${item.poster})`,
|
|
2417
|
+
backgroundImage: `url(${capturedPoster || item.poster})`,
|
|
2082
2418
|
backgroundSize: "cover",
|
|
2083
2419
|
backgroundPosition: "center",
|
|
2084
2420
|
opacity: showPosterOverlay ? 1 : 0,
|
|
2085
|
-
transition: "opacity 0.15s ease",
|
|
2421
|
+
transition: isActive ? "none" : "opacity 0.15s ease",
|
|
2086
2422
|
pointerEvents: "none"
|
|
2087
2423
|
}
|
|
2088
2424
|
}
|
|
@@ -2290,7 +2626,7 @@ function ReelsFeed({
|
|
|
2290
2626
|
}, 16);
|
|
2291
2627
|
};
|
|
2292
2628
|
const observer = new MutationObserver(debouncedRebuild);
|
|
2293
|
-
observer.observe(container, { childList: true, subtree:
|
|
2629
|
+
observer.observe(container, { childList: true, subtree: false });
|
|
2294
2630
|
return () => {
|
|
2295
2631
|
observer.disconnect();
|
|
2296
2632
|
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
@@ -2527,6 +2863,56 @@ function parsePxTranslateY(el) {
|
|
|
2527
2863
|
if (!match || !match[1]) return 0;
|
|
2528
2864
|
return Number.parseFloat(match[1]);
|
|
2529
2865
|
}
|
|
2866
|
+
function useNavigationSelector(selector) {
|
|
2867
|
+
const { navigationManager } = useSDK();
|
|
2868
|
+
const selectorRef = react.useRef(selector);
|
|
2869
|
+
selectorRef.current = selector;
|
|
2870
|
+
const lastSnapshot = react.useRef(void 0);
|
|
2871
|
+
const lastState = react.useRef(void 0);
|
|
2872
|
+
const getSnapshot = react.useCallback(() => {
|
|
2873
|
+
const state = navigationManager.store.getState();
|
|
2874
|
+
if (state !== lastState.current) {
|
|
2875
|
+
lastState.current = state;
|
|
2876
|
+
lastSnapshot.current = selectorRef.current(state);
|
|
2877
|
+
}
|
|
2878
|
+
return lastSnapshot.current;
|
|
2879
|
+
}, [navigationManager]);
|
|
2880
|
+
return react.useSyncExternalStore(
|
|
2881
|
+
navigationManager.store.subscribe,
|
|
2882
|
+
getSnapshot,
|
|
2883
|
+
getSnapshot
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
function useNavigation() {
|
|
2887
|
+
const { navigationManager } = useSDK();
|
|
2888
|
+
const selectPhase = react.useCallback((s) => s.phase, []);
|
|
2889
|
+
const selectOpenIndex = react.useCallback((s) => s.openIndex, []);
|
|
2890
|
+
const phase = useNavigationSelector(selectPhase);
|
|
2891
|
+
const openIndex = useNavigationSelector(selectOpenIndex);
|
|
2892
|
+
const isOpen = phase === "open" || phase === "opening";
|
|
2893
|
+
const shouldMount = phase !== "closed";
|
|
2894
|
+
const open = react.useCallback(
|
|
2895
|
+
(index) => navigationManager.open(index),
|
|
2896
|
+
[navigationManager]
|
|
2897
|
+
);
|
|
2898
|
+
const close = react.useCallback(
|
|
2899
|
+
() => navigationManager.close(),
|
|
2900
|
+
[navigationManager]
|
|
2901
|
+
);
|
|
2902
|
+
const setPhase = react.useCallback(
|
|
2903
|
+
(next) => navigationManager.setPhase(next),
|
|
2904
|
+
[navigationManager]
|
|
2905
|
+
);
|
|
2906
|
+
return {
|
|
2907
|
+
phase,
|
|
2908
|
+
openIndex,
|
|
2909
|
+
isOpen,
|
|
2910
|
+
shouldMount,
|
|
2911
|
+
open,
|
|
2912
|
+
close,
|
|
2913
|
+
setPhase
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2530
2916
|
function ReelsFeedThumbnail({
|
|
2531
2917
|
renderThumbnail,
|
|
2532
2918
|
onThumbnailClick,
|
|
@@ -2536,33 +2922,48 @@ function ReelsFeedThumbnail({
|
|
|
2536
2922
|
className = "grid grid-cols-2 gap-3",
|
|
2537
2923
|
wrap = true,
|
|
2538
2924
|
setFocusOnClick = true,
|
|
2925
|
+
openOnClick = false,
|
|
2539
2926
|
prefetchOnHover = false,
|
|
2927
|
+
prewarmOnClick = true,
|
|
2540
2928
|
getKey
|
|
2541
2929
|
}) {
|
|
2542
2930
|
const { items, loading, error, refresh } = useFeed();
|
|
2543
2931
|
const { setFocusedIndexImmediate } = useResource();
|
|
2932
|
+
const { open } = useNavigation();
|
|
2544
2933
|
const { adapters } = useSDK();
|
|
2545
2934
|
const prefetchedRef = react.useRef(/* @__PURE__ */ new Set());
|
|
2935
|
+
const prewarm = react.useCallback(
|
|
2936
|
+
(item) => {
|
|
2937
|
+
if (!isVideoItem(item)) return;
|
|
2938
|
+
if (item.source.type !== "hls") return;
|
|
2939
|
+
const url = item.source.url;
|
|
2940
|
+
if (prefetchedRef.current.has(url)) return;
|
|
2941
|
+
prefetchedRef.current.add(url);
|
|
2942
|
+
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2943
|
+
},
|
|
2944
|
+
[adapters.videoLoader]
|
|
2945
|
+
);
|
|
2546
2946
|
const handleClick = react.useCallback(
|
|
2547
2947
|
(id, item, index) => {
|
|
2948
|
+
if (prewarmOnClick) {
|
|
2949
|
+
prewarm(item);
|
|
2950
|
+
}
|
|
2548
2951
|
if (setFocusOnClick) {
|
|
2549
2952
|
setFocusedIndexImmediate(index);
|
|
2550
2953
|
}
|
|
2954
|
+
if (openOnClick) {
|
|
2955
|
+
open(index);
|
|
2956
|
+
}
|
|
2551
2957
|
onThumbnailClick?.(id, item, index);
|
|
2552
2958
|
},
|
|
2553
|
-
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2959
|
+
[prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
|
|
2554
2960
|
);
|
|
2555
2961
|
const handlePointerEnter = react.useCallback(
|
|
2556
2962
|
(item) => {
|
|
2557
2963
|
if (!prefetchOnHover) return;
|
|
2558
|
-
|
|
2559
|
-
if (item.source.type !== "hls") return;
|
|
2560
|
-
const url = item.source.url;
|
|
2561
|
-
if (prefetchedRef.current.has(url)) return;
|
|
2562
|
-
prefetchedRef.current.add(url);
|
|
2563
|
-
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2964
|
+
prewarm(item);
|
|
2564
2965
|
},
|
|
2565
|
-
[prefetchOnHover,
|
|
2966
|
+
[prefetchOnHover, prewarm]
|
|
2566
2967
|
);
|
|
2567
2968
|
if (loading && items.length === 0) {
|
|
2568
2969
|
if (renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
|
|
@@ -2602,6 +3003,309 @@ function ReelsFeedThumbnail({
|
|
|
2602
3003
|
}
|
|
2603
3004
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
|
|
2604
3005
|
}
|
|
3006
|
+
var DEFAULT_DURATION = 300;
|
|
3007
|
+
var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
3008
|
+
var DEFAULT_DIRECTION = "up";
|
|
3009
|
+
var DEFAULT_Z_INDEX = 1e3;
|
|
3010
|
+
var DEFAULT_PREWARM_FORWARD = 2;
|
|
3011
|
+
function offscreenTransform(direction) {
|
|
3012
|
+
switch (direction) {
|
|
3013
|
+
case "down":
|
|
3014
|
+
return "translateY(-100%)";
|
|
3015
|
+
case "fade":
|
|
3016
|
+
return "translateY(0)";
|
|
3017
|
+
case "up":
|
|
3018
|
+
default:
|
|
3019
|
+
return "translateY(100%)";
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
function prefersReducedMotion() {
|
|
3023
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
3024
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3025
|
+
}
|
|
3026
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
|
|
3027
|
+
function ReelsModal({
|
|
3028
|
+
feedProps,
|
|
3029
|
+
animationConfig,
|
|
3030
|
+
renderBackdrop,
|
|
3031
|
+
renderCloseButton,
|
|
3032
|
+
onOpen,
|
|
3033
|
+
onClose,
|
|
3034
|
+
closeOnBackdropClick = true,
|
|
3035
|
+
closeOnEscape = true,
|
|
3036
|
+
lockBodyScroll = true,
|
|
3037
|
+
prewarmForward = DEFAULT_PREWARM_FORWARD,
|
|
3038
|
+
className,
|
|
3039
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
3040
|
+
portalTarget
|
|
3041
|
+
}) {
|
|
3042
|
+
const { phase, openIndex, shouldMount, close, setPhase } = useNavigation();
|
|
3043
|
+
const { setFocusedIndexImmediate } = useResource();
|
|
3044
|
+
const { items } = useFeed();
|
|
3045
|
+
const { adapters } = useSDK();
|
|
3046
|
+
const duration = animationConfig?.duration ?? DEFAULT_DURATION;
|
|
3047
|
+
const easing = animationConfig?.easing ?? DEFAULT_EASING;
|
|
3048
|
+
const direction = animationConfig?.direction ?? DEFAULT_DIRECTION;
|
|
3049
|
+
const backdropRef = react.useRef(null);
|
|
3050
|
+
const panelRef = react.useRef(null);
|
|
3051
|
+
const animationsRef = react.useRef([]);
|
|
3052
|
+
const previouslyFocusedRef = react.useRef(null);
|
|
3053
|
+
const renderState = react.useMemo(
|
|
3054
|
+
() => ({ phase, openIndex, close }),
|
|
3055
|
+
[phase, openIndex, close]
|
|
3056
|
+
);
|
|
3057
|
+
const prewarmedRef = react.useRef(null);
|
|
3058
|
+
react.useEffect(() => {
|
|
3059
|
+
if (phase !== "opening") {
|
|
3060
|
+
if (phase === "closed") prewarmedRef.current = null;
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (openIndex == null) return;
|
|
3064
|
+
if (prewarmedRef.current === openIndex) return;
|
|
3065
|
+
prewarmedRef.current = openIndex;
|
|
3066
|
+
setFocusedIndexImmediate(openIndex);
|
|
3067
|
+
const preload = adapters.videoLoader?.preloadMetadata;
|
|
3068
|
+
if (!preload) return;
|
|
3069
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3070
|
+
targets.add(openIndex);
|
|
3071
|
+
for (let d = 1; d <= prewarmForward; d++) targets.add(openIndex + d);
|
|
3072
|
+
targets.add(openIndex - 1);
|
|
3073
|
+
for (const idx of targets) {
|
|
3074
|
+
if (idx < 0 || idx >= items.length) continue;
|
|
3075
|
+
const item = items[idx];
|
|
3076
|
+
if (item && isVideoItem(item) && item.source.type === "hls") {
|
|
3077
|
+
preload(item.source.url);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
}, [phase, openIndex, items, adapters.videoLoader, prewarmForward, setFocusedIndexImmediate]);
|
|
3081
|
+
react.useEffect(() => {
|
|
3082
|
+
if (!lockBodyScroll || !shouldMount) return;
|
|
3083
|
+
if (typeof document === "undefined") return;
|
|
3084
|
+
const prev = document.body.style.overflow;
|
|
3085
|
+
document.body.style.overflow = "hidden";
|
|
3086
|
+
return () => {
|
|
3087
|
+
document.body.style.overflow = prev;
|
|
3088
|
+
};
|
|
3089
|
+
}, [lockBodyScroll, shouldMount]);
|
|
3090
|
+
const cancelAnimations = react.useCallback(() => {
|
|
3091
|
+
for (const a of animationsRef.current) a.cancel();
|
|
3092
|
+
animationsRef.current = [];
|
|
3093
|
+
}, []);
|
|
3094
|
+
react.useLayoutEffect(() => {
|
|
3095
|
+
const panel = panelRef.current;
|
|
3096
|
+
const backdrop = backdropRef.current;
|
|
3097
|
+
if (!panel) return;
|
|
3098
|
+
if (phase === "open" || phase === "closed") return;
|
|
3099
|
+
const reduce = prefersReducedMotion();
|
|
3100
|
+
const enter = phase === "opening";
|
|
3101
|
+
const offscreen2 = offscreenTransform(direction);
|
|
3102
|
+
const panelFrames = direction === "fade" ? enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }] : enter ? [{ transform: offscreen2 }, { transform: "translateY(0)" }] : [{ transform: "translateY(0)" }, { transform: offscreen2 }];
|
|
3103
|
+
const backdropFrames = enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
|
|
3104
|
+
const animDuration = reduce ? 0 : duration;
|
|
3105
|
+
let cancelled = false;
|
|
3106
|
+
cancelAnimations();
|
|
3107
|
+
const anims = [];
|
|
3108
|
+
const panelAnim = panel.animate(panelFrames, {
|
|
3109
|
+
duration: animDuration,
|
|
3110
|
+
easing,
|
|
3111
|
+
fill: "forwards"
|
|
3112
|
+
});
|
|
3113
|
+
anims.push(panelAnim);
|
|
3114
|
+
if (backdrop) {
|
|
3115
|
+
anims.push(
|
|
3116
|
+
backdrop.animate(backdropFrames, {
|
|
3117
|
+
duration: animDuration,
|
|
3118
|
+
easing,
|
|
3119
|
+
fill: "forwards"
|
|
3120
|
+
})
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
animationsRef.current = anims;
|
|
3124
|
+
const finalize = () => {
|
|
3125
|
+
if (cancelled) return;
|
|
3126
|
+
if (enter) {
|
|
3127
|
+
if (direction === "fade") panel.style.opacity = "1";
|
|
3128
|
+
else panel.style.transform = "translateY(0)";
|
|
3129
|
+
if (backdrop) backdrop.style.opacity = "1";
|
|
3130
|
+
} else {
|
|
3131
|
+
if (direction === "fade") panel.style.opacity = "0";
|
|
3132
|
+
else panel.style.transform = offscreen2;
|
|
3133
|
+
if (backdrop) backdrop.style.opacity = "0";
|
|
3134
|
+
}
|
|
3135
|
+
for (const a of anims) a.cancel();
|
|
3136
|
+
animationsRef.current = [];
|
|
3137
|
+
setPhase(enter ? "open" : "closed");
|
|
3138
|
+
};
|
|
3139
|
+
panelAnim.addEventListener("finish", finalize);
|
|
3140
|
+
return () => {
|
|
3141
|
+
cancelled = true;
|
|
3142
|
+
panelAnim.removeEventListener("finish", finalize);
|
|
3143
|
+
cancelAnimations();
|
|
3144
|
+
};
|
|
3145
|
+
}, [phase, direction, duration, easing, setPhase, cancelAnimations]);
|
|
3146
|
+
const prevPhaseRef = react.useRef("closed");
|
|
3147
|
+
react.useEffect(() => {
|
|
3148
|
+
const prev = prevPhaseRef.current;
|
|
3149
|
+
if (prev !== "open" && phase === "open") onOpen?.(openIndex);
|
|
3150
|
+
if (prev !== "closed" && phase === "closed") onClose?.();
|
|
3151
|
+
prevPhaseRef.current = phase;
|
|
3152
|
+
}, [phase, openIndex, onOpen, onClose]);
|
|
3153
|
+
react.useEffect(() => {
|
|
3154
|
+
if (phase === "opening" && typeof document !== "undefined") {
|
|
3155
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
3156
|
+
const id = requestAnimationFrame(() => {
|
|
3157
|
+
const panel = panelRef.current;
|
|
3158
|
+
if (!panel) return;
|
|
3159
|
+
const first = panel.querySelector(FOCUSABLE);
|
|
3160
|
+
(first ?? panel).focus();
|
|
3161
|
+
});
|
|
3162
|
+
return () => cancelAnimationFrame(id);
|
|
3163
|
+
}
|
|
3164
|
+
if (phase === "closed") {
|
|
3165
|
+
previouslyFocusedRef.current?.focus?.();
|
|
3166
|
+
previouslyFocusedRef.current = null;
|
|
3167
|
+
}
|
|
3168
|
+
return void 0;
|
|
3169
|
+
}, [phase]);
|
|
3170
|
+
const handleKeyDown = react.useCallback(
|
|
3171
|
+
(e) => {
|
|
3172
|
+
if (closeOnEscape && e.key === "Escape") {
|
|
3173
|
+
e.stopPropagation();
|
|
3174
|
+
close();
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
if (e.key !== "Tab") return;
|
|
3178
|
+
const panel = panelRef.current;
|
|
3179
|
+
if (!panel) return;
|
|
3180
|
+
const focusables = Array.from(
|
|
3181
|
+
panel.querySelectorAll(FOCUSABLE)
|
|
3182
|
+
).filter((el) => el.offsetParent !== null);
|
|
3183
|
+
if (focusables.length === 0) {
|
|
3184
|
+
e.preventDefault();
|
|
3185
|
+
panel.focus();
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
const first = focusables[0];
|
|
3189
|
+
const last = focusables[focusables.length - 1];
|
|
3190
|
+
const active = document.activeElement;
|
|
3191
|
+
if (e.shiftKey && active === first) {
|
|
3192
|
+
e.preventDefault();
|
|
3193
|
+
last.focus();
|
|
3194
|
+
} else if (!e.shiftKey && active === last) {
|
|
3195
|
+
e.preventDefault();
|
|
3196
|
+
first.focus();
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
[closeOnEscape, close]
|
|
3200
|
+
);
|
|
3201
|
+
const handleBackdropClick = react.useCallback(() => {
|
|
3202
|
+
if (closeOnBackdropClick) close();
|
|
3203
|
+
}, [closeOnBackdropClick, close]);
|
|
3204
|
+
if (!shouldMount) return null;
|
|
3205
|
+
if (typeof document === "undefined") return null;
|
|
3206
|
+
const target = portalTarget ?? document.body;
|
|
3207
|
+
if (!target) return null;
|
|
3208
|
+
const enterStart = phase === "opening";
|
|
3209
|
+
const offscreen = offscreenTransform(direction);
|
|
3210
|
+
const panelInitialTransform = direction === "fade" ? "translateY(0)" : enterStart ? offscreen : "translateY(0)";
|
|
3211
|
+
const panelInitialOpacity = direction === "fade" ? enterStart ? 0 : 1 : 1;
|
|
3212
|
+
const backdropInitialOpacity = enterStart ? 0 : 1;
|
|
3213
|
+
const rootStyle = {
|
|
3214
|
+
position: "fixed",
|
|
3215
|
+
inset: 0,
|
|
3216
|
+
zIndex,
|
|
3217
|
+
// While opening, the panel is parked off-screen but MUST stay painted so
|
|
3218
|
+
// the <video> decodes its first frame. overflow:hidden keeps the
|
|
3219
|
+
// off-screen panel from creating scrollbars / capturing stray taps.
|
|
3220
|
+
overflow: "hidden"
|
|
3221
|
+
};
|
|
3222
|
+
const backdropStyle = {
|
|
3223
|
+
position: "absolute",
|
|
3224
|
+
inset: 0,
|
|
3225
|
+
opacity: backdropInitialOpacity,
|
|
3226
|
+
background: renderBackdrop ? "transparent" : "rgba(0,0,0,0.85)",
|
|
3227
|
+
// willChange hints the compositor for the opacity fade.
|
|
3228
|
+
willChange: "opacity"
|
|
3229
|
+
};
|
|
3230
|
+
const panelStyle = {
|
|
3231
|
+
position: "absolute",
|
|
3232
|
+
inset: 0,
|
|
3233
|
+
transform: panelInitialTransform,
|
|
3234
|
+
opacity: panelInitialOpacity,
|
|
3235
|
+
willChange: direction === "fade" ? "opacity" : "transform",
|
|
3236
|
+
outline: "none"
|
|
3237
|
+
};
|
|
3238
|
+
return reactDom.createPortal(
|
|
3239
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3240
|
+
"div",
|
|
3241
|
+
{
|
|
3242
|
+
style: rootStyle,
|
|
3243
|
+
onKeyDown: handleKeyDown,
|
|
3244
|
+
"data-reels-modal-phase": phase,
|
|
3245
|
+
children: [
|
|
3246
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3247
|
+
"div",
|
|
3248
|
+
{
|
|
3249
|
+
ref: backdropRef,
|
|
3250
|
+
style: backdropStyle,
|
|
3251
|
+
onClick: handleBackdropClick,
|
|
3252
|
+
"aria-hidden": "true",
|
|
3253
|
+
"data-reels-modal-backdrop": true,
|
|
3254
|
+
children: renderBackdrop ? renderBackdrop(renderState) : null
|
|
3255
|
+
}
|
|
3256
|
+
),
|
|
3257
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3258
|
+
"div",
|
|
3259
|
+
{
|
|
3260
|
+
ref: panelRef,
|
|
3261
|
+
role: "dialog",
|
|
3262
|
+
"aria-modal": "true",
|
|
3263
|
+
"aria-label": "Reels viewer",
|
|
3264
|
+
tabIndex: -1,
|
|
3265
|
+
className,
|
|
3266
|
+
style: panelStyle,
|
|
3267
|
+
"data-reels-modal-panel": true,
|
|
3268
|
+
children: [
|
|
3269
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReelsFeed, { ...feedProps }),
|
|
3270
|
+
renderCloseButton ? renderCloseButton(renderState) : /* @__PURE__ */ jsxRuntime.jsx(DefaultCloseButton, { onClose: close })
|
|
3271
|
+
]
|
|
3272
|
+
}
|
|
3273
|
+
)
|
|
3274
|
+
]
|
|
3275
|
+
}
|
|
3276
|
+
),
|
|
3277
|
+
target
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
function DefaultCloseButton({ onClose }) {
|
|
3281
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3282
|
+
"button",
|
|
3283
|
+
{
|
|
3284
|
+
type: "button",
|
|
3285
|
+
onClick: onClose,
|
|
3286
|
+
"aria-label": "Close reels viewer",
|
|
3287
|
+
style: {
|
|
3288
|
+
position: "absolute",
|
|
3289
|
+
top: "max(12px, env(safe-area-inset-top))",
|
|
3290
|
+
right: 12,
|
|
3291
|
+
width: 40,
|
|
3292
|
+
height: 40,
|
|
3293
|
+
display: "flex",
|
|
3294
|
+
alignItems: "center",
|
|
3295
|
+
justifyContent: "center",
|
|
3296
|
+
borderRadius: "50%",
|
|
3297
|
+
border: "none",
|
|
3298
|
+
background: "rgba(0,0,0,0.45)",
|
|
3299
|
+
color: "#fff",
|
|
3300
|
+
fontSize: 22,
|
|
3301
|
+
lineHeight: 1,
|
|
3302
|
+
cursor: "pointer",
|
|
3303
|
+
zIndex: 2
|
|
3304
|
+
},
|
|
3305
|
+
children: "\u2715"
|
|
3306
|
+
}
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
2605
3309
|
function usePlayerSelector(selector) {
|
|
2606
3310
|
const { playerEngine } = useSDK();
|
|
2607
3311
|
const selectorRef = react.useRef(selector);
|
|
@@ -3217,6 +3921,7 @@ var HttpError = class extends Error {
|
|
|
3217
3921
|
};
|
|
3218
3922
|
|
|
3219
3923
|
exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
|
|
3924
|
+
exports.DEFAULT_NAVIGATION_CONFIG = DEFAULT_NAVIGATION_CONFIG;
|
|
3220
3925
|
exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
|
|
3221
3926
|
exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
|
|
3222
3927
|
exports.DefaultActions = DefaultActions;
|
|
@@ -3234,11 +3939,13 @@ exports.MockLogger = MockLogger;
|
|
|
3234
3939
|
exports.MockNetworkAdapter = MockNetworkAdapter;
|
|
3235
3940
|
exports.MockSessionStorage = MockSessionStorage;
|
|
3236
3941
|
exports.MockVideoLoader = MockVideoLoader;
|
|
3942
|
+
exports.NavigationManager = NavigationManager;
|
|
3237
3943
|
exports.OptimisticManager = OptimisticManager;
|
|
3238
3944
|
exports.PlayerEngine = PlayerEngine;
|
|
3239
3945
|
exports.PlayerStatus = PlayerStatus;
|
|
3240
3946
|
exports.ReelsFeed = ReelsFeed;
|
|
3241
3947
|
exports.ReelsFeedThumbnail = ReelsFeedThumbnail;
|
|
3948
|
+
exports.ReelsModal = ReelsModal;
|
|
3242
3949
|
exports.ReelsProvider = ReelsProvider;
|
|
3243
3950
|
exports.ResourceGovernor = ResourceGovernor;
|
|
3244
3951
|
exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
|
|
@@ -3247,11 +3954,14 @@ exports.canPause = canPause;
|
|
|
3247
3954
|
exports.canPlay = canPlay;
|
|
3248
3955
|
exports.canSeek = canSeek;
|
|
3249
3956
|
exports.isArticle = isArticle;
|
|
3957
|
+
exports.isValidNavTransition = isValidNavTransition;
|
|
3250
3958
|
exports.isValidTransition = isValidTransition;
|
|
3251
3959
|
exports.isVideoItem = isVideoItem;
|
|
3252
3960
|
exports.useFeed = useFeed;
|
|
3253
3961
|
exports.useFeedSelector = useFeedSelector;
|
|
3254
3962
|
exports.useHls = useHls;
|
|
3963
|
+
exports.useNavigation = useNavigation;
|
|
3964
|
+
exports.useNavigationSelector = useNavigationSelector;
|
|
3255
3965
|
exports.usePlayer = usePlayer;
|
|
3256
3966
|
exports.usePlayerSelector = usePlayerSelector;
|
|
3257
3967
|
exports.usePointerGesture = usePointerGesture;
|