@xhub-reels/sdk 0.2.12 → 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 +730 -51
- package/dist/index.d.cts +219 -4
- package/dist/index.d.ts +219 -4
- package/dist/index.js +726 -53
- 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 };
|
|
@@ -531,7 +540,10 @@ var FeedManager = class {
|
|
|
531
540
|
// PUBLIC API — Lifecycle
|
|
532
541
|
// ═══════════════════════════════════════════
|
|
533
542
|
destroy() {
|
|
543
|
+
if (this._destroyed) return;
|
|
544
|
+
this._destroyed = true;
|
|
534
545
|
this.abortController?.abort();
|
|
546
|
+
this.abortController = null;
|
|
535
547
|
this.inFlightRequests.clear();
|
|
536
548
|
this.accessOrder.clear();
|
|
537
549
|
this.prefetchCache = null;
|
|
@@ -600,7 +612,13 @@ var FeedManager = class {
|
|
|
600
612
|
applyItems(incoming, _nextCursor, append) {
|
|
601
613
|
const { itemsById, displayOrder } = this.store.getState();
|
|
602
614
|
const nextById = new Map(itemsById);
|
|
615
|
+
const seenInBatch = /* @__PURE__ */ new Set();
|
|
616
|
+
const deduped = [];
|
|
603
617
|
for (const item of incoming) {
|
|
618
|
+
if (!seenInBatch.has(item.id)) {
|
|
619
|
+
seenInBatch.add(item.id);
|
|
620
|
+
deduped.push(item);
|
|
621
|
+
}
|
|
604
622
|
nextById.set(item.id, item);
|
|
605
623
|
this.accessOrder.set(item.id, Date.now());
|
|
606
624
|
}
|
|
@@ -608,14 +626,14 @@ var FeedManager = class {
|
|
|
608
626
|
if (append) {
|
|
609
627
|
const existingIds = new Set(displayOrder);
|
|
610
628
|
const newIds = [];
|
|
611
|
-
for (const item of
|
|
629
|
+
for (const item of deduped) {
|
|
612
630
|
if (!existingIds.has(item.id)) {
|
|
613
631
|
newIds.push(item.id);
|
|
614
632
|
}
|
|
615
633
|
}
|
|
616
634
|
nextOrder = [...displayOrder, ...newIds];
|
|
617
635
|
} else {
|
|
618
|
-
nextOrder =
|
|
636
|
+
nextOrder = deduped.map((item) => item.id);
|
|
619
637
|
}
|
|
620
638
|
if (nextById.size > this.config.maxCacheSize) {
|
|
621
639
|
this.evictLRU(nextById, nextOrder);
|
|
@@ -657,6 +675,8 @@ var OptimisticManager = class {
|
|
|
657
675
|
this.likeDebounceTimers = /* @__PURE__ */ new Map();
|
|
658
676
|
/** Pending like direction: contentId → final intended state */
|
|
659
677
|
this.pendingLikeState = /* @__PURE__ */ new Map();
|
|
678
|
+
/** Idempotency guard for destroy() */
|
|
679
|
+
this._destroyed = false;
|
|
660
680
|
this.interaction = interaction;
|
|
661
681
|
this.logger = logger;
|
|
662
682
|
this.store = vanilla.createStore(createInitialState3);
|
|
@@ -747,6 +767,8 @@ var OptimisticManager = class {
|
|
|
747
767
|
// PUBLIC API — Lifecycle
|
|
748
768
|
// ═══════════════════════════════════════════
|
|
749
769
|
destroy() {
|
|
770
|
+
if (this._destroyed) return;
|
|
771
|
+
this._destroyed = true;
|
|
750
772
|
for (const timer of this.likeDebounceTimers.values()) {
|
|
751
773
|
clearTimeout(timer);
|
|
752
774
|
}
|
|
@@ -768,6 +790,7 @@ var DEFAULT_RESOURCE_CONFIG = {
|
|
|
768
790
|
var ResourceGovernor = class {
|
|
769
791
|
constructor(config = {}, videoLoader, network, logger) {
|
|
770
792
|
this.focusDebounceTimer = null;
|
|
793
|
+
this._destroyed = false;
|
|
771
794
|
this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
|
|
772
795
|
this.videoLoader = videoLoader;
|
|
773
796
|
this.network = network;
|
|
@@ -798,11 +821,19 @@ var ResourceGovernor = class {
|
|
|
798
821
|
this.logger?.debug("[ResourceGovernor] Activated");
|
|
799
822
|
}
|
|
800
823
|
deactivate() {
|
|
824
|
+
if (this._destroyed) return;
|
|
825
|
+
if (!this.store.getState().isActive) return;
|
|
801
826
|
this.networkUnsubscribe?.();
|
|
802
|
-
|
|
827
|
+
this.networkUnsubscribe = void 0;
|
|
828
|
+
if (this.focusDebounceTimer) {
|
|
829
|
+
clearTimeout(this.focusDebounceTimer);
|
|
830
|
+
this.focusDebounceTimer = null;
|
|
831
|
+
}
|
|
803
832
|
this.store.setState({ isActive: false });
|
|
804
833
|
}
|
|
805
834
|
destroy() {
|
|
835
|
+
if (this._destroyed) return;
|
|
836
|
+
this._destroyed = true;
|
|
806
837
|
this.deactivate();
|
|
807
838
|
this.videoLoader?.clearAll();
|
|
808
839
|
}
|
|
@@ -926,6 +957,153 @@ var ResourceGovernor = class {
|
|
|
926
957
|
);
|
|
927
958
|
}
|
|
928
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
|
+
};
|
|
929
1107
|
function usePointerGesture(config = {}) {
|
|
930
1108
|
const {
|
|
931
1109
|
axis = "y",
|
|
@@ -1115,7 +1293,9 @@ function useSnapAnimation(config = {}) {
|
|
|
1115
1293
|
}
|
|
1116
1294
|
);
|
|
1117
1295
|
anim.addEventListener("finish", () => {
|
|
1118
|
-
element.
|
|
1296
|
+
if (element.isConnected) {
|
|
1297
|
+
element.style.transform = `translateY(${toY}px)`;
|
|
1298
|
+
}
|
|
1119
1299
|
anim.cancel();
|
|
1120
1300
|
});
|
|
1121
1301
|
animations.push(anim);
|
|
@@ -1143,39 +1323,100 @@ function useSnapAnimation(config = {}) {
|
|
|
1143
1323
|
}, [cancelAnimation]);
|
|
1144
1324
|
return { animateSnap, animateBounceBack, cancelAnimation };
|
|
1145
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
|
+
};
|
|
1146
1377
|
var SDKContext = react.createContext(null);
|
|
1147
1378
|
function ReelsProvider({
|
|
1148
1379
|
children,
|
|
1149
1380
|
adapters,
|
|
1150
1381
|
initialItems,
|
|
1151
|
-
debug = false
|
|
1382
|
+
debug = false,
|
|
1383
|
+
navigationConfig
|
|
1152
1384
|
}) {
|
|
1153
|
-
const
|
|
1154
|
-
|
|
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);
|
|
1155
1398
|
const value = react.useMemo(() => {
|
|
1156
|
-
if (sdkRef.current) {
|
|
1157
|
-
sdkRef.current.feedManager.destroy();
|
|
1158
|
-
sdkRef.current.playerEngine.destroy();
|
|
1159
|
-
sdkRef.current.resourceGovernor.destroy();
|
|
1160
|
-
sdkRef.current.optimisticManager.destroy();
|
|
1161
|
-
}
|
|
1162
1399
|
const feedManager = new FeedManager(adapters.dataSource, {}, logger);
|
|
1163
1400
|
if (initialItems && initialItems.length > 0) {
|
|
1164
1401
|
feedManager.setInitialItems(initialItems);
|
|
1165
1402
|
}
|
|
1166
1403
|
const playerEngine = new PlayerEngine(
|
|
1167
1404
|
{},
|
|
1168
|
-
|
|
1405
|
+
resolvedAdapters.analytics,
|
|
1169
1406
|
logger
|
|
1170
1407
|
);
|
|
1171
1408
|
const resourceGovernor = new ResourceGovernor(
|
|
1172
1409
|
{},
|
|
1173
|
-
|
|
1174
|
-
|
|
1410
|
+
resolvedAdapters.videoLoader,
|
|
1411
|
+
resolvedAdapters.network,
|
|
1175
1412
|
logger
|
|
1176
1413
|
);
|
|
1177
1414
|
const optimisticManager = new OptimisticManager(
|
|
1178
|
-
|
|
1415
|
+
resolvedAdapters.interaction,
|
|
1416
|
+
logger
|
|
1417
|
+
);
|
|
1418
|
+
const navigationManager = new NavigationManager(
|
|
1419
|
+
navigationConfig ?? {},
|
|
1179
1420
|
logger
|
|
1180
1421
|
);
|
|
1181
1422
|
const instance = {
|
|
@@ -1183,15 +1424,27 @@ function ReelsProvider({
|
|
|
1183
1424
|
playerEngine,
|
|
1184
1425
|
resourceGovernor,
|
|
1185
1426
|
optimisticManager,
|
|
1186
|
-
|
|
1427
|
+
navigationManager,
|
|
1428
|
+
adapters: resolvedAdapters
|
|
1187
1429
|
};
|
|
1188
|
-
sdkRef.current = instance;
|
|
1189
1430
|
return instance;
|
|
1190
1431
|
}, [adapters.dataSource]);
|
|
1191
1432
|
react.useEffect(() => {
|
|
1192
|
-
|
|
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();
|
|
1193
1446
|
return () => {
|
|
1194
|
-
|
|
1447
|
+
governor.deactivate();
|
|
1195
1448
|
};
|
|
1196
1449
|
}, [value.resourceGovernor]);
|
|
1197
1450
|
react.useEffect(() => {
|
|
@@ -1201,10 +1454,11 @@ function ReelsProvider({
|
|
|
1201
1454
|
}, [debug, logger]);
|
|
1202
1455
|
react.useEffect(() => {
|
|
1203
1456
|
return () => {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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();
|
|
1208
1462
|
};
|
|
1209
1463
|
}, []);
|
|
1210
1464
|
return /* @__PURE__ */ jsxRuntime.jsx(SDKContext.Provider, { value, children });
|
|
@@ -1708,25 +1962,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
|
1708
1962
|
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1709
1963
|
var _playAheadActive = 0;
|
|
1710
1964
|
var _playAheadQueue = [];
|
|
1965
|
+
var _tokenCounter = 0;
|
|
1966
|
+
var _releasedTokens = /* @__PURE__ */ new Set();
|
|
1711
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
|
+
};
|
|
1712
1982
|
if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
|
|
1713
1983
|
_playAheadActive++;
|
|
1714
|
-
return Promise.resolve();
|
|
1984
|
+
return Promise.resolve(makeRelease());
|
|
1715
1985
|
}
|
|
1716
1986
|
return new Promise((resolve) => {
|
|
1717
1987
|
_playAheadQueue.push(() => {
|
|
1718
|
-
|
|
1988
|
+
_playAheadActive++;
|
|
1989
|
+
setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
|
|
1719
1990
|
});
|
|
1720
1991
|
});
|
|
1721
1992
|
}
|
|
1722
|
-
function releasePlayAhead() {
|
|
1723
|
-
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1724
|
-
const next = _playAheadQueue.shift();
|
|
1725
|
-
if (next) {
|
|
1726
|
-
_playAheadActive++;
|
|
1727
|
-
next();
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
1993
|
function VideoSlot({
|
|
1731
1994
|
item,
|
|
1732
1995
|
index,
|
|
@@ -1849,6 +2112,42 @@ function VideoSlotInner({
|
|
|
1849
2112
|
}
|
|
1850
2113
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
1851
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]);
|
|
1852
2151
|
const [isVideoPlaying, setIsVideoPlaying] = react.useState(false);
|
|
1853
2152
|
react.useEffect(() => {
|
|
1854
2153
|
const video = videoRef.current;
|
|
@@ -1872,32 +2171,38 @@ function VideoSlotInner({
|
|
|
1872
2171
|
const prevMuted = video.muted;
|
|
1873
2172
|
video.muted = true;
|
|
1874
2173
|
let cancelled = false;
|
|
2174
|
+
let release = null;
|
|
1875
2175
|
const doPlayAhead = async () => {
|
|
1876
|
-
await acquirePlayAhead();
|
|
2176
|
+
release = await acquirePlayAhead();
|
|
2177
|
+
if (cancelled) {
|
|
2178
|
+
release();
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
1877
2181
|
try {
|
|
1878
2182
|
await video.play();
|
|
1879
2183
|
if (cancelled) {
|
|
1880
2184
|
video.pause();
|
|
1881
|
-
|
|
2185
|
+
release();
|
|
1882
2186
|
return;
|
|
1883
2187
|
}
|
|
1884
2188
|
const pauseAfterDecode = () => {
|
|
1885
2189
|
video.pause();
|
|
1886
2190
|
video.currentTime = 0;
|
|
1887
2191
|
video.muted = prevMuted;
|
|
1888
|
-
|
|
2192
|
+
release?.();
|
|
1889
2193
|
if (!cancelled) {
|
|
1890
2194
|
setHasPlayedAhead(true);
|
|
1891
2195
|
}
|
|
1892
2196
|
};
|
|
1893
2197
|
setTimeout(pauseAfterDecode, 50);
|
|
1894
2198
|
} catch {
|
|
1895
|
-
|
|
2199
|
+
release?.();
|
|
1896
2200
|
}
|
|
1897
2201
|
};
|
|
1898
2202
|
doPlayAhead();
|
|
1899
2203
|
return () => {
|
|
1900
2204
|
cancelled = true;
|
|
2205
|
+
release?.();
|
|
1901
2206
|
};
|
|
1902
2207
|
}, [isActive, isReady, hasPlayedAhead]);
|
|
1903
2208
|
react.useEffect(() => {
|
|
@@ -2103,13 +2408,13 @@ function VideoSlotInner({
|
|
|
2103
2408
|
}
|
|
2104
2409
|
}
|
|
2105
2410
|
),
|
|
2106
|
-
item.poster && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2411
|
+
(capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2107
2412
|
"div",
|
|
2108
2413
|
{
|
|
2109
2414
|
style: {
|
|
2110
2415
|
position: "absolute",
|
|
2111
2416
|
inset: 0,
|
|
2112
|
-
backgroundImage: `url(${item.poster})`,
|
|
2417
|
+
backgroundImage: `url(${capturedPoster || item.poster})`,
|
|
2113
2418
|
backgroundSize: "cover",
|
|
2114
2419
|
backgroundPosition: "center",
|
|
2115
2420
|
opacity: showPosterOverlay ? 1 : 0,
|
|
@@ -2321,7 +2626,7 @@ function ReelsFeed({
|
|
|
2321
2626
|
}, 16);
|
|
2322
2627
|
};
|
|
2323
2628
|
const observer = new MutationObserver(debouncedRebuild);
|
|
2324
|
-
observer.observe(container, { childList: true, subtree:
|
|
2629
|
+
observer.observe(container, { childList: true, subtree: false });
|
|
2325
2630
|
return () => {
|
|
2326
2631
|
observer.disconnect();
|
|
2327
2632
|
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
@@ -2558,6 +2863,56 @@ function parsePxTranslateY(el) {
|
|
|
2558
2863
|
if (!match || !match[1]) return 0;
|
|
2559
2864
|
return Number.parseFloat(match[1]);
|
|
2560
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
|
+
}
|
|
2561
2916
|
function ReelsFeedThumbnail({
|
|
2562
2917
|
renderThumbnail,
|
|
2563
2918
|
onThumbnailClick,
|
|
@@ -2567,33 +2922,48 @@ function ReelsFeedThumbnail({
|
|
|
2567
2922
|
className = "grid grid-cols-2 gap-3",
|
|
2568
2923
|
wrap = true,
|
|
2569
2924
|
setFocusOnClick = true,
|
|
2925
|
+
openOnClick = false,
|
|
2570
2926
|
prefetchOnHover = false,
|
|
2927
|
+
prewarmOnClick = true,
|
|
2571
2928
|
getKey
|
|
2572
2929
|
}) {
|
|
2573
2930
|
const { items, loading, error, refresh } = useFeed();
|
|
2574
2931
|
const { setFocusedIndexImmediate } = useResource();
|
|
2932
|
+
const { open } = useNavigation();
|
|
2575
2933
|
const { adapters } = useSDK();
|
|
2576
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
|
+
);
|
|
2577
2946
|
const handleClick = react.useCallback(
|
|
2578
2947
|
(id, item, index) => {
|
|
2948
|
+
if (prewarmOnClick) {
|
|
2949
|
+
prewarm(item);
|
|
2950
|
+
}
|
|
2579
2951
|
if (setFocusOnClick) {
|
|
2580
2952
|
setFocusedIndexImmediate(index);
|
|
2581
2953
|
}
|
|
2954
|
+
if (openOnClick) {
|
|
2955
|
+
open(index);
|
|
2956
|
+
}
|
|
2582
2957
|
onThumbnailClick?.(id, item, index);
|
|
2583
2958
|
},
|
|
2584
|
-
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2959
|
+
[prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
|
|
2585
2960
|
);
|
|
2586
2961
|
const handlePointerEnter = react.useCallback(
|
|
2587
2962
|
(item) => {
|
|
2588
2963
|
if (!prefetchOnHover) return;
|
|
2589
|
-
|
|
2590
|
-
if (item.source.type !== "hls") return;
|
|
2591
|
-
const url = item.source.url;
|
|
2592
|
-
if (prefetchedRef.current.has(url)) return;
|
|
2593
|
-
prefetchedRef.current.add(url);
|
|
2594
|
-
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2964
|
+
prewarm(item);
|
|
2595
2965
|
},
|
|
2596
|
-
[prefetchOnHover,
|
|
2966
|
+
[prefetchOnHover, prewarm]
|
|
2597
2967
|
);
|
|
2598
2968
|
if (loading && items.length === 0) {
|
|
2599
2969
|
if (renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
|
|
@@ -2633,6 +3003,309 @@ function ReelsFeedThumbnail({
|
|
|
2633
3003
|
}
|
|
2634
3004
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
|
|
2635
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
|
+
}
|
|
2636
3309
|
function usePlayerSelector(selector) {
|
|
2637
3310
|
const { playerEngine } = useSDK();
|
|
2638
3311
|
const selectorRef = react.useRef(selector);
|
|
@@ -3248,6 +3921,7 @@ var HttpError = class extends Error {
|
|
|
3248
3921
|
};
|
|
3249
3922
|
|
|
3250
3923
|
exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
|
|
3924
|
+
exports.DEFAULT_NAVIGATION_CONFIG = DEFAULT_NAVIGATION_CONFIG;
|
|
3251
3925
|
exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
|
|
3252
3926
|
exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
|
|
3253
3927
|
exports.DefaultActions = DefaultActions;
|
|
@@ -3265,11 +3939,13 @@ exports.MockLogger = MockLogger;
|
|
|
3265
3939
|
exports.MockNetworkAdapter = MockNetworkAdapter;
|
|
3266
3940
|
exports.MockSessionStorage = MockSessionStorage;
|
|
3267
3941
|
exports.MockVideoLoader = MockVideoLoader;
|
|
3942
|
+
exports.NavigationManager = NavigationManager;
|
|
3268
3943
|
exports.OptimisticManager = OptimisticManager;
|
|
3269
3944
|
exports.PlayerEngine = PlayerEngine;
|
|
3270
3945
|
exports.PlayerStatus = PlayerStatus;
|
|
3271
3946
|
exports.ReelsFeed = ReelsFeed;
|
|
3272
3947
|
exports.ReelsFeedThumbnail = ReelsFeedThumbnail;
|
|
3948
|
+
exports.ReelsModal = ReelsModal;
|
|
3273
3949
|
exports.ReelsProvider = ReelsProvider;
|
|
3274
3950
|
exports.ResourceGovernor = ResourceGovernor;
|
|
3275
3951
|
exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
|
|
@@ -3278,11 +3954,14 @@ exports.canPause = canPause;
|
|
|
3278
3954
|
exports.canPlay = canPlay;
|
|
3279
3955
|
exports.canSeek = canSeek;
|
|
3280
3956
|
exports.isArticle = isArticle;
|
|
3957
|
+
exports.isValidNavTransition = isValidNavTransition;
|
|
3281
3958
|
exports.isValidTransition = isValidTransition;
|
|
3282
3959
|
exports.isVideoItem = isVideoItem;
|
|
3283
3960
|
exports.useFeed = useFeed;
|
|
3284
3961
|
exports.useFeedSelector = useFeedSelector;
|
|
3285
3962
|
exports.useHls = useHls;
|
|
3963
|
+
exports.useNavigation = useNavigation;
|
|
3964
|
+
exports.useNavigationSelector = useNavigationSelector;
|
|
3286
3965
|
exports.usePlayer = usePlayer;
|
|
3287
3966
|
exports.usePlayerSelector = usePlayerSelector;
|
|
3288
3967
|
exports.usePointerGesture = usePointerGesture;
|