@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.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createStore } from 'zustand/vanilla';
|
|
2
|
-
import { createContext, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore, useState } from 'react';
|
|
2
|
+
import { createContext, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore, useState, useLayoutEffect } from 'react';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
import Hls from 'hls.js';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
5
6
|
|
|
6
7
|
// src/types/content.ts
|
|
7
8
|
function isVideoItem(item) {
|
|
@@ -100,6 +101,7 @@ var PlayerEngine = class {
|
|
|
100
101
|
this.circuitResetTimer = null;
|
|
101
102
|
this.watchTimeInterval = null;
|
|
102
103
|
this.lastStatus = "idle" /* IDLE */;
|
|
104
|
+
this._destroyed = false;
|
|
103
105
|
this.config = { ...DEFAULT_PLAYER_CONFIG, ...config };
|
|
104
106
|
this.analytics = analytics;
|
|
105
107
|
this.logger = logger;
|
|
@@ -309,8 +311,13 @@ var PlayerEngine = class {
|
|
|
309
311
|
this.lastStatus = "idle" /* IDLE */;
|
|
310
312
|
}
|
|
311
313
|
destroy() {
|
|
314
|
+
if (this._destroyed) return;
|
|
315
|
+
this._destroyed = true;
|
|
312
316
|
this.stopWatchTime();
|
|
313
|
-
if (this.circuitResetTimer)
|
|
317
|
+
if (this.circuitResetTimer) {
|
|
318
|
+
clearTimeout(this.circuitResetTimer);
|
|
319
|
+
this.circuitResetTimer = null;
|
|
320
|
+
}
|
|
314
321
|
this.listeners.clear();
|
|
315
322
|
}
|
|
316
323
|
// ═══════════════════════════════════════════
|
|
@@ -413,6 +420,8 @@ var FeedManager = class {
|
|
|
413
420
|
this.inFlightRequests = /* @__PURE__ */ new Map();
|
|
414
421
|
/** LRU tracking: itemId → lastAccessTime */
|
|
415
422
|
this.accessOrder = /* @__PURE__ */ new Map();
|
|
423
|
+
/** Idempotency guard for destroy() */
|
|
424
|
+
this._destroyed = false;
|
|
416
425
|
/** Prefetch cache — instance-scoped (not static) */
|
|
417
426
|
this.prefetchCache = null;
|
|
418
427
|
this.config = { ...DEFAULT_FEED_CONFIG, ...config };
|
|
@@ -438,6 +447,13 @@ var FeedManager = class {
|
|
|
438
447
|
// ═══════════════════════════════════════════
|
|
439
448
|
// PUBLIC API — Prefetch
|
|
440
449
|
// ═══════════════════════════════════════════
|
|
450
|
+
setInitialItems(items) {
|
|
451
|
+
this.prefetchCache = {
|
|
452
|
+
items,
|
|
453
|
+
nextCursor: null,
|
|
454
|
+
timestamp: Date.now()
|
|
455
|
+
};
|
|
456
|
+
}
|
|
441
457
|
async prefetch(ttlMs) {
|
|
442
458
|
if (this.prefetchCache) {
|
|
443
459
|
const ttl = ttlMs ?? this.config.staleTTL;
|
|
@@ -518,7 +534,10 @@ var FeedManager = class {
|
|
|
518
534
|
// PUBLIC API — Lifecycle
|
|
519
535
|
// ═══════════════════════════════════════════
|
|
520
536
|
destroy() {
|
|
537
|
+
if (this._destroyed) return;
|
|
538
|
+
this._destroyed = true;
|
|
521
539
|
this.abortController?.abort();
|
|
540
|
+
this.abortController = null;
|
|
522
541
|
this.inFlightRequests.clear();
|
|
523
542
|
this.accessOrder.clear();
|
|
524
543
|
this.prefetchCache = null;
|
|
@@ -587,7 +606,13 @@ var FeedManager = class {
|
|
|
587
606
|
applyItems(incoming, _nextCursor, append) {
|
|
588
607
|
const { itemsById, displayOrder } = this.store.getState();
|
|
589
608
|
const nextById = new Map(itemsById);
|
|
609
|
+
const seenInBatch = /* @__PURE__ */ new Set();
|
|
610
|
+
const deduped = [];
|
|
590
611
|
for (const item of incoming) {
|
|
612
|
+
if (!seenInBatch.has(item.id)) {
|
|
613
|
+
seenInBatch.add(item.id);
|
|
614
|
+
deduped.push(item);
|
|
615
|
+
}
|
|
591
616
|
nextById.set(item.id, item);
|
|
592
617
|
this.accessOrder.set(item.id, Date.now());
|
|
593
618
|
}
|
|
@@ -595,14 +620,14 @@ var FeedManager = class {
|
|
|
595
620
|
if (append) {
|
|
596
621
|
const existingIds = new Set(displayOrder);
|
|
597
622
|
const newIds = [];
|
|
598
|
-
for (const item of
|
|
623
|
+
for (const item of deduped) {
|
|
599
624
|
if (!existingIds.has(item.id)) {
|
|
600
625
|
newIds.push(item.id);
|
|
601
626
|
}
|
|
602
627
|
}
|
|
603
628
|
nextOrder = [...displayOrder, ...newIds];
|
|
604
629
|
} else {
|
|
605
|
-
nextOrder =
|
|
630
|
+
nextOrder = deduped.map((item) => item.id);
|
|
606
631
|
}
|
|
607
632
|
if (nextById.size > this.config.maxCacheSize) {
|
|
608
633
|
this.evictLRU(nextById, nextOrder);
|
|
@@ -644,6 +669,8 @@ var OptimisticManager = class {
|
|
|
644
669
|
this.likeDebounceTimers = /* @__PURE__ */ new Map();
|
|
645
670
|
/** Pending like direction: contentId → final intended state */
|
|
646
671
|
this.pendingLikeState = /* @__PURE__ */ new Map();
|
|
672
|
+
/** Idempotency guard for destroy() */
|
|
673
|
+
this._destroyed = false;
|
|
647
674
|
this.interaction = interaction;
|
|
648
675
|
this.logger = logger;
|
|
649
676
|
this.store = createStore(createInitialState3);
|
|
@@ -734,6 +761,8 @@ var OptimisticManager = class {
|
|
|
734
761
|
// PUBLIC API — Lifecycle
|
|
735
762
|
// ═══════════════════════════════════════════
|
|
736
763
|
destroy() {
|
|
764
|
+
if (this._destroyed) return;
|
|
765
|
+
this._destroyed = true;
|
|
737
766
|
for (const timer of this.likeDebounceTimers.values()) {
|
|
738
767
|
clearTimeout(timer);
|
|
739
768
|
}
|
|
@@ -755,6 +784,7 @@ var DEFAULT_RESOURCE_CONFIG = {
|
|
|
755
784
|
var ResourceGovernor = class {
|
|
756
785
|
constructor(config = {}, videoLoader, network, logger) {
|
|
757
786
|
this.focusDebounceTimer = null;
|
|
787
|
+
this._destroyed = false;
|
|
758
788
|
this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
|
|
759
789
|
this.videoLoader = videoLoader;
|
|
760
790
|
this.network = network;
|
|
@@ -785,11 +815,19 @@ var ResourceGovernor = class {
|
|
|
785
815
|
this.logger?.debug("[ResourceGovernor] Activated");
|
|
786
816
|
}
|
|
787
817
|
deactivate() {
|
|
818
|
+
if (this._destroyed) return;
|
|
819
|
+
if (!this.store.getState().isActive) return;
|
|
788
820
|
this.networkUnsubscribe?.();
|
|
789
|
-
|
|
821
|
+
this.networkUnsubscribe = void 0;
|
|
822
|
+
if (this.focusDebounceTimer) {
|
|
823
|
+
clearTimeout(this.focusDebounceTimer);
|
|
824
|
+
this.focusDebounceTimer = null;
|
|
825
|
+
}
|
|
790
826
|
this.store.setState({ isActive: false });
|
|
791
827
|
}
|
|
792
828
|
destroy() {
|
|
829
|
+
if (this._destroyed) return;
|
|
830
|
+
this._destroyed = true;
|
|
793
831
|
this.deactivate();
|
|
794
832
|
this.videoLoader?.clearAll();
|
|
795
833
|
}
|
|
@@ -913,6 +951,153 @@ var ResourceGovernor = class {
|
|
|
913
951
|
);
|
|
914
952
|
}
|
|
915
953
|
};
|
|
954
|
+
var DEFAULT_NAVIGATION_CONFIG = {
|
|
955
|
+
phaseTimeoutMs: 1200
|
|
956
|
+
};
|
|
957
|
+
var VALID_NAV_TRANSITIONS = {
|
|
958
|
+
closed: ["opening"],
|
|
959
|
+
// Allow opening → closing so a fast user can dismiss mid-open.
|
|
960
|
+
opening: ["open", "closing", "closed"],
|
|
961
|
+
open: ["closing"],
|
|
962
|
+
// Allow closing → opening so re-opening during the exit animation works.
|
|
963
|
+
closing: ["closed", "opening"]
|
|
964
|
+
};
|
|
965
|
+
function isValidNavTransition(from, to) {
|
|
966
|
+
if (from === to) return false;
|
|
967
|
+
return VALID_NAV_TRANSITIONS[from].includes(to);
|
|
968
|
+
}
|
|
969
|
+
var NavigationManager = class {
|
|
970
|
+
constructor(config = {}, logger) {
|
|
971
|
+
/** Fallback timer guarding against a missing animationend report. */
|
|
972
|
+
this.phaseTimer = null;
|
|
973
|
+
this._destroyed = false;
|
|
974
|
+
this.config = { ...DEFAULT_NAVIGATION_CONFIG, ...config };
|
|
975
|
+
this.logger = logger;
|
|
976
|
+
this.store = createStore(() => ({
|
|
977
|
+
phase: "closed",
|
|
978
|
+
openIndex: null
|
|
979
|
+
}));
|
|
980
|
+
}
|
|
981
|
+
// ═══════════════════════════════════════════
|
|
982
|
+
// PUBLIC API — Intent
|
|
983
|
+
// ═══════════════════════════════════════════
|
|
984
|
+
/**
|
|
985
|
+
* Open the reels viewer at `index`. Safe to call from a thumbnail click
|
|
986
|
+
* handler — it transitions into `opening`, at which point ReelsModal mounts
|
|
987
|
+
* <ReelsFeed> hidden and begins prewarming.
|
|
988
|
+
*
|
|
989
|
+
* Calling open() while already open/opening just retargets the index
|
|
990
|
+
* (e.g. user taps a different thumbnail before the animation settles).
|
|
991
|
+
*/
|
|
992
|
+
open(index) {
|
|
993
|
+
if (this._destroyed) return;
|
|
994
|
+
const { phase, openIndex } = this.store.getState();
|
|
995
|
+
if ((phase === "open" || phase === "opening") && index === openIndex) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (phase === "open" || phase === "opening") {
|
|
999
|
+
this.store.setState({ openIndex: index });
|
|
1000
|
+
this.logger?.debug(`[NavigationManager] retarget openIndex=${index}`);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (!this.transition("opening")) return;
|
|
1004
|
+
this.store.setState({ openIndex: index });
|
|
1005
|
+
this.logger?.debug(`[NavigationManager] open(${index}) \u2192 opening`);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Begin closing the viewer. ReelsModal animates out then calls
|
|
1009
|
+
* `setPhase('closed')`. Idempotent if already closing/closed.
|
|
1010
|
+
*/
|
|
1011
|
+
close() {
|
|
1012
|
+
if (this._destroyed) return;
|
|
1013
|
+
const { phase } = this.store.getState();
|
|
1014
|
+
if (phase === "closed" || phase === "closing") return;
|
|
1015
|
+
this.transition("closing");
|
|
1016
|
+
this.logger?.debug("[NavigationManager] close() \u2192 closing");
|
|
1017
|
+
}
|
|
1018
|
+
// ═══════════════════════════════════════════
|
|
1019
|
+
// PUBLIC API — Phase reporting (called by ReelsModal)
|
|
1020
|
+
// ═══════════════════════════════════════════
|
|
1021
|
+
/**
|
|
1022
|
+
* Report an animation-driven phase change from the component layer.
|
|
1023
|
+
* Invalid transitions are dropped. When reaching `closed`, openIndex is
|
|
1024
|
+
* cleared so <ReelsFeed> unmounts.
|
|
1025
|
+
*/
|
|
1026
|
+
setPhase(next) {
|
|
1027
|
+
if (this._destroyed) return;
|
|
1028
|
+
this.transition(next);
|
|
1029
|
+
}
|
|
1030
|
+
// ═══════════════════════════════════════════
|
|
1031
|
+
// PUBLIC API — Queries
|
|
1032
|
+
// ═══════════════════════════════════════════
|
|
1033
|
+
getPhase() {
|
|
1034
|
+
return this.store.getState().phase;
|
|
1035
|
+
}
|
|
1036
|
+
getOpenIndex() {
|
|
1037
|
+
return this.store.getState().openIndex;
|
|
1038
|
+
}
|
|
1039
|
+
isOpen() {
|
|
1040
|
+
const { phase } = this.store.getState();
|
|
1041
|
+
return phase === "open" || phase === "opening";
|
|
1042
|
+
}
|
|
1043
|
+
/** Whether <ReelsFeed> should be mounted (any non-closed phase). */
|
|
1044
|
+
shouldMount() {
|
|
1045
|
+
return this.store.getState().phase !== "closed";
|
|
1046
|
+
}
|
|
1047
|
+
// ═══════════════════════════════════════════
|
|
1048
|
+
// PUBLIC API — Lifecycle
|
|
1049
|
+
// ═══════════════════════════════════════════
|
|
1050
|
+
destroy() {
|
|
1051
|
+
if (this._destroyed) return;
|
|
1052
|
+
this._destroyed = true;
|
|
1053
|
+
if (this.phaseTimer) {
|
|
1054
|
+
clearTimeout(this.phaseTimer);
|
|
1055
|
+
this.phaseTimer = null;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// ═══════════════════════════════════════════
|
|
1059
|
+
// PRIVATE
|
|
1060
|
+
// ═══════════════════════════════════════════
|
|
1061
|
+
/**
|
|
1062
|
+
* Apply a guarded phase transition. Returns true if it was applied.
|
|
1063
|
+
* Manages the fallback timer for transient phases (opening/closing).
|
|
1064
|
+
*/
|
|
1065
|
+
transition(next) {
|
|
1066
|
+
const { phase } = this.store.getState();
|
|
1067
|
+
if (!isValidNavTransition(phase, next)) {
|
|
1068
|
+
this.logger?.debug(
|
|
1069
|
+
`[NavigationManager] dropped invalid transition ${phase} \u2192 ${next}`
|
|
1070
|
+
);
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
this.clearPhaseTimer();
|
|
1074
|
+
if (next === "closed") {
|
|
1075
|
+
this.store.setState({ phase: "closed", openIndex: null });
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
this.store.setState({ phase: next });
|
|
1079
|
+
if (next === "opening" || next === "closing") {
|
|
1080
|
+
const fallbackTarget = next === "opening" ? "open" : "closed";
|
|
1081
|
+
this.phaseTimer = setTimeout(() => {
|
|
1082
|
+
this.phaseTimer = null;
|
|
1083
|
+
const current = this.store.getState().phase;
|
|
1084
|
+
if (current === next) {
|
|
1085
|
+
this.logger?.warn(
|
|
1086
|
+
`[NavigationManager] phase fallback ${next} \u2192 ${fallbackTarget}`
|
|
1087
|
+
);
|
|
1088
|
+
this.transition(fallbackTarget);
|
|
1089
|
+
}
|
|
1090
|
+
}, this.config.phaseTimeoutMs);
|
|
1091
|
+
}
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
clearPhaseTimer() {
|
|
1095
|
+
if (this.phaseTimer) {
|
|
1096
|
+
clearTimeout(this.phaseTimer);
|
|
1097
|
+
this.phaseTimer = null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
916
1101
|
function usePointerGesture(config = {}) {
|
|
917
1102
|
const {
|
|
918
1103
|
axis = "y",
|
|
@@ -1102,7 +1287,9 @@ function useSnapAnimation(config = {}) {
|
|
|
1102
1287
|
}
|
|
1103
1288
|
);
|
|
1104
1289
|
anim.addEventListener("finish", () => {
|
|
1105
|
-
element.
|
|
1290
|
+
if (element.isConnected) {
|
|
1291
|
+
element.style.transform = `translateY(${toY}px)`;
|
|
1292
|
+
}
|
|
1106
1293
|
anim.cancel();
|
|
1107
1294
|
});
|
|
1108
1295
|
animations.push(anim);
|
|
@@ -1130,31 +1317,100 @@ function useSnapAnimation(config = {}) {
|
|
|
1130
1317
|
}, [cancelAnimation]);
|
|
1131
1318
|
return { animateSnap, animateBounceBack, cancelAnimation };
|
|
1132
1319
|
}
|
|
1320
|
+
var noop = () => {
|
|
1321
|
+
};
|
|
1322
|
+
var noopAsync = () => Promise.resolve();
|
|
1323
|
+
var DEFAULT_LOGGER = {
|
|
1324
|
+
debug: noop,
|
|
1325
|
+
info: noop,
|
|
1326
|
+
warn: noop,
|
|
1327
|
+
error: noop
|
|
1328
|
+
};
|
|
1329
|
+
var DEFAULT_ANALYTICS = {
|
|
1330
|
+
trackView: noop,
|
|
1331
|
+
trackLike: noop,
|
|
1332
|
+
trackShare: noop,
|
|
1333
|
+
trackComment: noop,
|
|
1334
|
+
trackError: noop,
|
|
1335
|
+
trackPlaybackEvent: noop
|
|
1336
|
+
};
|
|
1337
|
+
var DEFAULT_INTERACTION = {
|
|
1338
|
+
like: noopAsync,
|
|
1339
|
+
unlike: noopAsync,
|
|
1340
|
+
follow: noopAsync,
|
|
1341
|
+
unfollow: noopAsync,
|
|
1342
|
+
bookmark: noopAsync,
|
|
1343
|
+
unbookmark: noopAsync,
|
|
1344
|
+
share: noopAsync
|
|
1345
|
+
};
|
|
1346
|
+
var DEFAULT_STORAGE = {
|
|
1347
|
+
get: () => null,
|
|
1348
|
+
set: noop,
|
|
1349
|
+
remove: noop,
|
|
1350
|
+
clear: noop
|
|
1351
|
+
};
|
|
1352
|
+
var DEFAULT_NETWORK = {
|
|
1353
|
+
getNetworkType: () => "unknown",
|
|
1354
|
+
isOnline: () => true,
|
|
1355
|
+
onNetworkChange: () => noop
|
|
1356
|
+
};
|
|
1357
|
+
var DEFAULT_VIDEO_LOADER = {
|
|
1358
|
+
preload: (videoId) => Promise.resolve({ videoId, status: "idle" }),
|
|
1359
|
+
cancel: noop,
|
|
1360
|
+
isPreloaded: () => false,
|
|
1361
|
+
getPreloadStatus: () => "idle",
|
|
1362
|
+
clearAll: noop
|
|
1363
|
+
};
|
|
1364
|
+
var DEFAULT_COMMENT = {
|
|
1365
|
+
fetchComments: () => Promise.resolve({ items: [], nextCursor: null, hasMore: false, total: 0 }),
|
|
1366
|
+
postComment: () => Promise.reject(new Error("Comment adapter not configured")),
|
|
1367
|
+
deleteComment: noopAsync,
|
|
1368
|
+
likeComment: noopAsync,
|
|
1369
|
+
unlikeComment: noopAsync
|
|
1370
|
+
};
|
|
1133
1371
|
var SDKContext = createContext(null);
|
|
1134
|
-
function ReelsProvider({
|
|
1135
|
-
|
|
1136
|
-
|
|
1372
|
+
function ReelsProvider({
|
|
1373
|
+
children,
|
|
1374
|
+
adapters,
|
|
1375
|
+
initialItems,
|
|
1376
|
+
debug = false,
|
|
1377
|
+
navigationConfig
|
|
1378
|
+
}) {
|
|
1379
|
+
const resolvedAdapters = useMemo(() => ({
|
|
1380
|
+
dataSource: adapters.dataSource,
|
|
1381
|
+
interaction: adapters.interaction ?? DEFAULT_INTERACTION,
|
|
1382
|
+
storage: adapters.storage ?? DEFAULT_STORAGE,
|
|
1383
|
+
analytics: adapters.analytics ?? DEFAULT_ANALYTICS,
|
|
1384
|
+
logger: adapters.logger ?? DEFAULT_LOGGER,
|
|
1385
|
+
network: adapters.network ?? DEFAULT_NETWORK,
|
|
1386
|
+
videoLoader: adapters.videoLoader ?? DEFAULT_VIDEO_LOADER,
|
|
1387
|
+
comment: adapters.comment ?? DEFAULT_COMMENT
|
|
1388
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1389
|
+
}), [adapters.dataSource]);
|
|
1390
|
+
const logger = resolvedAdapters.logger;
|
|
1391
|
+
const prevInstanceRef = useRef(null);
|
|
1137
1392
|
const value = useMemo(() => {
|
|
1138
|
-
if (sdkRef.current) {
|
|
1139
|
-
sdkRef.current.feedManager.destroy();
|
|
1140
|
-
sdkRef.current.playerEngine.destroy();
|
|
1141
|
-
sdkRef.current.resourceGovernor.destroy();
|
|
1142
|
-
sdkRef.current.optimisticManager.destroy();
|
|
1143
|
-
}
|
|
1144
1393
|
const feedManager = new FeedManager(adapters.dataSource, {}, logger);
|
|
1394
|
+
if (initialItems && initialItems.length > 0) {
|
|
1395
|
+
feedManager.setInitialItems(initialItems);
|
|
1396
|
+
}
|
|
1145
1397
|
const playerEngine = new PlayerEngine(
|
|
1146
1398
|
{},
|
|
1147
|
-
|
|
1399
|
+
resolvedAdapters.analytics,
|
|
1148
1400
|
logger
|
|
1149
1401
|
);
|
|
1150
1402
|
const resourceGovernor = new ResourceGovernor(
|
|
1151
1403
|
{},
|
|
1152
|
-
|
|
1153
|
-
|
|
1404
|
+
resolvedAdapters.videoLoader,
|
|
1405
|
+
resolvedAdapters.network,
|
|
1154
1406
|
logger
|
|
1155
1407
|
);
|
|
1156
1408
|
const optimisticManager = new OptimisticManager(
|
|
1157
|
-
|
|
1409
|
+
resolvedAdapters.interaction,
|
|
1410
|
+
logger
|
|
1411
|
+
);
|
|
1412
|
+
const navigationManager = new NavigationManager(
|
|
1413
|
+
navigationConfig ?? {},
|
|
1158
1414
|
logger
|
|
1159
1415
|
);
|
|
1160
1416
|
const instance = {
|
|
@@ -1162,15 +1418,27 @@ function ReelsProvider({ children, adapters, debug = false }) {
|
|
|
1162
1418
|
playerEngine,
|
|
1163
1419
|
resourceGovernor,
|
|
1164
1420
|
optimisticManager,
|
|
1165
|
-
|
|
1421
|
+
navigationManager,
|
|
1422
|
+
adapters: resolvedAdapters
|
|
1166
1423
|
};
|
|
1167
|
-
sdkRef.current = instance;
|
|
1168
1424
|
return instance;
|
|
1169
1425
|
}, [adapters.dataSource]);
|
|
1170
1426
|
useEffect(() => {
|
|
1171
|
-
|
|
1427
|
+
const prev = prevInstanceRef.current;
|
|
1428
|
+
if (prev && prev !== value) {
|
|
1429
|
+
prev.feedManager.destroy();
|
|
1430
|
+
prev.playerEngine.destroy();
|
|
1431
|
+
prev.resourceGovernor.destroy();
|
|
1432
|
+
prev.optimisticManager.destroy();
|
|
1433
|
+
prev.navigationManager.destroy();
|
|
1434
|
+
}
|
|
1435
|
+
prevInstanceRef.current = value;
|
|
1436
|
+
}, [value]);
|
|
1437
|
+
useEffect(() => {
|
|
1438
|
+
const governor = value.resourceGovernor;
|
|
1439
|
+
governor.activate();
|
|
1172
1440
|
return () => {
|
|
1173
|
-
|
|
1441
|
+
governor.deactivate();
|
|
1174
1442
|
};
|
|
1175
1443
|
}, [value.resourceGovernor]);
|
|
1176
1444
|
useEffect(() => {
|
|
@@ -1180,10 +1448,11 @@ function ReelsProvider({ children, adapters, debug = false }) {
|
|
|
1180
1448
|
}, [debug, logger]);
|
|
1181
1449
|
useEffect(() => {
|
|
1182
1450
|
return () => {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1451
|
+
prevInstanceRef.current?.feedManager.destroy();
|
|
1452
|
+
prevInstanceRef.current?.playerEngine.destroy();
|
|
1453
|
+
prevInstanceRef.current?.resourceGovernor.destroy();
|
|
1454
|
+
prevInstanceRef.current?.optimisticManager.destroy();
|
|
1455
|
+
prevInstanceRef.current?.navigationManager.destroy();
|
|
1187
1456
|
};
|
|
1188
1457
|
}, []);
|
|
1189
1458
|
return /* @__PURE__ */ jsx(SDKContext.Provider, { value, children });
|
|
@@ -1330,10 +1599,11 @@ var ACTIVE_HLS_DEFAULTS = {
|
|
|
1330
1599
|
maxMaxBufferLength: 15,
|
|
1331
1600
|
capLevelToPlayerSize: true,
|
|
1332
1601
|
startLevel: 0,
|
|
1333
|
-
abrEwmaDefaultEstimate:
|
|
1602
|
+
abrEwmaDefaultEstimate: 2e6,
|
|
1334
1603
|
lowLatencyMode: false,
|
|
1335
1604
|
backBufferLength: 5,
|
|
1336
|
-
enableWorker: true
|
|
1605
|
+
enableWorker: true,
|
|
1606
|
+
startFragPrefetch: true
|
|
1337
1607
|
};
|
|
1338
1608
|
var HOT_HLS_DEFAULTS = {
|
|
1339
1609
|
maxBufferLength: 2,
|
|
@@ -1686,25 +1956,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
|
1686
1956
|
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1687
1957
|
var _playAheadActive = 0;
|
|
1688
1958
|
var _playAheadQueue = [];
|
|
1959
|
+
var _tokenCounter = 0;
|
|
1960
|
+
var _releasedTokens = /* @__PURE__ */ new Set();
|
|
1689
1961
|
function acquirePlayAhead() {
|
|
1962
|
+
const token = ++_tokenCounter;
|
|
1963
|
+
const makeRelease = () => {
|
|
1964
|
+
let released = false;
|
|
1965
|
+
return () => {
|
|
1966
|
+
if (released) return;
|
|
1967
|
+
released = true;
|
|
1968
|
+
_releasedTokens.add(token);
|
|
1969
|
+
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1970
|
+
const next = _playAheadQueue.shift();
|
|
1971
|
+
if (next) {
|
|
1972
|
+
next();
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
};
|
|
1690
1976
|
if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
|
|
1691
1977
|
_playAheadActive++;
|
|
1692
|
-
return Promise.resolve();
|
|
1978
|
+
return Promise.resolve(makeRelease());
|
|
1693
1979
|
}
|
|
1694
1980
|
return new Promise((resolve) => {
|
|
1695
1981
|
_playAheadQueue.push(() => {
|
|
1696
|
-
|
|
1982
|
+
_playAheadActive++;
|
|
1983
|
+
setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
|
|
1697
1984
|
});
|
|
1698
1985
|
});
|
|
1699
1986
|
}
|
|
1700
|
-
function releasePlayAhead() {
|
|
1701
|
-
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1702
|
-
const next = _playAheadQueue.shift();
|
|
1703
|
-
if (next) {
|
|
1704
|
-
_playAheadActive++;
|
|
1705
|
-
next();
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
1987
|
function VideoSlot({
|
|
1709
1988
|
item,
|
|
1710
1989
|
index,
|
|
@@ -1827,6 +2106,56 @@ function VideoSlotInner({
|
|
|
1827
2106
|
}
|
|
1828
2107
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
1829
2108
|
const isReady = isHlsSource ? hlsReady : mp4Ready;
|
|
2109
|
+
const [capturedPoster, setCapturedPoster] = useState(null);
|
|
2110
|
+
useEffect(() => {
|
|
2111
|
+
const video = videoRef.current;
|
|
2112
|
+
if (!video || !shouldLoadSrc) return;
|
|
2113
|
+
let cancelled = false;
|
|
2114
|
+
const captureFrame = () => {
|
|
2115
|
+
if (cancelled) return;
|
|
2116
|
+
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
2117
|
+
if (video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2118
|
+
try {
|
|
2119
|
+
const canvas = document.createElement("canvas");
|
|
2120
|
+
canvas.width = video.videoWidth;
|
|
2121
|
+
canvas.height = video.videoHeight;
|
|
2122
|
+
const ctx = canvas.getContext("2d");
|
|
2123
|
+
if (!ctx) return;
|
|
2124
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
2125
|
+
const dataUrl = canvas.toDataURL("image/webp", 0.85);
|
|
2126
|
+
if (!cancelled) {
|
|
2127
|
+
setCapturedPoster(dataUrl);
|
|
2128
|
+
}
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
|
|
2133
|
+
captureFrame();
|
|
2134
|
+
} else {
|
|
2135
|
+
video.addEventListener("loadeddata", captureFrame, { once: true });
|
|
2136
|
+
}
|
|
2137
|
+
return () => {
|
|
2138
|
+
cancelled = true;
|
|
2139
|
+
video.removeEventListener("loadeddata", captureFrame);
|
|
2140
|
+
};
|
|
2141
|
+
}, [src, shouldLoadSrc]);
|
|
2142
|
+
useEffect(() => {
|
|
2143
|
+
setCapturedPoster(null);
|
|
2144
|
+
}, [src]);
|
|
2145
|
+
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
|
2146
|
+
useEffect(() => {
|
|
2147
|
+
const video = videoRef.current;
|
|
2148
|
+
if (!video || !isActive) {
|
|
2149
|
+
setIsVideoPlaying(false);
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
const onPlaying = () => setIsVideoPlaying(true);
|
|
2153
|
+
video.addEventListener("playing", onPlaying);
|
|
2154
|
+
return () => {
|
|
2155
|
+
video.removeEventListener("playing", onPlaying);
|
|
2156
|
+
setIsVideoPlaying(false);
|
|
2157
|
+
};
|
|
2158
|
+
}, [isActive]);
|
|
1830
2159
|
const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
|
|
1831
2160
|
useEffect(() => {
|
|
1832
2161
|
const video = videoRef.current;
|
|
@@ -1836,36 +2165,43 @@ function VideoSlotInner({
|
|
|
1836
2165
|
const prevMuted = video.muted;
|
|
1837
2166
|
video.muted = true;
|
|
1838
2167
|
let cancelled = false;
|
|
2168
|
+
let release = null;
|
|
1839
2169
|
const doPlayAhead = async () => {
|
|
1840
|
-
await acquirePlayAhead();
|
|
2170
|
+
release = await acquirePlayAhead();
|
|
2171
|
+
if (cancelled) {
|
|
2172
|
+
release();
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
1841
2175
|
try {
|
|
1842
2176
|
await video.play();
|
|
1843
2177
|
if (cancelled) {
|
|
1844
2178
|
video.pause();
|
|
1845
|
-
|
|
2179
|
+
release();
|
|
1846
2180
|
return;
|
|
1847
2181
|
}
|
|
1848
2182
|
const pauseAfterDecode = () => {
|
|
1849
2183
|
video.pause();
|
|
1850
2184
|
video.currentTime = 0;
|
|
1851
2185
|
video.muted = prevMuted;
|
|
1852
|
-
|
|
2186
|
+
release?.();
|
|
1853
2187
|
if (!cancelled) {
|
|
1854
2188
|
setHasPlayedAhead(true);
|
|
1855
2189
|
}
|
|
1856
2190
|
};
|
|
1857
2191
|
setTimeout(pauseAfterDecode, 50);
|
|
1858
2192
|
} catch {
|
|
1859
|
-
|
|
2193
|
+
release?.();
|
|
1860
2194
|
}
|
|
1861
2195
|
};
|
|
1862
2196
|
doPlayAhead();
|
|
1863
2197
|
return () => {
|
|
1864
2198
|
cancelled = true;
|
|
2199
|
+
release?.();
|
|
1865
2200
|
};
|
|
1866
2201
|
}, [isActive, isReady, hasPlayedAhead]);
|
|
1867
2202
|
useEffect(() => {
|
|
1868
2203
|
setHasPlayedAhead(false);
|
|
2204
|
+
setIsVideoPlaying(false);
|
|
1869
2205
|
}, [src]);
|
|
1870
2206
|
const wasActiveRef = useRef(false);
|
|
1871
2207
|
const [isManuallyPaused, setIsManuallyPaused] = useState(false);
|
|
@@ -1942,7 +2278,7 @@ function VideoSlotInner({
|
|
|
1942
2278
|
if (!video) return;
|
|
1943
2279
|
video.muted = isActive ? isMuted : true;
|
|
1944
2280
|
}, [isMuted, isActive]);
|
|
1945
|
-
const showPosterOverlay = !isReady && !hasPlayedAhead;
|
|
2281
|
+
const showPosterOverlay = isActive ? !isVideoPlaying : !isReady && !hasPlayedAhead;
|
|
1946
2282
|
const isPreDecoded = hasPlayedAhead;
|
|
1947
2283
|
const [showMuteIndicator, setShowMuteIndicator] = useState(false);
|
|
1948
2284
|
const muteIndicatorTimer = useRef(null);
|
|
@@ -2060,23 +2396,23 @@ function VideoSlotInner({
|
|
|
2060
2396
|
height: "100%",
|
|
2061
2397
|
objectFit: "cover",
|
|
2062
2398
|
// Hide video until ready to avoid black frame flash.
|
|
2063
|
-
// When pre-decoded, skip transition — first frame is already on canvas.
|
|
2399
|
+
// When pre-decoded or active, skip transition — first frame is already on canvas or playing.
|
|
2064
2400
|
opacity: showPosterOverlay ? 0 : 1,
|
|
2065
|
-
transition: isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2401
|
+
transition: isActive ? "none" : isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2066
2402
|
}
|
|
2067
2403
|
}
|
|
2068
2404
|
),
|
|
2069
|
-
item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2405
|
+
(capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2070
2406
|
"div",
|
|
2071
2407
|
{
|
|
2072
2408
|
style: {
|
|
2073
2409
|
position: "absolute",
|
|
2074
2410
|
inset: 0,
|
|
2075
|
-
backgroundImage: `url(${item.poster})`,
|
|
2411
|
+
backgroundImage: `url(${capturedPoster || item.poster})`,
|
|
2076
2412
|
backgroundSize: "cover",
|
|
2077
2413
|
backgroundPosition: "center",
|
|
2078
2414
|
opacity: showPosterOverlay ? 1 : 0,
|
|
2079
|
-
transition: "opacity 0.15s ease",
|
|
2415
|
+
transition: isActive ? "none" : "opacity 0.15s ease",
|
|
2080
2416
|
pointerEvents: "none"
|
|
2081
2417
|
}
|
|
2082
2418
|
}
|
|
@@ -2284,7 +2620,7 @@ function ReelsFeed({
|
|
|
2284
2620
|
}, 16);
|
|
2285
2621
|
};
|
|
2286
2622
|
const observer = new MutationObserver(debouncedRebuild);
|
|
2287
|
-
observer.observe(container, { childList: true, subtree:
|
|
2623
|
+
observer.observe(container, { childList: true, subtree: false });
|
|
2288
2624
|
return () => {
|
|
2289
2625
|
observer.disconnect();
|
|
2290
2626
|
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
@@ -2521,6 +2857,56 @@ function parsePxTranslateY(el) {
|
|
|
2521
2857
|
if (!match || !match[1]) return 0;
|
|
2522
2858
|
return Number.parseFloat(match[1]);
|
|
2523
2859
|
}
|
|
2860
|
+
function useNavigationSelector(selector) {
|
|
2861
|
+
const { navigationManager } = useSDK();
|
|
2862
|
+
const selectorRef = useRef(selector);
|
|
2863
|
+
selectorRef.current = selector;
|
|
2864
|
+
const lastSnapshot = useRef(void 0);
|
|
2865
|
+
const lastState = useRef(void 0);
|
|
2866
|
+
const getSnapshot = useCallback(() => {
|
|
2867
|
+
const state = navigationManager.store.getState();
|
|
2868
|
+
if (state !== lastState.current) {
|
|
2869
|
+
lastState.current = state;
|
|
2870
|
+
lastSnapshot.current = selectorRef.current(state);
|
|
2871
|
+
}
|
|
2872
|
+
return lastSnapshot.current;
|
|
2873
|
+
}, [navigationManager]);
|
|
2874
|
+
return useSyncExternalStore(
|
|
2875
|
+
navigationManager.store.subscribe,
|
|
2876
|
+
getSnapshot,
|
|
2877
|
+
getSnapshot
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
function useNavigation() {
|
|
2881
|
+
const { navigationManager } = useSDK();
|
|
2882
|
+
const selectPhase = useCallback((s) => s.phase, []);
|
|
2883
|
+
const selectOpenIndex = useCallback((s) => s.openIndex, []);
|
|
2884
|
+
const phase = useNavigationSelector(selectPhase);
|
|
2885
|
+
const openIndex = useNavigationSelector(selectOpenIndex);
|
|
2886
|
+
const isOpen = phase === "open" || phase === "opening";
|
|
2887
|
+
const shouldMount = phase !== "closed";
|
|
2888
|
+
const open = useCallback(
|
|
2889
|
+
(index) => navigationManager.open(index),
|
|
2890
|
+
[navigationManager]
|
|
2891
|
+
);
|
|
2892
|
+
const close = useCallback(
|
|
2893
|
+
() => navigationManager.close(),
|
|
2894
|
+
[navigationManager]
|
|
2895
|
+
);
|
|
2896
|
+
const setPhase = useCallback(
|
|
2897
|
+
(next) => navigationManager.setPhase(next),
|
|
2898
|
+
[navigationManager]
|
|
2899
|
+
);
|
|
2900
|
+
return {
|
|
2901
|
+
phase,
|
|
2902
|
+
openIndex,
|
|
2903
|
+
isOpen,
|
|
2904
|
+
shouldMount,
|
|
2905
|
+
open,
|
|
2906
|
+
close,
|
|
2907
|
+
setPhase
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2524
2910
|
function ReelsFeedThumbnail({
|
|
2525
2911
|
renderThumbnail,
|
|
2526
2912
|
onThumbnailClick,
|
|
@@ -2530,33 +2916,48 @@ function ReelsFeedThumbnail({
|
|
|
2530
2916
|
className = "grid grid-cols-2 gap-3",
|
|
2531
2917
|
wrap = true,
|
|
2532
2918
|
setFocusOnClick = true,
|
|
2919
|
+
openOnClick = false,
|
|
2533
2920
|
prefetchOnHover = false,
|
|
2921
|
+
prewarmOnClick = true,
|
|
2534
2922
|
getKey
|
|
2535
2923
|
}) {
|
|
2536
2924
|
const { items, loading, error, refresh } = useFeed();
|
|
2537
2925
|
const { setFocusedIndexImmediate } = useResource();
|
|
2926
|
+
const { open } = useNavigation();
|
|
2538
2927
|
const { adapters } = useSDK();
|
|
2539
2928
|
const prefetchedRef = useRef(/* @__PURE__ */ new Set());
|
|
2929
|
+
const prewarm = useCallback(
|
|
2930
|
+
(item) => {
|
|
2931
|
+
if (!isVideoItem(item)) return;
|
|
2932
|
+
if (item.source.type !== "hls") return;
|
|
2933
|
+
const url = item.source.url;
|
|
2934
|
+
if (prefetchedRef.current.has(url)) return;
|
|
2935
|
+
prefetchedRef.current.add(url);
|
|
2936
|
+
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2937
|
+
},
|
|
2938
|
+
[adapters.videoLoader]
|
|
2939
|
+
);
|
|
2540
2940
|
const handleClick = useCallback(
|
|
2541
2941
|
(id, item, index) => {
|
|
2942
|
+
if (prewarmOnClick) {
|
|
2943
|
+
prewarm(item);
|
|
2944
|
+
}
|
|
2542
2945
|
if (setFocusOnClick) {
|
|
2543
2946
|
setFocusedIndexImmediate(index);
|
|
2544
2947
|
}
|
|
2948
|
+
if (openOnClick) {
|
|
2949
|
+
open(index);
|
|
2950
|
+
}
|
|
2545
2951
|
onThumbnailClick?.(id, item, index);
|
|
2546
2952
|
},
|
|
2547
|
-
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2953
|
+
[prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
|
|
2548
2954
|
);
|
|
2549
2955
|
const handlePointerEnter = useCallback(
|
|
2550
2956
|
(item) => {
|
|
2551
2957
|
if (!prefetchOnHover) return;
|
|
2552
|
-
|
|
2553
|
-
if (item.source.type !== "hls") return;
|
|
2554
|
-
const url = item.source.url;
|
|
2555
|
-
if (prefetchedRef.current.has(url)) return;
|
|
2556
|
-
prefetchedRef.current.add(url);
|
|
2557
|
-
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2958
|
+
prewarm(item);
|
|
2558
2959
|
},
|
|
2559
|
-
[prefetchOnHover,
|
|
2960
|
+
[prefetchOnHover, prewarm]
|
|
2560
2961
|
);
|
|
2561
2962
|
if (loading && items.length === 0) {
|
|
2562
2963
|
if (renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
|
|
@@ -2596,6 +2997,309 @@ function ReelsFeedThumbnail({
|
|
|
2596
2997
|
}
|
|
2597
2998
|
return /* @__PURE__ */ jsx("div", { className, children: content });
|
|
2598
2999
|
}
|
|
3000
|
+
var DEFAULT_DURATION = 300;
|
|
3001
|
+
var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
3002
|
+
var DEFAULT_DIRECTION = "up";
|
|
3003
|
+
var DEFAULT_Z_INDEX = 1e3;
|
|
3004
|
+
var DEFAULT_PREWARM_FORWARD = 2;
|
|
3005
|
+
function offscreenTransform(direction) {
|
|
3006
|
+
switch (direction) {
|
|
3007
|
+
case "down":
|
|
3008
|
+
return "translateY(-100%)";
|
|
3009
|
+
case "fade":
|
|
3010
|
+
return "translateY(0)";
|
|
3011
|
+
case "up":
|
|
3012
|
+
default:
|
|
3013
|
+
return "translateY(100%)";
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
function prefersReducedMotion() {
|
|
3017
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
3018
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3019
|
+
}
|
|
3020
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
|
|
3021
|
+
function ReelsModal({
|
|
3022
|
+
feedProps,
|
|
3023
|
+
animationConfig,
|
|
3024
|
+
renderBackdrop,
|
|
3025
|
+
renderCloseButton,
|
|
3026
|
+
onOpen,
|
|
3027
|
+
onClose,
|
|
3028
|
+
closeOnBackdropClick = true,
|
|
3029
|
+
closeOnEscape = true,
|
|
3030
|
+
lockBodyScroll = true,
|
|
3031
|
+
prewarmForward = DEFAULT_PREWARM_FORWARD,
|
|
3032
|
+
className,
|
|
3033
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
3034
|
+
portalTarget
|
|
3035
|
+
}) {
|
|
3036
|
+
const { phase, openIndex, shouldMount, close, setPhase } = useNavigation();
|
|
3037
|
+
const { setFocusedIndexImmediate } = useResource();
|
|
3038
|
+
const { items } = useFeed();
|
|
3039
|
+
const { adapters } = useSDK();
|
|
3040
|
+
const duration = animationConfig?.duration ?? DEFAULT_DURATION;
|
|
3041
|
+
const easing = animationConfig?.easing ?? DEFAULT_EASING;
|
|
3042
|
+
const direction = animationConfig?.direction ?? DEFAULT_DIRECTION;
|
|
3043
|
+
const backdropRef = useRef(null);
|
|
3044
|
+
const panelRef = useRef(null);
|
|
3045
|
+
const animationsRef = useRef([]);
|
|
3046
|
+
const previouslyFocusedRef = useRef(null);
|
|
3047
|
+
const renderState = useMemo(
|
|
3048
|
+
() => ({ phase, openIndex, close }),
|
|
3049
|
+
[phase, openIndex, close]
|
|
3050
|
+
);
|
|
3051
|
+
const prewarmedRef = useRef(null);
|
|
3052
|
+
useEffect(() => {
|
|
3053
|
+
if (phase !== "opening") {
|
|
3054
|
+
if (phase === "closed") prewarmedRef.current = null;
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (openIndex == null) return;
|
|
3058
|
+
if (prewarmedRef.current === openIndex) return;
|
|
3059
|
+
prewarmedRef.current = openIndex;
|
|
3060
|
+
setFocusedIndexImmediate(openIndex);
|
|
3061
|
+
const preload = adapters.videoLoader?.preloadMetadata;
|
|
3062
|
+
if (!preload) return;
|
|
3063
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3064
|
+
targets.add(openIndex);
|
|
3065
|
+
for (let d = 1; d <= prewarmForward; d++) targets.add(openIndex + d);
|
|
3066
|
+
targets.add(openIndex - 1);
|
|
3067
|
+
for (const idx of targets) {
|
|
3068
|
+
if (idx < 0 || idx >= items.length) continue;
|
|
3069
|
+
const item = items[idx];
|
|
3070
|
+
if (item && isVideoItem(item) && item.source.type === "hls") {
|
|
3071
|
+
preload(item.source.url);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}, [phase, openIndex, items, adapters.videoLoader, prewarmForward, setFocusedIndexImmediate]);
|
|
3075
|
+
useEffect(() => {
|
|
3076
|
+
if (!lockBodyScroll || !shouldMount) return;
|
|
3077
|
+
if (typeof document === "undefined") return;
|
|
3078
|
+
const prev = document.body.style.overflow;
|
|
3079
|
+
document.body.style.overflow = "hidden";
|
|
3080
|
+
return () => {
|
|
3081
|
+
document.body.style.overflow = prev;
|
|
3082
|
+
};
|
|
3083
|
+
}, [lockBodyScroll, shouldMount]);
|
|
3084
|
+
const cancelAnimations = useCallback(() => {
|
|
3085
|
+
for (const a of animationsRef.current) a.cancel();
|
|
3086
|
+
animationsRef.current = [];
|
|
3087
|
+
}, []);
|
|
3088
|
+
useLayoutEffect(() => {
|
|
3089
|
+
const panel = panelRef.current;
|
|
3090
|
+
const backdrop = backdropRef.current;
|
|
3091
|
+
if (!panel) return;
|
|
3092
|
+
if (phase === "open" || phase === "closed") return;
|
|
3093
|
+
const reduce = prefersReducedMotion();
|
|
3094
|
+
const enter = phase === "opening";
|
|
3095
|
+
const offscreen2 = offscreenTransform(direction);
|
|
3096
|
+
const panelFrames = direction === "fade" ? enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }] : enter ? [{ transform: offscreen2 }, { transform: "translateY(0)" }] : [{ transform: "translateY(0)" }, { transform: offscreen2 }];
|
|
3097
|
+
const backdropFrames = enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
|
|
3098
|
+
const animDuration = reduce ? 0 : duration;
|
|
3099
|
+
let cancelled = false;
|
|
3100
|
+
cancelAnimations();
|
|
3101
|
+
const anims = [];
|
|
3102
|
+
const panelAnim = panel.animate(panelFrames, {
|
|
3103
|
+
duration: animDuration,
|
|
3104
|
+
easing,
|
|
3105
|
+
fill: "forwards"
|
|
3106
|
+
});
|
|
3107
|
+
anims.push(panelAnim);
|
|
3108
|
+
if (backdrop) {
|
|
3109
|
+
anims.push(
|
|
3110
|
+
backdrop.animate(backdropFrames, {
|
|
3111
|
+
duration: animDuration,
|
|
3112
|
+
easing,
|
|
3113
|
+
fill: "forwards"
|
|
3114
|
+
})
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
animationsRef.current = anims;
|
|
3118
|
+
const finalize = () => {
|
|
3119
|
+
if (cancelled) return;
|
|
3120
|
+
if (enter) {
|
|
3121
|
+
if (direction === "fade") panel.style.opacity = "1";
|
|
3122
|
+
else panel.style.transform = "translateY(0)";
|
|
3123
|
+
if (backdrop) backdrop.style.opacity = "1";
|
|
3124
|
+
} else {
|
|
3125
|
+
if (direction === "fade") panel.style.opacity = "0";
|
|
3126
|
+
else panel.style.transform = offscreen2;
|
|
3127
|
+
if (backdrop) backdrop.style.opacity = "0";
|
|
3128
|
+
}
|
|
3129
|
+
for (const a of anims) a.cancel();
|
|
3130
|
+
animationsRef.current = [];
|
|
3131
|
+
setPhase(enter ? "open" : "closed");
|
|
3132
|
+
};
|
|
3133
|
+
panelAnim.addEventListener("finish", finalize);
|
|
3134
|
+
return () => {
|
|
3135
|
+
cancelled = true;
|
|
3136
|
+
panelAnim.removeEventListener("finish", finalize);
|
|
3137
|
+
cancelAnimations();
|
|
3138
|
+
};
|
|
3139
|
+
}, [phase, direction, duration, easing, setPhase, cancelAnimations]);
|
|
3140
|
+
const prevPhaseRef = useRef("closed");
|
|
3141
|
+
useEffect(() => {
|
|
3142
|
+
const prev = prevPhaseRef.current;
|
|
3143
|
+
if (prev !== "open" && phase === "open") onOpen?.(openIndex);
|
|
3144
|
+
if (prev !== "closed" && phase === "closed") onClose?.();
|
|
3145
|
+
prevPhaseRef.current = phase;
|
|
3146
|
+
}, [phase, openIndex, onOpen, onClose]);
|
|
3147
|
+
useEffect(() => {
|
|
3148
|
+
if (phase === "opening" && typeof document !== "undefined") {
|
|
3149
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
3150
|
+
const id = requestAnimationFrame(() => {
|
|
3151
|
+
const panel = panelRef.current;
|
|
3152
|
+
if (!panel) return;
|
|
3153
|
+
const first = panel.querySelector(FOCUSABLE);
|
|
3154
|
+
(first ?? panel).focus();
|
|
3155
|
+
});
|
|
3156
|
+
return () => cancelAnimationFrame(id);
|
|
3157
|
+
}
|
|
3158
|
+
if (phase === "closed") {
|
|
3159
|
+
previouslyFocusedRef.current?.focus?.();
|
|
3160
|
+
previouslyFocusedRef.current = null;
|
|
3161
|
+
}
|
|
3162
|
+
return void 0;
|
|
3163
|
+
}, [phase]);
|
|
3164
|
+
const handleKeyDown = useCallback(
|
|
3165
|
+
(e) => {
|
|
3166
|
+
if (closeOnEscape && e.key === "Escape") {
|
|
3167
|
+
e.stopPropagation();
|
|
3168
|
+
close();
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
if (e.key !== "Tab") return;
|
|
3172
|
+
const panel = panelRef.current;
|
|
3173
|
+
if (!panel) return;
|
|
3174
|
+
const focusables = Array.from(
|
|
3175
|
+
panel.querySelectorAll(FOCUSABLE)
|
|
3176
|
+
).filter((el) => el.offsetParent !== null);
|
|
3177
|
+
if (focusables.length === 0) {
|
|
3178
|
+
e.preventDefault();
|
|
3179
|
+
panel.focus();
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
const first = focusables[0];
|
|
3183
|
+
const last = focusables[focusables.length - 1];
|
|
3184
|
+
const active = document.activeElement;
|
|
3185
|
+
if (e.shiftKey && active === first) {
|
|
3186
|
+
e.preventDefault();
|
|
3187
|
+
last.focus();
|
|
3188
|
+
} else if (!e.shiftKey && active === last) {
|
|
3189
|
+
e.preventDefault();
|
|
3190
|
+
first.focus();
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
[closeOnEscape, close]
|
|
3194
|
+
);
|
|
3195
|
+
const handleBackdropClick = useCallback(() => {
|
|
3196
|
+
if (closeOnBackdropClick) close();
|
|
3197
|
+
}, [closeOnBackdropClick, close]);
|
|
3198
|
+
if (!shouldMount) return null;
|
|
3199
|
+
if (typeof document === "undefined") return null;
|
|
3200
|
+
const target = portalTarget ?? document.body;
|
|
3201
|
+
if (!target) return null;
|
|
3202
|
+
const enterStart = phase === "opening";
|
|
3203
|
+
const offscreen = offscreenTransform(direction);
|
|
3204
|
+
const panelInitialTransform = direction === "fade" ? "translateY(0)" : enterStart ? offscreen : "translateY(0)";
|
|
3205
|
+
const panelInitialOpacity = direction === "fade" ? enterStart ? 0 : 1 : 1;
|
|
3206
|
+
const backdropInitialOpacity = enterStart ? 0 : 1;
|
|
3207
|
+
const rootStyle = {
|
|
3208
|
+
position: "fixed",
|
|
3209
|
+
inset: 0,
|
|
3210
|
+
zIndex,
|
|
3211
|
+
// While opening, the panel is parked off-screen but MUST stay painted so
|
|
3212
|
+
// the <video> decodes its first frame. overflow:hidden keeps the
|
|
3213
|
+
// off-screen panel from creating scrollbars / capturing stray taps.
|
|
3214
|
+
overflow: "hidden"
|
|
3215
|
+
};
|
|
3216
|
+
const backdropStyle = {
|
|
3217
|
+
position: "absolute",
|
|
3218
|
+
inset: 0,
|
|
3219
|
+
opacity: backdropInitialOpacity,
|
|
3220
|
+
background: renderBackdrop ? "transparent" : "rgba(0,0,0,0.85)",
|
|
3221
|
+
// willChange hints the compositor for the opacity fade.
|
|
3222
|
+
willChange: "opacity"
|
|
3223
|
+
};
|
|
3224
|
+
const panelStyle = {
|
|
3225
|
+
position: "absolute",
|
|
3226
|
+
inset: 0,
|
|
3227
|
+
transform: panelInitialTransform,
|
|
3228
|
+
opacity: panelInitialOpacity,
|
|
3229
|
+
willChange: direction === "fade" ? "opacity" : "transform",
|
|
3230
|
+
outline: "none"
|
|
3231
|
+
};
|
|
3232
|
+
return createPortal(
|
|
3233
|
+
/* @__PURE__ */ jsxs(
|
|
3234
|
+
"div",
|
|
3235
|
+
{
|
|
3236
|
+
style: rootStyle,
|
|
3237
|
+
onKeyDown: handleKeyDown,
|
|
3238
|
+
"data-reels-modal-phase": phase,
|
|
3239
|
+
children: [
|
|
3240
|
+
/* @__PURE__ */ jsx(
|
|
3241
|
+
"div",
|
|
3242
|
+
{
|
|
3243
|
+
ref: backdropRef,
|
|
3244
|
+
style: backdropStyle,
|
|
3245
|
+
onClick: handleBackdropClick,
|
|
3246
|
+
"aria-hidden": "true",
|
|
3247
|
+
"data-reels-modal-backdrop": true,
|
|
3248
|
+
children: renderBackdrop ? renderBackdrop(renderState) : null
|
|
3249
|
+
}
|
|
3250
|
+
),
|
|
3251
|
+
/* @__PURE__ */ jsxs(
|
|
3252
|
+
"div",
|
|
3253
|
+
{
|
|
3254
|
+
ref: panelRef,
|
|
3255
|
+
role: "dialog",
|
|
3256
|
+
"aria-modal": "true",
|
|
3257
|
+
"aria-label": "Reels viewer",
|
|
3258
|
+
tabIndex: -1,
|
|
3259
|
+
className,
|
|
3260
|
+
style: panelStyle,
|
|
3261
|
+
"data-reels-modal-panel": true,
|
|
3262
|
+
children: [
|
|
3263
|
+
/* @__PURE__ */ jsx(ReelsFeed, { ...feedProps }),
|
|
3264
|
+
renderCloseButton ? renderCloseButton(renderState) : /* @__PURE__ */ jsx(DefaultCloseButton, { onClose: close })
|
|
3265
|
+
]
|
|
3266
|
+
}
|
|
3267
|
+
)
|
|
3268
|
+
]
|
|
3269
|
+
}
|
|
3270
|
+
),
|
|
3271
|
+
target
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
function DefaultCloseButton({ onClose }) {
|
|
3275
|
+
return /* @__PURE__ */ jsx(
|
|
3276
|
+
"button",
|
|
3277
|
+
{
|
|
3278
|
+
type: "button",
|
|
3279
|
+
onClick: onClose,
|
|
3280
|
+
"aria-label": "Close reels viewer",
|
|
3281
|
+
style: {
|
|
3282
|
+
position: "absolute",
|
|
3283
|
+
top: "max(12px, env(safe-area-inset-top))",
|
|
3284
|
+
right: 12,
|
|
3285
|
+
width: 40,
|
|
3286
|
+
height: 40,
|
|
3287
|
+
display: "flex",
|
|
3288
|
+
alignItems: "center",
|
|
3289
|
+
justifyContent: "center",
|
|
3290
|
+
borderRadius: "50%",
|
|
3291
|
+
border: "none",
|
|
3292
|
+
background: "rgba(0,0,0,0.45)",
|
|
3293
|
+
color: "#fff",
|
|
3294
|
+
fontSize: 22,
|
|
3295
|
+
lineHeight: 1,
|
|
3296
|
+
cursor: "pointer",
|
|
3297
|
+
zIndex: 2
|
|
3298
|
+
},
|
|
3299
|
+
children: "\u2715"
|
|
3300
|
+
}
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
2599
3303
|
function usePlayerSelector(selector) {
|
|
2600
3304
|
const { playerEngine } = useSDK();
|
|
2601
3305
|
const selectorRef = useRef(selector);
|
|
@@ -3210,4 +3914,4 @@ var HttpError = class extends Error {
|
|
|
3210
3914
|
}
|
|
3211
3915
|
};
|
|
3212
3916
|
|
|
3213
|
-
export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
|
|
3917
|
+
export { DEFAULT_FEED_CONFIG, DEFAULT_NAVIGATION_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, NavigationManager, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsModal, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidNavTransition, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, useNavigation, useNavigationSelector, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
|