@xhub-reels/sdk 0.2.12 → 0.2.14
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 +734 -52
- package/dist/index.d.cts +219 -4
- package/dist/index.d.ts +219 -4
- package/dist/index.js +730 -54
- 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 };
|
|
@@ -525,7 +534,10 @@ var FeedManager = class {
|
|
|
525
534
|
// PUBLIC API — Lifecycle
|
|
526
535
|
// ═══════════════════════════════════════════
|
|
527
536
|
destroy() {
|
|
537
|
+
if (this._destroyed) return;
|
|
538
|
+
this._destroyed = true;
|
|
528
539
|
this.abortController?.abort();
|
|
540
|
+
this.abortController = null;
|
|
529
541
|
this.inFlightRequests.clear();
|
|
530
542
|
this.accessOrder.clear();
|
|
531
543
|
this.prefetchCache = null;
|
|
@@ -594,7 +606,13 @@ var FeedManager = class {
|
|
|
594
606
|
applyItems(incoming, _nextCursor, append) {
|
|
595
607
|
const { itemsById, displayOrder } = this.store.getState();
|
|
596
608
|
const nextById = new Map(itemsById);
|
|
609
|
+
const seenInBatch = /* @__PURE__ */ new Set();
|
|
610
|
+
const deduped = [];
|
|
597
611
|
for (const item of incoming) {
|
|
612
|
+
if (!seenInBatch.has(item.id)) {
|
|
613
|
+
seenInBatch.add(item.id);
|
|
614
|
+
deduped.push(item);
|
|
615
|
+
}
|
|
598
616
|
nextById.set(item.id, item);
|
|
599
617
|
this.accessOrder.set(item.id, Date.now());
|
|
600
618
|
}
|
|
@@ -602,14 +620,14 @@ var FeedManager = class {
|
|
|
602
620
|
if (append) {
|
|
603
621
|
const existingIds = new Set(displayOrder);
|
|
604
622
|
const newIds = [];
|
|
605
|
-
for (const item of
|
|
623
|
+
for (const item of deduped) {
|
|
606
624
|
if (!existingIds.has(item.id)) {
|
|
607
625
|
newIds.push(item.id);
|
|
608
626
|
}
|
|
609
627
|
}
|
|
610
628
|
nextOrder = [...displayOrder, ...newIds];
|
|
611
629
|
} else {
|
|
612
|
-
nextOrder =
|
|
630
|
+
nextOrder = deduped.map((item) => item.id);
|
|
613
631
|
}
|
|
614
632
|
if (nextById.size > this.config.maxCacheSize) {
|
|
615
633
|
this.evictLRU(nextById, nextOrder);
|
|
@@ -651,6 +669,8 @@ var OptimisticManager = class {
|
|
|
651
669
|
this.likeDebounceTimers = /* @__PURE__ */ new Map();
|
|
652
670
|
/** Pending like direction: contentId → final intended state */
|
|
653
671
|
this.pendingLikeState = /* @__PURE__ */ new Map();
|
|
672
|
+
/** Idempotency guard for destroy() */
|
|
673
|
+
this._destroyed = false;
|
|
654
674
|
this.interaction = interaction;
|
|
655
675
|
this.logger = logger;
|
|
656
676
|
this.store = createStore(createInitialState3);
|
|
@@ -741,6 +761,8 @@ var OptimisticManager = class {
|
|
|
741
761
|
// PUBLIC API — Lifecycle
|
|
742
762
|
// ═══════════════════════════════════════════
|
|
743
763
|
destroy() {
|
|
764
|
+
if (this._destroyed) return;
|
|
765
|
+
this._destroyed = true;
|
|
744
766
|
for (const timer of this.likeDebounceTimers.values()) {
|
|
745
767
|
clearTimeout(timer);
|
|
746
768
|
}
|
|
@@ -762,6 +784,7 @@ var DEFAULT_RESOURCE_CONFIG = {
|
|
|
762
784
|
var ResourceGovernor = class {
|
|
763
785
|
constructor(config = {}, videoLoader, network, logger) {
|
|
764
786
|
this.focusDebounceTimer = null;
|
|
787
|
+
this._destroyed = false;
|
|
765
788
|
this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
|
|
766
789
|
this.videoLoader = videoLoader;
|
|
767
790
|
this.network = network;
|
|
@@ -792,11 +815,19 @@ var ResourceGovernor = class {
|
|
|
792
815
|
this.logger?.debug("[ResourceGovernor] Activated");
|
|
793
816
|
}
|
|
794
817
|
deactivate() {
|
|
818
|
+
if (this._destroyed) return;
|
|
819
|
+
if (!this.store.getState().isActive) return;
|
|
795
820
|
this.networkUnsubscribe?.();
|
|
796
|
-
|
|
821
|
+
this.networkUnsubscribe = void 0;
|
|
822
|
+
if (this.focusDebounceTimer) {
|
|
823
|
+
clearTimeout(this.focusDebounceTimer);
|
|
824
|
+
this.focusDebounceTimer = null;
|
|
825
|
+
}
|
|
797
826
|
this.store.setState({ isActive: false });
|
|
798
827
|
}
|
|
799
828
|
destroy() {
|
|
829
|
+
if (this._destroyed) return;
|
|
830
|
+
this._destroyed = true;
|
|
800
831
|
this.deactivate();
|
|
801
832
|
this.videoLoader?.clearAll();
|
|
802
833
|
}
|
|
@@ -920,6 +951,153 @@ var ResourceGovernor = class {
|
|
|
920
951
|
);
|
|
921
952
|
}
|
|
922
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
|
+
};
|
|
923
1101
|
function usePointerGesture(config = {}) {
|
|
924
1102
|
const {
|
|
925
1103
|
axis = "y",
|
|
@@ -1109,7 +1287,9 @@ function useSnapAnimation(config = {}) {
|
|
|
1109
1287
|
}
|
|
1110
1288
|
);
|
|
1111
1289
|
anim.addEventListener("finish", () => {
|
|
1112
|
-
element.
|
|
1290
|
+
if (element.isConnected) {
|
|
1291
|
+
element.style.transform = `translateY(${toY}px)`;
|
|
1292
|
+
}
|
|
1113
1293
|
anim.cancel();
|
|
1114
1294
|
});
|
|
1115
1295
|
animations.push(anim);
|
|
@@ -1137,39 +1317,100 @@ function useSnapAnimation(config = {}) {
|
|
|
1137
1317
|
}, [cancelAnimation]);
|
|
1138
1318
|
return { animateSnap, animateBounceBack, cancelAnimation };
|
|
1139
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
|
+
};
|
|
1140
1371
|
var SDKContext = createContext(null);
|
|
1141
1372
|
function ReelsProvider({
|
|
1142
1373
|
children,
|
|
1143
1374
|
adapters,
|
|
1144
1375
|
initialItems,
|
|
1145
|
-
debug = false
|
|
1376
|
+
debug = false,
|
|
1377
|
+
navigationConfig
|
|
1146
1378
|
}) {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
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);
|
|
1149
1392
|
const value = useMemo(() => {
|
|
1150
|
-
if (sdkRef.current) {
|
|
1151
|
-
sdkRef.current.feedManager.destroy();
|
|
1152
|
-
sdkRef.current.playerEngine.destroy();
|
|
1153
|
-
sdkRef.current.resourceGovernor.destroy();
|
|
1154
|
-
sdkRef.current.optimisticManager.destroy();
|
|
1155
|
-
}
|
|
1156
1393
|
const feedManager = new FeedManager(adapters.dataSource, {}, logger);
|
|
1157
1394
|
if (initialItems && initialItems.length > 0) {
|
|
1158
1395
|
feedManager.setInitialItems(initialItems);
|
|
1159
1396
|
}
|
|
1160
1397
|
const playerEngine = new PlayerEngine(
|
|
1161
1398
|
{},
|
|
1162
|
-
|
|
1399
|
+
resolvedAdapters.analytics,
|
|
1163
1400
|
logger
|
|
1164
1401
|
);
|
|
1165
1402
|
const resourceGovernor = new ResourceGovernor(
|
|
1166
1403
|
{},
|
|
1167
|
-
|
|
1168
|
-
|
|
1404
|
+
resolvedAdapters.videoLoader,
|
|
1405
|
+
resolvedAdapters.network,
|
|
1169
1406
|
logger
|
|
1170
1407
|
);
|
|
1171
1408
|
const optimisticManager = new OptimisticManager(
|
|
1172
|
-
|
|
1409
|
+
resolvedAdapters.interaction,
|
|
1410
|
+
logger
|
|
1411
|
+
);
|
|
1412
|
+
const navigationManager = new NavigationManager(
|
|
1413
|
+
navigationConfig ?? {},
|
|
1173
1414
|
logger
|
|
1174
1415
|
);
|
|
1175
1416
|
const instance = {
|
|
@@ -1177,15 +1418,27 @@ function ReelsProvider({
|
|
|
1177
1418
|
playerEngine,
|
|
1178
1419
|
resourceGovernor,
|
|
1179
1420
|
optimisticManager,
|
|
1180
|
-
|
|
1421
|
+
navigationManager,
|
|
1422
|
+
adapters: resolvedAdapters
|
|
1181
1423
|
};
|
|
1182
|
-
sdkRef.current = instance;
|
|
1183
1424
|
return instance;
|
|
1184
1425
|
}, [adapters.dataSource]);
|
|
1185
1426
|
useEffect(() => {
|
|
1186
|
-
|
|
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();
|
|
1187
1440
|
return () => {
|
|
1188
|
-
|
|
1441
|
+
governor.deactivate();
|
|
1189
1442
|
};
|
|
1190
1443
|
}, [value.resourceGovernor]);
|
|
1191
1444
|
useEffect(() => {
|
|
@@ -1195,10 +1448,11 @@ function ReelsProvider({
|
|
|
1195
1448
|
}, [debug, logger]);
|
|
1196
1449
|
useEffect(() => {
|
|
1197
1450
|
return () => {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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();
|
|
1202
1456
|
};
|
|
1203
1457
|
}, []);
|
|
1204
1458
|
return /* @__PURE__ */ jsx(SDKContext.Provider, { value, children });
|
|
@@ -1702,25 +1956,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
|
1702
1956
|
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1703
1957
|
var _playAheadActive = 0;
|
|
1704
1958
|
var _playAheadQueue = [];
|
|
1959
|
+
var _tokenCounter = 0;
|
|
1960
|
+
var _releasedTokens = /* @__PURE__ */ new Set();
|
|
1705
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
|
+
};
|
|
1706
1976
|
if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
|
|
1707
1977
|
_playAheadActive++;
|
|
1708
|
-
return Promise.resolve();
|
|
1978
|
+
return Promise.resolve(makeRelease());
|
|
1709
1979
|
}
|
|
1710
1980
|
return new Promise((resolve) => {
|
|
1711
1981
|
_playAheadQueue.push(() => {
|
|
1712
|
-
|
|
1982
|
+
_playAheadActive++;
|
|
1983
|
+
setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
|
|
1713
1984
|
});
|
|
1714
1985
|
});
|
|
1715
1986
|
}
|
|
1716
|
-
function releasePlayAhead() {
|
|
1717
|
-
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
1718
|
-
const next = _playAheadQueue.shift();
|
|
1719
|
-
if (next) {
|
|
1720
|
-
_playAheadActive++;
|
|
1721
|
-
next();
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
1987
|
function VideoSlot({
|
|
1725
1988
|
item,
|
|
1726
1989
|
index,
|
|
@@ -1843,6 +2106,42 @@ function VideoSlotInner({
|
|
|
1843
2106
|
}
|
|
1844
2107
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
1845
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]);
|
|
1846
2145
|
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
|
1847
2146
|
useEffect(() => {
|
|
1848
2147
|
const video = videoRef.current;
|
|
@@ -1866,32 +2165,38 @@ function VideoSlotInner({
|
|
|
1866
2165
|
const prevMuted = video.muted;
|
|
1867
2166
|
video.muted = true;
|
|
1868
2167
|
let cancelled = false;
|
|
2168
|
+
let release = null;
|
|
1869
2169
|
const doPlayAhead = async () => {
|
|
1870
|
-
await acquirePlayAhead();
|
|
2170
|
+
release = await acquirePlayAhead();
|
|
2171
|
+
if (cancelled) {
|
|
2172
|
+
release();
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
1871
2175
|
try {
|
|
1872
2176
|
await video.play();
|
|
1873
2177
|
if (cancelled) {
|
|
1874
2178
|
video.pause();
|
|
1875
|
-
|
|
2179
|
+
release();
|
|
1876
2180
|
return;
|
|
1877
2181
|
}
|
|
1878
2182
|
const pauseAfterDecode = () => {
|
|
1879
2183
|
video.pause();
|
|
1880
2184
|
video.currentTime = 0;
|
|
1881
2185
|
video.muted = prevMuted;
|
|
1882
|
-
|
|
2186
|
+
release?.();
|
|
1883
2187
|
if (!cancelled) {
|
|
1884
2188
|
setHasPlayedAhead(true);
|
|
1885
2189
|
}
|
|
1886
2190
|
};
|
|
1887
2191
|
setTimeout(pauseAfterDecode, 50);
|
|
1888
2192
|
} catch {
|
|
1889
|
-
|
|
2193
|
+
release?.();
|
|
1890
2194
|
}
|
|
1891
2195
|
};
|
|
1892
2196
|
doPlayAhead();
|
|
1893
2197
|
return () => {
|
|
1894
2198
|
cancelled = true;
|
|
2199
|
+
release?.();
|
|
1895
2200
|
};
|
|
1896
2201
|
}, [isActive, isReady, hasPlayedAhead]);
|
|
1897
2202
|
useEffect(() => {
|
|
@@ -2097,13 +2402,13 @@ function VideoSlotInner({
|
|
|
2097
2402
|
}
|
|
2098
2403
|
}
|
|
2099
2404
|
),
|
|
2100
|
-
item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2405
|
+
(capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2101
2406
|
"div",
|
|
2102
2407
|
{
|
|
2103
2408
|
style: {
|
|
2104
2409
|
position: "absolute",
|
|
2105
2410
|
inset: 0,
|
|
2106
|
-
backgroundImage: `url(${item.poster})`,
|
|
2411
|
+
backgroundImage: `url(${capturedPoster || item.poster})`,
|
|
2107
2412
|
backgroundSize: "cover",
|
|
2108
2413
|
backgroundPosition: "center",
|
|
2109
2414
|
opacity: showPosterOverlay ? 1 : 0,
|
|
@@ -2315,7 +2620,7 @@ function ReelsFeed({
|
|
|
2315
2620
|
}, 16);
|
|
2316
2621
|
};
|
|
2317
2622
|
const observer = new MutationObserver(debouncedRebuild);
|
|
2318
|
-
observer.observe(container, { childList: true, subtree:
|
|
2623
|
+
observer.observe(container, { childList: true, subtree: false });
|
|
2319
2624
|
return () => {
|
|
2320
2625
|
observer.disconnect();
|
|
2321
2626
|
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
@@ -2552,6 +2857,56 @@ function parsePxTranslateY(el) {
|
|
|
2552
2857
|
if (!match || !match[1]) return 0;
|
|
2553
2858
|
return Number.parseFloat(match[1]);
|
|
2554
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
|
+
}
|
|
2555
2910
|
function ReelsFeedThumbnail({
|
|
2556
2911
|
renderThumbnail,
|
|
2557
2912
|
onThumbnailClick,
|
|
@@ -2561,33 +2916,51 @@ function ReelsFeedThumbnail({
|
|
|
2561
2916
|
className = "grid grid-cols-2 gap-3",
|
|
2562
2917
|
wrap = true,
|
|
2563
2918
|
setFocusOnClick = true,
|
|
2919
|
+
openOnClick = false,
|
|
2564
2920
|
prefetchOnHover = false,
|
|
2921
|
+
prewarmOnClick = true,
|
|
2565
2922
|
getKey
|
|
2566
2923
|
}) {
|
|
2567
|
-
const { items, loading, error, refresh } = useFeed();
|
|
2924
|
+
const { items, loading, error, refresh, loadInitial } = useFeed();
|
|
2568
2925
|
const { setFocusedIndexImmediate } = useResource();
|
|
2926
|
+
const { open } = useNavigation();
|
|
2569
2927
|
const { adapters } = useSDK();
|
|
2928
|
+
useEffect(() => {
|
|
2929
|
+
loadInitial();
|
|
2930
|
+
}, [loadInitial]);
|
|
2570
2931
|
const prefetchedRef = useRef(/* @__PURE__ */ new Set());
|
|
2932
|
+
const prewarm = useCallback(
|
|
2933
|
+
(item) => {
|
|
2934
|
+
if (!isVideoItem(item)) return;
|
|
2935
|
+
if (item.source.type !== "hls") return;
|
|
2936
|
+
const url = item.source.url;
|
|
2937
|
+
if (prefetchedRef.current.has(url)) return;
|
|
2938
|
+
prefetchedRef.current.add(url);
|
|
2939
|
+
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2940
|
+
},
|
|
2941
|
+
[adapters.videoLoader]
|
|
2942
|
+
);
|
|
2571
2943
|
const handleClick = useCallback(
|
|
2572
2944
|
(id, item, index) => {
|
|
2945
|
+
if (prewarmOnClick) {
|
|
2946
|
+
prewarm(item);
|
|
2947
|
+
}
|
|
2573
2948
|
if (setFocusOnClick) {
|
|
2574
2949
|
setFocusedIndexImmediate(index);
|
|
2575
2950
|
}
|
|
2951
|
+
if (openOnClick) {
|
|
2952
|
+
open(index);
|
|
2953
|
+
}
|
|
2576
2954
|
onThumbnailClick?.(id, item, index);
|
|
2577
2955
|
},
|
|
2578
|
-
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2956
|
+
[prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
|
|
2579
2957
|
);
|
|
2580
2958
|
const handlePointerEnter = useCallback(
|
|
2581
2959
|
(item) => {
|
|
2582
2960
|
if (!prefetchOnHover) return;
|
|
2583
|
-
|
|
2584
|
-
if (item.source.type !== "hls") return;
|
|
2585
|
-
const url = item.source.url;
|
|
2586
|
-
if (prefetchedRef.current.has(url)) return;
|
|
2587
|
-
prefetchedRef.current.add(url);
|
|
2588
|
-
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2961
|
+
prewarm(item);
|
|
2589
2962
|
},
|
|
2590
|
-
[prefetchOnHover,
|
|
2963
|
+
[prefetchOnHover, prewarm]
|
|
2591
2964
|
);
|
|
2592
2965
|
if (loading && items.length === 0) {
|
|
2593
2966
|
if (renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
|
|
@@ -2627,6 +3000,309 @@ function ReelsFeedThumbnail({
|
|
|
2627
3000
|
}
|
|
2628
3001
|
return /* @__PURE__ */ jsx("div", { className, children: content });
|
|
2629
3002
|
}
|
|
3003
|
+
var DEFAULT_DURATION = 300;
|
|
3004
|
+
var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
3005
|
+
var DEFAULT_DIRECTION = "up";
|
|
3006
|
+
var DEFAULT_Z_INDEX = 1e3;
|
|
3007
|
+
var DEFAULT_PREWARM_FORWARD = 2;
|
|
3008
|
+
function offscreenTransform(direction) {
|
|
3009
|
+
switch (direction) {
|
|
3010
|
+
case "down":
|
|
3011
|
+
return "translateY(-100%)";
|
|
3012
|
+
case "fade":
|
|
3013
|
+
return "translateY(0)";
|
|
3014
|
+
case "up":
|
|
3015
|
+
default:
|
|
3016
|
+
return "translateY(100%)";
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
function prefersReducedMotion() {
|
|
3020
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
3021
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3022
|
+
}
|
|
3023
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
|
|
3024
|
+
function ReelsModal({
|
|
3025
|
+
feedProps,
|
|
3026
|
+
animationConfig,
|
|
3027
|
+
renderBackdrop,
|
|
3028
|
+
renderCloseButton,
|
|
3029
|
+
onOpen,
|
|
3030
|
+
onClose,
|
|
3031
|
+
closeOnBackdropClick = true,
|
|
3032
|
+
closeOnEscape = true,
|
|
3033
|
+
lockBodyScroll = true,
|
|
3034
|
+
prewarmForward = DEFAULT_PREWARM_FORWARD,
|
|
3035
|
+
className,
|
|
3036
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
3037
|
+
portalTarget
|
|
3038
|
+
}) {
|
|
3039
|
+
const { phase, openIndex, shouldMount, close, setPhase } = useNavigation();
|
|
3040
|
+
const { setFocusedIndexImmediate } = useResource();
|
|
3041
|
+
const { items } = useFeed();
|
|
3042
|
+
const { adapters } = useSDK();
|
|
3043
|
+
const duration = animationConfig?.duration ?? DEFAULT_DURATION;
|
|
3044
|
+
const easing = animationConfig?.easing ?? DEFAULT_EASING;
|
|
3045
|
+
const direction = animationConfig?.direction ?? DEFAULT_DIRECTION;
|
|
3046
|
+
const backdropRef = useRef(null);
|
|
3047
|
+
const panelRef = useRef(null);
|
|
3048
|
+
const animationsRef = useRef([]);
|
|
3049
|
+
const previouslyFocusedRef = useRef(null);
|
|
3050
|
+
const renderState = useMemo(
|
|
3051
|
+
() => ({ phase, openIndex, close }),
|
|
3052
|
+
[phase, openIndex, close]
|
|
3053
|
+
);
|
|
3054
|
+
const prewarmedRef = useRef(null);
|
|
3055
|
+
useEffect(() => {
|
|
3056
|
+
if (phase !== "opening") {
|
|
3057
|
+
if (phase === "closed") prewarmedRef.current = null;
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
if (openIndex == null) return;
|
|
3061
|
+
if (prewarmedRef.current === openIndex) return;
|
|
3062
|
+
prewarmedRef.current = openIndex;
|
|
3063
|
+
setFocusedIndexImmediate(openIndex);
|
|
3064
|
+
const preload = adapters.videoLoader?.preloadMetadata;
|
|
3065
|
+
if (!preload) return;
|
|
3066
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3067
|
+
targets.add(openIndex);
|
|
3068
|
+
for (let d = 1; d <= prewarmForward; d++) targets.add(openIndex + d);
|
|
3069
|
+
targets.add(openIndex - 1);
|
|
3070
|
+
for (const idx of targets) {
|
|
3071
|
+
if (idx < 0 || idx >= items.length) continue;
|
|
3072
|
+
const item = items[idx];
|
|
3073
|
+
if (item && isVideoItem(item) && item.source.type === "hls") {
|
|
3074
|
+
preload(item.source.url);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}, [phase, openIndex, items, adapters.videoLoader, prewarmForward, setFocusedIndexImmediate]);
|
|
3078
|
+
useEffect(() => {
|
|
3079
|
+
if (!lockBodyScroll || !shouldMount) return;
|
|
3080
|
+
if (typeof document === "undefined") return;
|
|
3081
|
+
const prev = document.body.style.overflow;
|
|
3082
|
+
document.body.style.overflow = "hidden";
|
|
3083
|
+
return () => {
|
|
3084
|
+
document.body.style.overflow = prev;
|
|
3085
|
+
};
|
|
3086
|
+
}, [lockBodyScroll, shouldMount]);
|
|
3087
|
+
const cancelAnimations = useCallback(() => {
|
|
3088
|
+
for (const a of animationsRef.current) a.cancel();
|
|
3089
|
+
animationsRef.current = [];
|
|
3090
|
+
}, []);
|
|
3091
|
+
useLayoutEffect(() => {
|
|
3092
|
+
const panel = panelRef.current;
|
|
3093
|
+
const backdrop = backdropRef.current;
|
|
3094
|
+
if (!panel) return;
|
|
3095
|
+
if (phase === "open" || phase === "closed") return;
|
|
3096
|
+
const reduce = prefersReducedMotion();
|
|
3097
|
+
const enter = phase === "opening";
|
|
3098
|
+
const offscreen2 = offscreenTransform(direction);
|
|
3099
|
+
const panelFrames = direction === "fade" ? enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }] : enter ? [{ transform: offscreen2 }, { transform: "translateY(0)" }] : [{ transform: "translateY(0)" }, { transform: offscreen2 }];
|
|
3100
|
+
const backdropFrames = enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
|
|
3101
|
+
const animDuration = reduce ? 0 : duration;
|
|
3102
|
+
let cancelled = false;
|
|
3103
|
+
cancelAnimations();
|
|
3104
|
+
const anims = [];
|
|
3105
|
+
const panelAnim = panel.animate(panelFrames, {
|
|
3106
|
+
duration: animDuration,
|
|
3107
|
+
easing,
|
|
3108
|
+
fill: "forwards"
|
|
3109
|
+
});
|
|
3110
|
+
anims.push(panelAnim);
|
|
3111
|
+
if (backdrop) {
|
|
3112
|
+
anims.push(
|
|
3113
|
+
backdrop.animate(backdropFrames, {
|
|
3114
|
+
duration: animDuration,
|
|
3115
|
+
easing,
|
|
3116
|
+
fill: "forwards"
|
|
3117
|
+
})
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
animationsRef.current = anims;
|
|
3121
|
+
const finalize = () => {
|
|
3122
|
+
if (cancelled) return;
|
|
3123
|
+
if (enter) {
|
|
3124
|
+
if (direction === "fade") panel.style.opacity = "1";
|
|
3125
|
+
else panel.style.transform = "translateY(0)";
|
|
3126
|
+
if (backdrop) backdrop.style.opacity = "1";
|
|
3127
|
+
} else {
|
|
3128
|
+
if (direction === "fade") panel.style.opacity = "0";
|
|
3129
|
+
else panel.style.transform = offscreen2;
|
|
3130
|
+
if (backdrop) backdrop.style.opacity = "0";
|
|
3131
|
+
}
|
|
3132
|
+
for (const a of anims) a.cancel();
|
|
3133
|
+
animationsRef.current = [];
|
|
3134
|
+
setPhase(enter ? "open" : "closed");
|
|
3135
|
+
};
|
|
3136
|
+
panelAnim.addEventListener("finish", finalize);
|
|
3137
|
+
return () => {
|
|
3138
|
+
cancelled = true;
|
|
3139
|
+
panelAnim.removeEventListener("finish", finalize);
|
|
3140
|
+
cancelAnimations();
|
|
3141
|
+
};
|
|
3142
|
+
}, [phase, direction, duration, easing, setPhase, cancelAnimations]);
|
|
3143
|
+
const prevPhaseRef = useRef("closed");
|
|
3144
|
+
useEffect(() => {
|
|
3145
|
+
const prev = prevPhaseRef.current;
|
|
3146
|
+
if (prev !== "open" && phase === "open") onOpen?.(openIndex);
|
|
3147
|
+
if (prev !== "closed" && phase === "closed") onClose?.();
|
|
3148
|
+
prevPhaseRef.current = phase;
|
|
3149
|
+
}, [phase, openIndex, onOpen, onClose]);
|
|
3150
|
+
useEffect(() => {
|
|
3151
|
+
if (phase === "opening" && typeof document !== "undefined") {
|
|
3152
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
3153
|
+
const id = requestAnimationFrame(() => {
|
|
3154
|
+
const panel = panelRef.current;
|
|
3155
|
+
if (!panel) return;
|
|
3156
|
+
const first = panel.querySelector(FOCUSABLE);
|
|
3157
|
+
(first ?? panel).focus();
|
|
3158
|
+
});
|
|
3159
|
+
return () => cancelAnimationFrame(id);
|
|
3160
|
+
}
|
|
3161
|
+
if (phase === "closed") {
|
|
3162
|
+
previouslyFocusedRef.current?.focus?.();
|
|
3163
|
+
previouslyFocusedRef.current = null;
|
|
3164
|
+
}
|
|
3165
|
+
return void 0;
|
|
3166
|
+
}, [phase]);
|
|
3167
|
+
const handleKeyDown = useCallback(
|
|
3168
|
+
(e) => {
|
|
3169
|
+
if (closeOnEscape && e.key === "Escape") {
|
|
3170
|
+
e.stopPropagation();
|
|
3171
|
+
close();
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (e.key !== "Tab") return;
|
|
3175
|
+
const panel = panelRef.current;
|
|
3176
|
+
if (!panel) return;
|
|
3177
|
+
const focusables = Array.from(
|
|
3178
|
+
panel.querySelectorAll(FOCUSABLE)
|
|
3179
|
+
).filter((el) => el.offsetParent !== null);
|
|
3180
|
+
if (focusables.length === 0) {
|
|
3181
|
+
e.preventDefault();
|
|
3182
|
+
panel.focus();
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const first = focusables[0];
|
|
3186
|
+
const last = focusables[focusables.length - 1];
|
|
3187
|
+
const active = document.activeElement;
|
|
3188
|
+
if (e.shiftKey && active === first) {
|
|
3189
|
+
e.preventDefault();
|
|
3190
|
+
last.focus();
|
|
3191
|
+
} else if (!e.shiftKey && active === last) {
|
|
3192
|
+
e.preventDefault();
|
|
3193
|
+
first.focus();
|
|
3194
|
+
}
|
|
3195
|
+
},
|
|
3196
|
+
[closeOnEscape, close]
|
|
3197
|
+
);
|
|
3198
|
+
const handleBackdropClick = useCallback(() => {
|
|
3199
|
+
if (closeOnBackdropClick) close();
|
|
3200
|
+
}, [closeOnBackdropClick, close]);
|
|
3201
|
+
if (!shouldMount) return null;
|
|
3202
|
+
if (typeof document === "undefined") return null;
|
|
3203
|
+
const target = portalTarget ?? document.body;
|
|
3204
|
+
if (!target) return null;
|
|
3205
|
+
const enterStart = phase === "opening";
|
|
3206
|
+
const offscreen = offscreenTransform(direction);
|
|
3207
|
+
const panelInitialTransform = direction === "fade" ? "translateY(0)" : enterStart ? offscreen : "translateY(0)";
|
|
3208
|
+
const panelInitialOpacity = direction === "fade" ? enterStart ? 0 : 1 : 1;
|
|
3209
|
+
const backdropInitialOpacity = enterStart ? 0 : 1;
|
|
3210
|
+
const rootStyle = {
|
|
3211
|
+
position: "fixed",
|
|
3212
|
+
inset: 0,
|
|
3213
|
+
zIndex,
|
|
3214
|
+
// While opening, the panel is parked off-screen but MUST stay painted so
|
|
3215
|
+
// the <video> decodes its first frame. overflow:hidden keeps the
|
|
3216
|
+
// off-screen panel from creating scrollbars / capturing stray taps.
|
|
3217
|
+
overflow: "hidden"
|
|
3218
|
+
};
|
|
3219
|
+
const backdropStyle = {
|
|
3220
|
+
position: "absolute",
|
|
3221
|
+
inset: 0,
|
|
3222
|
+
opacity: backdropInitialOpacity,
|
|
3223
|
+
background: renderBackdrop ? "transparent" : "rgba(0,0,0,0.85)",
|
|
3224
|
+
// willChange hints the compositor for the opacity fade.
|
|
3225
|
+
willChange: "opacity"
|
|
3226
|
+
};
|
|
3227
|
+
const panelStyle = {
|
|
3228
|
+
position: "absolute",
|
|
3229
|
+
inset: 0,
|
|
3230
|
+
transform: panelInitialTransform,
|
|
3231
|
+
opacity: panelInitialOpacity,
|
|
3232
|
+
willChange: direction === "fade" ? "opacity" : "transform",
|
|
3233
|
+
outline: "none"
|
|
3234
|
+
};
|
|
3235
|
+
return createPortal(
|
|
3236
|
+
/* @__PURE__ */ jsxs(
|
|
3237
|
+
"div",
|
|
3238
|
+
{
|
|
3239
|
+
style: rootStyle,
|
|
3240
|
+
onKeyDown: handleKeyDown,
|
|
3241
|
+
"data-reels-modal-phase": phase,
|
|
3242
|
+
children: [
|
|
3243
|
+
/* @__PURE__ */ jsx(
|
|
3244
|
+
"div",
|
|
3245
|
+
{
|
|
3246
|
+
ref: backdropRef,
|
|
3247
|
+
style: backdropStyle,
|
|
3248
|
+
onClick: handleBackdropClick,
|
|
3249
|
+
"aria-hidden": "true",
|
|
3250
|
+
"data-reels-modal-backdrop": true,
|
|
3251
|
+
children: renderBackdrop ? renderBackdrop(renderState) : null
|
|
3252
|
+
}
|
|
3253
|
+
),
|
|
3254
|
+
/* @__PURE__ */ jsxs(
|
|
3255
|
+
"div",
|
|
3256
|
+
{
|
|
3257
|
+
ref: panelRef,
|
|
3258
|
+
role: "dialog",
|
|
3259
|
+
"aria-modal": "true",
|
|
3260
|
+
"aria-label": "Reels viewer",
|
|
3261
|
+
tabIndex: -1,
|
|
3262
|
+
className,
|
|
3263
|
+
style: panelStyle,
|
|
3264
|
+
"data-reels-modal-panel": true,
|
|
3265
|
+
children: [
|
|
3266
|
+
/* @__PURE__ */ jsx(ReelsFeed, { ...feedProps }),
|
|
3267
|
+
renderCloseButton ? renderCloseButton(renderState) : /* @__PURE__ */ jsx(DefaultCloseButton, { onClose: close })
|
|
3268
|
+
]
|
|
3269
|
+
}
|
|
3270
|
+
)
|
|
3271
|
+
]
|
|
3272
|
+
}
|
|
3273
|
+
),
|
|
3274
|
+
target
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
function DefaultCloseButton({ onClose }) {
|
|
3278
|
+
return /* @__PURE__ */ jsx(
|
|
3279
|
+
"button",
|
|
3280
|
+
{
|
|
3281
|
+
type: "button",
|
|
3282
|
+
onClick: onClose,
|
|
3283
|
+
"aria-label": "Close reels viewer",
|
|
3284
|
+
style: {
|
|
3285
|
+
position: "absolute",
|
|
3286
|
+
top: "max(12px, env(safe-area-inset-top))",
|
|
3287
|
+
right: 12,
|
|
3288
|
+
width: 40,
|
|
3289
|
+
height: 40,
|
|
3290
|
+
display: "flex",
|
|
3291
|
+
alignItems: "center",
|
|
3292
|
+
justifyContent: "center",
|
|
3293
|
+
borderRadius: "50%",
|
|
3294
|
+
border: "none",
|
|
3295
|
+
background: "rgba(0,0,0,0.45)",
|
|
3296
|
+
color: "#fff",
|
|
3297
|
+
fontSize: 22,
|
|
3298
|
+
lineHeight: 1,
|
|
3299
|
+
cursor: "pointer",
|
|
3300
|
+
zIndex: 2
|
|
3301
|
+
},
|
|
3302
|
+
children: "\u2715"
|
|
3303
|
+
}
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
2630
3306
|
function usePlayerSelector(selector) {
|
|
2631
3307
|
const { playerEngine } = useSDK();
|
|
2632
3308
|
const selectorRef = useRef(selector);
|
|
@@ -3241,4 +3917,4 @@ var HttpError = class extends Error {
|
|
|
3241
3917
|
}
|
|
3242
3918
|
};
|
|
3243
3919
|
|
|
3244
|
-
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 };
|
|
3920
|
+
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 };
|