@tiledev/sdk-apptile-live-selling 0.8.0 → 0.9.0
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 +443 -34
- package/dist/auctionComments.d.ts +25 -0
- package/dist/auctionComments.d.ts.map +1 -0
- package/dist/auctionComments.js +28 -0
- package/dist/auctionComments.js.map +1 -0
- package/dist/auctionRules.d.ts +76 -0
- package/dist/auctionRules.d.ts.map +1 -0
- package/dist/auctionRules.js +106 -0
- package/dist/auctionRules.js.map +1 -0
- package/dist/auctions.d.ts.map +1 -1
- package/dist/auctions.js +19 -10
- package/dist/auctions.js.map +1 -1
- package/dist/client.d.ts +51 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +123 -23
- package/dist/client.js.map +1 -1
- package/dist/commentHistory.d.ts +66 -0
- package/dist/commentHistory.d.ts.map +1 -0
- package/dist/commentHistory.js +125 -0
- package/dist/commentHistory.js.map +1 -0
- package/dist/engine.native.d.ts.map +1 -1
- package/dist/engine.native.js +17 -845
- package/dist/engine.native.js.map +1 -1
- package/dist/giveawayPrize.d.ts +13 -0
- package/dist/giveawayPrize.d.ts.map +1 -0
- package/dist/giveawayPrize.js +18 -0
- package/dist/giveawayPrize.js.map +1 -0
- package/dist/giveawayPrizeLines.d.ts +67 -0
- package/dist/giveawayPrizeLines.d.ts.map +1 -0
- package/dist/giveawayPrizeLines.js +48 -0
- package/dist/giveawayPrizeLines.js.map +1 -0
- package/dist/giveawayRoster.d.ts +53 -0
- package/dist/giveawayRoster.d.ts.map +1 -0
- package/dist/giveawayRoster.js +93 -0
- package/dist/giveawayRoster.js.map +1 -0
- package/dist/heldComments.d.ts +29 -0
- package/dist/heldComments.d.ts.map +1 -0
- package/dist/heldComments.js +61 -0
- package/dist/heldComments.js.map +1 -0
- package/dist/hiddenShows.d.ts +34 -0
- package/dist/hiddenShows.d.ts.map +1 -0
- package/dist/hiddenShows.js +42 -0
- package/dist/hiddenShows.js.map +1 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -3
- package/dist/index.js.map +1 -1
- package/dist/liveShow.d.ts +58 -0
- package/dist/liveShow.d.ts.map +1 -0
- package/dist/liveShow.js +55 -0
- package/dist/liveShow.js.map +1 -0
- package/dist/pipWindow.d.ts +85 -0
- package/dist/pipWindow.d.ts.map +1 -0
- package/dist/pipWindow.js +88 -0
- package/dist/pipWindow.js.map +1 -0
- package/dist/polls.d.ts.map +1 -1
- package/dist/polls.js +3 -0
- package/dist/polls.js.map +1 -1
- package/dist/react/LiveSellingProvider.d.ts +10 -1
- package/dist/react/LiveSellingProvider.d.ts.map +1 -1
- package/dist/react/LiveSellingProvider.js +6 -2
- package/dist/react/LiveSellingProvider.js.map +1 -1
- package/dist/react/LiveVideoTarget.d.ts.map +1 -1
- package/dist/react/LiveVideoTarget.js +41 -3
- package/dist/react/LiveVideoTarget.js.map +1 -1
- package/dist/react/clocks.d.ts +29 -0
- package/dist/react/clocks.d.ts.map +1 -0
- package/dist/react/clocks.js +124 -0
- package/dist/react/clocks.js.map +1 -0
- package/dist/react/index.d.ts +7 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +15 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.web.d.ts +9 -2
- package/dist/react/index.web.d.ts.map +1 -1
- package/dist/react/index.web.js +27 -4
- package/dist/react/index.web.js.map +1 -1
- package/dist/react/startSignIn.d.ts +3 -0
- package/dist/react/startSignIn.d.ts.map +1 -0
- package/dist/react/startSignIn.js +19 -0
- package/dist/react/startSignIn.js.map +1 -0
- package/dist/react/useAuctionBidGate.d.ts +54 -4
- package/dist/react/useAuctionBidGate.d.ts.map +1 -1
- package/dist/react/useAuctionBidGate.js +169 -25
- package/dist/react/useAuctionBidGate.js.map +1 -1
- package/dist/react/useAuctionWins.d.ts +9 -1
- package/dist/react/useAuctionWins.d.ts.map +1 -1
- package/dist/react/useAuctionWins.js +9 -3
- package/dist/react/useAuctionWins.js.map +1 -1
- package/dist/react/useGiveawayEntry.d.ts +59 -0
- package/dist/react/useGiveawayEntry.d.ts.map +1 -0
- package/dist/react/useGiveawayEntry.js +170 -0
- package/dist/react/useGiveawayEntry.js.map +1 -0
- package/dist/react/useGiveawayPrize.d.ts +12 -0
- package/dist/react/useGiveawayPrize.d.ts.map +1 -0
- package/dist/react/useGiveawayPrize.js +53 -0
- package/dist/react/useGiveawayPrize.js.map +1 -0
- package/dist/react/useHiddenShows.d.ts +10 -0
- package/dist/react/useHiddenShows.d.ts.map +1 -0
- package/dist/react/useHiddenShows.js +30 -0
- package/dist/react/useHiddenShows.js.map +1 -0
- package/dist/react/useLiveSession.d.ts +7 -3
- package/dist/react/useLiveSession.d.ts.map +1 -1
- package/dist/react/useLiveSession.js +522 -157
- package/dist/react/useLiveSession.js.map +1 -1
- package/dist/react/useLiveShow.d.ts +3 -0
- package/dist/react/useLiveShow.d.ts.map +1 -0
- package/dist/react/useLiveShow.js +21 -0
- package/dist/react/useLiveShow.js.map +1 -0
- package/dist/react/useLiveStreams.d.ts.map +1 -1
- package/dist/react/useLiveStreams.js +6 -2
- package/dist/react/useLiveStreams.js.map +1 -1
- package/dist/react/useOngoingLive.d.ts +1 -0
- package/dist/react/useOngoingLive.d.ts.map +1 -1
- package/dist/react/useOngoingLive.js +7 -1
- package/dist/react/useOngoingLive.js.map +1 -1
- package/dist/react/useReplays.d.ts +12 -0
- package/dist/react/useReplays.d.ts.map +1 -1
- package/dist/react/useReplays.js +56 -24
- package/dist/react/useReplays.js.map +1 -1
- package/dist/react/useStreamInfo.d.ts +13 -2
- package/dist/react/useStreamInfo.d.ts.map +1 -1
- package/dist/react/useStreamInfo.js +31 -5
- package/dist/react/useStreamInfo.js.map +1 -1
- package/dist/replayPages.d.ts +31 -0
- package/dist/replayPages.d.ts.map +1 -0
- package/dist/replayPages.js +34 -0
- package/dist/replayPages.js.map +1 -0
- package/dist/replays.d.ts +7 -0
- package/dist/replays.d.ts.map +1 -1
- package/dist/replays.js +52 -22
- package/dist/replays.js.map +1 -1
- package/dist/session.d.ts +39 -6
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +209 -22
- package/dist/session.js.map +1 -1
- package/dist/sessionTypes.d.ts +190 -14
- package/dist/sessionTypes.d.ts.map +1 -1
- package/dist/sessionTypes.js +2 -1
- package/dist/sessionTypes.js.map +1 -1
- package/dist/shopifyCommerce.d.ts +149 -0
- package/dist/shopifyCommerce.d.ts.map +1 -0
- package/dist/shopifyCommerce.js +123 -0
- package/dist/shopifyCommerce.js.map +1 -0
- package/dist/shopifyIdentity.d.ts +51 -0
- package/dist/shopifyIdentity.d.ts.map +1 -0
- package/dist/shopifyIdentity.js +52 -0
- package/dist/shopifyIdentity.js.map +1 -0
- package/dist/shopifyIds.d.ts +9 -0
- package/dist/shopifyIds.d.ts.map +1 -0
- package/dist/shopifyIds.js +18 -0
- package/dist/shopifyIds.js.map +1 -0
- package/dist/showLinks.d.ts +24 -0
- package/dist/showLinks.d.ts.map +1 -0
- package/dist/showLinks.js +23 -0
- package/dist/showLinks.js.map +1 -0
- package/dist/showLots.d.ts +28 -0
- package/dist/showLots.d.ts.map +1 -0
- package/dist/showLots.js +51 -0
- package/dist/showLots.js.map +1 -0
- package/dist/showNames.d.ts +45 -0
- package/dist/showNames.d.ts.map +1 -0
- package/dist/showNames.js +74 -0
- package/dist/showNames.js.map +1 -0
- package/dist/streams.d.ts +27 -1
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +90 -21
- package/dist/streams.js.map +1 -1
- package/dist/types.d.ts +195 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/dist/wire.d.ts +31 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +25 -0
- package/dist/wire.js.map +1 -1
- package/dist/zego.d.ts +3 -1
- package/dist/zego.d.ts.map +1 -1
- package/dist/zego.js +1 -1
- package/dist/zegoEngine.d.ts +168 -0
- package/dist/zegoEngine.d.ts.map +1 -0
- package/dist/zegoEngine.js +1052 -0
- package/dist/zegoEngine.js.map +1 -0
- package/package.json +3 -2
|
@@ -6,9 +6,17 @@ exports.LiveSessionProvider = LiveSessionProvider;
|
|
|
6
6
|
* `LiveSessionProvider` / `useLiveSession` — the assembled session.
|
|
7
7
|
*
|
|
8
8
|
* This is the layer that turns the engine's event stream plus the polled record into one renderable
|
|
9
|
-
* state. It owns the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* state. It owns the picture-in-picture mode machine, product resolution through the commerce adapter,
|
|
10
|
+
* and every action a live screen fires.
|
|
11
|
+
*
|
|
12
|
+
* **It never ticks.** The session changes only when something happens — a room event, a record read,
|
|
13
|
+
* an action's answer — and is the same object until one of its fields changes. It holds moments, not
|
|
14
|
+
* clocks: when the round closes (`auction.closesAt`) and when the show started (`stream.startedAtMs`).
|
|
15
|
+
* A component that shows a clock counts to one of them itself (`useAuctionCountdown`, `useShowClock`,
|
|
16
|
+
* in `clocks.ts`), so only that component is drawn as the clock moves. Before 0.9.0 the session held
|
|
17
|
+
* both clocks, and every reader — every screen in the app, since the session is app-wide — was drawn
|
|
18
|
+
* five times a second during an auction round. Its own timers are timeouts to the moment they are for:
|
|
19
|
+
* the round's fallback close, and the next card or reaction to run out.
|
|
12
20
|
*
|
|
13
21
|
* **Mount it above your navigator.** The Zego engine holds exactly one room, and two places usually
|
|
14
22
|
* render the same stream — a home-screen card and the full-screen player. Owning the session per
|
|
@@ -22,18 +30,17 @@ const react_1 = require("react");
|
|
|
22
30
|
const react_native_1 = require("react-native");
|
|
23
31
|
const cartAttributes_1 = require("../cartAttributes");
|
|
24
32
|
const client_1 = require("../client");
|
|
33
|
+
const auctionRules_1 = require("../auctionRules");
|
|
34
|
+
const giveawayRoster_1 = require("../giveawayRoster");
|
|
35
|
+
const showNames_1 = require("../showNames");
|
|
36
|
+
const showLots_1 = require("../showLots");
|
|
37
|
+
const heldComments_1 = require("../heldComments");
|
|
25
38
|
const liveSelling_1 = require("../liveSelling");
|
|
26
39
|
const sdk_apptile_live_selling_native_1 = require("@tiledev/sdk-apptile-live-selling-native");
|
|
40
|
+
const pipWindow_1 = require("../pipWindow");
|
|
27
41
|
const session_1 = require("../session");
|
|
28
42
|
const useStreamInfo_1 = require("./useStreamInfo");
|
|
29
43
|
const videoTargets_1 = require("./videoTargets");
|
|
30
|
-
/** The elapsed timer and the transient-card pruning share one tick. */
|
|
31
|
-
const TICK_MS = 1000;
|
|
32
|
-
/**
|
|
33
|
-
* The auction clock runs faster than the session tick. Seconds are the unit a bidder acts on, and a
|
|
34
|
-
* 1s tick aliases against the deadline badly enough to show the same second twice and skip another.
|
|
35
|
-
*/
|
|
36
|
-
const AUCTION_TICK_MS = 200;
|
|
37
44
|
/**
|
|
38
45
|
* How long past the deadline to wait before closing the round ourselves.
|
|
39
46
|
*
|
|
@@ -42,6 +49,8 @@ const AUCTION_TICK_MS = 200;
|
|
|
42
49
|
* already given up.
|
|
43
50
|
*/
|
|
44
51
|
const AUCTION_GRACE_MS = 1000;
|
|
52
|
+
/** The longest wait a JavaScript timer can hold (2^31 − 1 ms, about 24 days); a longer one fires at once. */
|
|
53
|
+
const LONGEST_TIMER_MS = 2147483647;
|
|
45
54
|
/**
|
|
46
55
|
* How long the OS is given to actually put a picture-in-picture window up after the app goes away.
|
|
47
56
|
*
|
|
@@ -51,6 +60,11 @@ const AUCTION_GRACE_MS = 1000;
|
|
|
51
60
|
* means the host is talking to nobody from inside a backgrounded app.
|
|
52
61
|
*/
|
|
53
62
|
const PIP_GRANT_MS = 1200;
|
|
63
|
+
/**
|
|
64
|
+
* The device said it has no picture-in-picture (L21). Module state, because it is a fact about the
|
|
65
|
+
* device rather than about one session: once heard, arming stops and the error is not reported again.
|
|
66
|
+
*/
|
|
67
|
+
let pipUnavailableOnDevice = false;
|
|
54
68
|
const LiveSessionContext = (0, react_1.createContext)(null);
|
|
55
69
|
/**
|
|
56
70
|
* Reads the shared live session.
|
|
@@ -68,12 +82,11 @@ function useLiveSession() {
|
|
|
68
82
|
/* Everything but the stream hold, which belongs to the provider component: it owns which stream this
|
|
69
83
|
hook is watching, so it is the only thing that can pin it. */
|
|
70
84
|
function useSessionValue(props) {
|
|
71
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
85
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
72
86
|
const { streamingId, viewerName: viewerNameInput, onPipRestore } = props;
|
|
73
87
|
const [state, dispatch] = (0, react_1.useReducer)(session_1.sessionReducer, session_1.initialSessionState);
|
|
74
88
|
const [joining, setJoining] = (0, react_1.useState)(false);
|
|
75
89
|
const [error, setError] = (0, react_1.useState)(null);
|
|
76
|
-
const [now, setNow] = (0, react_1.useState)(() => Date.now());
|
|
77
90
|
const [muted, setMuted] = (0, react_1.useState)(false);
|
|
78
91
|
const [pipMode, setPipMode] = (0, react_1.useState)('none');
|
|
79
92
|
/**
|
|
@@ -88,6 +101,24 @@ function useSessionValue(props) {
|
|
|
88
101
|
pipModeRef.current = pipMode;
|
|
89
102
|
/** Read inside the PIP listeners, which subscribe once and would otherwise close over a stale value. */
|
|
90
103
|
const handoverRef = (0, react_1.useRef)(false);
|
|
104
|
+
/**
|
|
105
|
+
* Where the picture was when the system window took it: the in-app bubble (`'inapp'`) or a
|
|
106
|
+
* full-screen player (`'none'`). Coming back puts it there again (`whereTheWindowCameFrom`).
|
|
107
|
+
*/
|
|
108
|
+
const windowCameFromRef = (0, react_1.useRef)('none');
|
|
109
|
+
/**
|
|
110
|
+
* The session is taking the window down itself (iOS: the app came back with it still up), so the
|
|
111
|
+
* `onPipStop` that follows is not the shopper closing it.
|
|
112
|
+
*/
|
|
113
|
+
const takingWindowDownRef = (0, react_1.useRef)(false);
|
|
114
|
+
/**
|
|
115
|
+
* The show was stopped because the app is away: no window came (`PIP_GRANT_MS`), or the shopper
|
|
116
|
+
* closed the window. Until the app is back in the foreground, a surface taking the picture does not
|
|
117
|
+
* start it again (`bindVideoView`). Without this, closing a window opened from the full-screen
|
|
118
|
+
* player handed the picture straight back to that player, still focused behind the window, and the
|
|
119
|
+
* show played on with sound behind an app nobody was looking at.
|
|
120
|
+
*/
|
|
121
|
+
const stoppedWhileAwayRef = (0, react_1.useRef)(false);
|
|
91
122
|
const viewerIdRef = (0, react_1.useRef)(null);
|
|
92
123
|
const logger = (0, client_1.getLogger)();
|
|
93
124
|
const identity = (0, client_1.getIdentity)();
|
|
@@ -100,7 +131,8 @@ function useSessionValue(props) {
|
|
|
100
131
|
* name is consulted at all so a signed-in bidder is never asked to invent one.
|
|
101
132
|
*/
|
|
102
133
|
const [chosenName, setChosenName] = (0, react_1.useState)(() => liveSelling_1.liveSelling.viewer.cachedName());
|
|
103
|
-
|
|
134
|
+
// Every name here is published to the whole room, so none may be an email (`publicName`, 0.9.0).
|
|
135
|
+
const accountName = (0, showNames_1.publicName)(viewerNameInput);
|
|
104
136
|
(0, react_1.useEffect)(() => {
|
|
105
137
|
if (accountName || chosenName)
|
|
106
138
|
return;
|
|
@@ -113,31 +145,70 @@ function useSessionValue(props) {
|
|
|
113
145
|
alive = false;
|
|
114
146
|
};
|
|
115
147
|
}, [accountName, chosenName]);
|
|
116
|
-
const customerName = (
|
|
117
|
-
const namedBy = (
|
|
148
|
+
const customerName = (0, showNames_1.publicName)((_a = identity === null || identity === void 0 ? void 0 : identity.customerName) === null || _a === void 0 ? void 0 : _a.call(identity));
|
|
149
|
+
const namedBy = (_b = accountName !== null && accountName !== void 0 ? accountName : (0, showNames_1.publicName)(chosenName)) !== null && _b !== void 0 ? _b : customerName;
|
|
118
150
|
const viewerName = namedBy !== null && namedBy !== void 0 ? namedBy : 'Anonymous';
|
|
151
|
+
const bidderId = (_d = (_c = identity === null || identity === void 0 ? void 0 : identity.bidderId) === null || _c === void 0 ? void 0 : _c.call(identity)) !== null && _d !== void 0 ? _d : null;
|
|
152
|
+
/**
|
|
153
|
+
* A signed-in shopper whose name has not loaded yet. They are never asked for one — the session
|
|
154
|
+
* uses the customer's own — so a comment sent now is held until it lands (L6).
|
|
155
|
+
*/
|
|
156
|
+
const nameResolving = !namedBy && !!bidderId;
|
|
157
|
+
const accountNameRef = (0, react_1.useRef)(accountName);
|
|
158
|
+
accountNameRef.current = accountName;
|
|
159
|
+
const namedByRef = (0, react_1.useRef)(namedBy);
|
|
160
|
+
namedByRef.current = namedBy;
|
|
161
|
+
const nameResolvingRef = (0, react_1.useRef)(nameResolving);
|
|
162
|
+
nameResolvingRef.current = nameResolving;
|
|
119
163
|
/**
|
|
120
164
|
* Pushed to the engine rather than passed at join: the room is entered once, and a name given
|
|
121
165
|
* mid-show must not cost a rejoin — that interrupts the video for everyone watching on this device.
|
|
122
166
|
* Comments and reactions carry the author in their own payload.
|
|
167
|
+
*
|
|
168
|
+
* This effect alone is too late for the FIRST comment after a name is chosen, which is why
|
|
169
|
+
* `chooseViewerName` also sets it directly — see there.
|
|
123
170
|
*/
|
|
124
171
|
(0, react_1.useEffect)(() => {
|
|
125
172
|
liveSelling_1.liveSelling.engine.setViewerName(namedBy);
|
|
126
173
|
}, [namedBy]);
|
|
127
|
-
/**
|
|
128
|
-
* The auction clock, separate from `now` so a 200ms tick does not redraw the comment pane five
|
|
129
|
-
* times a second on an ordinary live stream. This one only runs while a round is open.
|
|
130
|
-
*/
|
|
131
|
-
const [auctionNow, setAuctionNow] = (0, react_1.useState)(() => Date.now());
|
|
132
174
|
// Poll so a stream ending (or the host swapping the product set) is noticed without a manual
|
|
133
175
|
// refresh. The engine's channels cover in-stream changes; this covers the record itself. With no
|
|
134
176
|
// `streamingId` it follows whichever stream is on air.
|
|
135
|
-
const { stream, refresh: refreshStreamInfo } = (0, useStreamInfo_1.useStreamInfo)(streamingId);
|
|
177
|
+
const { stream, refresh: refreshStreamInfo, hasRead, forcedReadAt } = (0, useStreamInfo_1.useStreamInfo)(streamingId);
|
|
136
178
|
const watching = streamingId !== null && streamingId !== void 0 ? streamingId : stream === null || stream === void 0 ? void 0 : stream.streamingId;
|
|
179
|
+
/** The comment pipeline; `heldComments` uses it, so it is defined before the show reset below. */
|
|
180
|
+
const deliverComment = (0, react_1.useCallback)(async (text, options) => {
|
|
181
|
+
var _a;
|
|
182
|
+
try {
|
|
183
|
+
// The host mutes by device; a per-install viewer id is the same granularity.
|
|
184
|
+
const deviceId = (_a = viewerIdRef.current) !== null && _a !== void 0 ? _a : (await liveSelling_1.liveSelling.viewer.id());
|
|
185
|
+
const own = await liveSelling_1.liveSelling.engine.sendComment(text, deviceId, options);
|
|
186
|
+
if (own)
|
|
187
|
+
dispatch({ kind: 'ownComment', comment: own });
|
|
188
|
+
}
|
|
189
|
+
catch (caught) {
|
|
190
|
+
(0, client_1.getLogger)().error(caught, { at: 'liveSession.sendComment' });
|
|
191
|
+
}
|
|
192
|
+
}, []);
|
|
193
|
+
const heldCommentsRef = (0, react_1.useRef)(null);
|
|
194
|
+
if (!heldCommentsRef.current) {
|
|
195
|
+
heldCommentsRef.current = new heldComments_1.HeldComments(({ text, options }) => deliverComment(text, options));
|
|
196
|
+
}
|
|
137
197
|
(0, react_1.useEffect)(() => {
|
|
198
|
+
var _a;
|
|
138
199
|
dispatch({ kind: 'reset' });
|
|
139
200
|
setError(null);
|
|
201
|
+
// A comment held for the last show's room must not be said in this one.
|
|
202
|
+
(_a = heldCommentsRef.current) === null || _a === void 0 ? void 0 : _a.drop();
|
|
140
203
|
}, [watching]);
|
|
204
|
+
/** The name arrived: say it to the engine first, then send what was waiting for it, in order. */
|
|
205
|
+
(0, react_1.useEffect)(() => {
|
|
206
|
+
var _a;
|
|
207
|
+
if (!namedBy || !((_a = heldCommentsRef.current) === null || _a === void 0 ? void 0 : _a.size))
|
|
208
|
+
return;
|
|
209
|
+
liveSelling_1.liveSelling.engine.setViewerName(namedBy);
|
|
210
|
+
void heldCommentsRef.current.release();
|
|
211
|
+
}, [namedBy]);
|
|
141
212
|
(0, react_1.useEffect)(() => liveSelling_1.liveSelling.engine.subscribe((event) => {
|
|
142
213
|
dispatch({ kind: 'event', event, at: Date.now() });
|
|
143
214
|
}), []);
|
|
@@ -208,14 +279,42 @@ function useSessionValue(props) {
|
|
|
208
279
|
* importantly an *unmute*, which is otherwise invisible until the app restarts. Keyed on the joined
|
|
209
280
|
* ids because the array is a fresh object on every poll.
|
|
210
281
|
*/
|
|
211
|
-
const mutedKey = ((
|
|
282
|
+
const mutedKey = ((_e = stream === null || stream === void 0 ? void 0 : stream.mutedDeviceIds) !== null && _e !== void 0 ? _e : []).join(',');
|
|
212
283
|
(0, react_1.useEffect)(() => {
|
|
213
284
|
if (!stream)
|
|
214
285
|
return;
|
|
215
286
|
liveSelling_1.liveSelling.engine.setMutedDeviceIds(mutedKey ? mutedKey.split(',') : []);
|
|
216
287
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
217
288
|
}, [mutedKey]);
|
|
218
|
-
|
|
289
|
+
/** The room's and the record's view, before the off-air wait below has its say. */
|
|
290
|
+
const derivedStatus = (0, session_1.deriveStatus)(state, !!stream, joining, error, (stream === null || stream === void 0 ? void 0 : stream.status) === 'live');
|
|
291
|
+
/**
|
|
292
|
+
* Nothing on air, settled — Freckled Poppy's `NOTHING_ON_AIR_MS`, moved here.
|
|
293
|
+
*
|
|
294
|
+
* With no record at all `deriveStatus` says `loading`, and keeps saying it: a deep link to a show
|
|
295
|
+
* that has gone, or a session with nothing live, spun for ever. After `offAirGraceMs` (4 s) with a
|
|
296
|
+
* read finished, nothing live in it, and the room not reconnecting, the status becomes `ended`.
|
|
297
|
+
*
|
|
298
|
+
* Every forced read (`refreshStreamRecords()`, which a screen calls on focus) starts the wait again.
|
|
299
|
+
* That is the guard against a stale record: a push tap lands on a screen whose last read predates
|
|
300
|
+
* the show, and without the restart the show would be declared over before the fresh read came
|
|
301
|
+
* back.
|
|
302
|
+
*/
|
|
303
|
+
const nothingOnAir = (0, session_1.isNothingOnAir)({
|
|
304
|
+
hasRead,
|
|
305
|
+
streamStatus: (_f = stream === null || stream === void 0 ? void 0 : stream.status) !== null && _f !== void 0 ? _f : null,
|
|
306
|
+
status: derivedStatus,
|
|
307
|
+
});
|
|
308
|
+
const graceMs = (0, client_1.offAirGraceMs)();
|
|
309
|
+
const [settledOffAir, setSettledOffAir] = (0, react_1.useState)(false);
|
|
310
|
+
(0, react_1.useEffect)(() => {
|
|
311
|
+
setSettledOffAir(false);
|
|
312
|
+
if (!nothingOnAir)
|
|
313
|
+
return;
|
|
314
|
+
const timer = setTimeout(() => setSettledOffAir(true), graceMs);
|
|
315
|
+
return () => clearTimeout(timer);
|
|
316
|
+
}, [nothingOnAir, forcedReadAt, graceMs]);
|
|
317
|
+
const status = (0, session_1.withOffAirSettled)(derivedStatus, settledOffAir);
|
|
219
318
|
// A PIP surface outlives the screen that opened it, so nothing else would take it down when the
|
|
220
319
|
// host ends the stream — it would sit there over the app showing a frozen last frame.
|
|
221
320
|
(0, react_1.useEffect)(() => {
|
|
@@ -227,15 +326,19 @@ function useSessionValue(props) {
|
|
|
227
326
|
// The host's end broadcast reaches us long before the polled record catches up. Stop the picture,
|
|
228
327
|
// then force a refetch so every poll-driven surface drops the stream instead of waiting out the
|
|
229
328
|
// interval.
|
|
329
|
+
//
|
|
330
|
+
// Keyed on the status BEFORE the off-air wait: an end the wait declared has no record to catch up
|
|
331
|
+
// with, and its forced read would restart the wait — which would end the show again four seconds
|
|
332
|
+
// later, for ever.
|
|
230
333
|
(0, react_1.useEffect)(() => {
|
|
231
|
-
if (
|
|
334
|
+
if (derivedStatus !== 'ended')
|
|
232
335
|
return;
|
|
233
336
|
liveSelling_1.liveSelling.engine
|
|
234
337
|
.stopPlayback()
|
|
235
338
|
.catch((caught) => logger.error(caught, { at: 'liveSession.stopOnEnd' }));
|
|
236
339
|
(0, useStreamInfo_1.refreshStreamRecords)();
|
|
237
340
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
|
-
}, [
|
|
341
|
+
}, [derivedStatus]);
|
|
239
342
|
/**
|
|
240
343
|
* Whether a mounted surface has asked to hand over to a system window.
|
|
241
344
|
*
|
|
@@ -253,16 +356,29 @@ function useSessionValue(props) {
|
|
|
253
356
|
// `pipMode !== 'none'` covers the in-app bubble; `handoverWanted` is what lets the FULL-SCREEN
|
|
254
357
|
// player hand over too. Without it, minimising from the player armed nothing, no window opened,
|
|
255
358
|
// and the audio played on behind the app because the plugin declares the `audio` background mode.
|
|
359
|
+
//
|
|
360
|
+
// What was last armed, and when it was last disarmed, are what decide on Android whether a window
|
|
361
|
+
// that opens is ours — see `ownsSystemPipWindow`.
|
|
362
|
+
//
|
|
363
|
+
// Only while the show is `playing` (`shouldArmSystemWindow`): joining, a host off camera and an ended
|
|
364
|
+
// show have no picture to put in a window. A window already up stays armed whatever the status does.
|
|
365
|
+
const armedRef = (0, react_1.useRef)(false);
|
|
366
|
+
const disarmedAtRef = (0, react_1.useRef)(0);
|
|
256
367
|
(0, react_1.useEffect)(() => {
|
|
257
368
|
if (!sdk_apptile_live_selling_native_1.isLivePipSupported)
|
|
258
369
|
return;
|
|
259
|
-
const armed = pipMode
|
|
260
|
-
|
|
370
|
+
const armed = (0, pipWindow_1.shouldArmSystemWindow)({ pipMode, handoverWanted, status });
|
|
371
|
+
if (armedRef.current && !armed)
|
|
372
|
+
disarmedAtRef.current = Date.now();
|
|
373
|
+
armedRef.current = armed;
|
|
374
|
+
// A device without picture-in-picture answers every arming with an error (L21); stop asking.
|
|
375
|
+
if (!(armed && pipUnavailableOnDevice))
|
|
376
|
+
void sdk_apptile_live_selling_native_1.livePip.setArmed(armed);
|
|
261
377
|
liveSelling_1.liveSelling.engine
|
|
262
378
|
.setBackgroundVideoEnabled(armed)
|
|
263
379
|
.catch((caught) => logger.error(caught, { at: 'liveSession.backgroundVideo' }));
|
|
264
380
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
265
|
-
}, [pipMode, handoverWanted]);
|
|
381
|
+
}, [pipMode, handoverWanted, status]);
|
|
266
382
|
/**
|
|
267
383
|
* The sound must not outlive the app when no window follows.
|
|
268
384
|
*
|
|
@@ -271,11 +387,16 @@ function useSessionValue(props) {
|
|
|
271
387
|
* declared. So a backgrounded session gives the OS `PIP_GRANT_MS` to produce a window and stops
|
|
272
388
|
* playback if none arrived. `pipModeRef` rather than `pipMode`, because this subscribes once and
|
|
273
389
|
* the answer is only knowable when the timer fires.
|
|
390
|
+
*
|
|
391
|
+
* A show stopped here stays stopped until the app is back (`stoppedWhileAwayRef`): a surface that
|
|
392
|
+
* re-claims meanwhile — its status changed, a window was closed — would otherwise start it again
|
|
393
|
+
* behind the app. Back in the foreground, `LiveVideoTarget` takes the picture again by itself.
|
|
274
394
|
*/
|
|
275
395
|
(0, react_1.useEffect)(() => {
|
|
276
396
|
let grant = null;
|
|
277
397
|
const subscription = react_native_1.AppState.addEventListener('change', (next) => {
|
|
278
398
|
if (next === 'active') {
|
|
399
|
+
stoppedWhileAwayRef.current = false;
|
|
279
400
|
if (grant)
|
|
280
401
|
clearTimeout(grant);
|
|
281
402
|
grant = null;
|
|
@@ -289,6 +410,7 @@ function useSessionValue(props) {
|
|
|
289
410
|
grant = null;
|
|
290
411
|
if (pipModeRef.current === 'system')
|
|
291
412
|
return;
|
|
413
|
+
stoppedWhileAwayRef.current = true;
|
|
292
414
|
liveSelling_1.liveSelling.engine
|
|
293
415
|
.stopPlayback()
|
|
294
416
|
.catch((caught) => logger.error(caught, { at: 'liveSession.noPipWindow' }));
|
|
@@ -306,22 +428,52 @@ function useSessionValue(props) {
|
|
|
306
428
|
return;
|
|
307
429
|
/**
|
|
308
430
|
* Only ours if this session had a surface up first — either the in-app bubble, or the full-screen
|
|
309
|
-
* player having asked for the handover
|
|
310
|
-
*
|
|
311
|
-
* opened.
|
|
431
|
+
* player having asked for the handover — or, on Android, if this session armed the window the OS
|
|
432
|
+
* just opened. Anything else minimising into a system window raises the same event, and taking it
|
|
433
|
+
* means the live session believes it is in a PIP window it never opened. See `ownsSystemPipWindow`
|
|
434
|
+
* for why Android cannot go by the handover alone.
|
|
312
435
|
*/
|
|
313
436
|
const offStart = sdk_apptile_live_selling_native_1.livePip.addListener('onPipStart', () => {
|
|
314
|
-
|
|
437
|
+
const ours = (0, pipWindow_1.ownsSystemPipWindow)({
|
|
438
|
+
platform: react_native_1.Platform.OS,
|
|
439
|
+
pipMode: pipModeRef.current,
|
|
440
|
+
handoverWanted: handoverRef.current,
|
|
441
|
+
armed: armedRef.current,
|
|
442
|
+
disarmedAt: disarmedAtRef.current,
|
|
443
|
+
now: Date.now(),
|
|
444
|
+
});
|
|
445
|
+
if (!ours)
|
|
315
446
|
return;
|
|
447
|
+
windowCameFromRef.current = (0, pipWindow_1.whereTheWindowCameFrom)(pipModeRef.current, windowCameFromRef.current);
|
|
448
|
+
takingWindowDownRef.current = false;
|
|
316
449
|
setPipMode('system');
|
|
317
450
|
});
|
|
451
|
+
/**
|
|
452
|
+
* Coming back: the picture goes back to where the window took it from. The in-app bubble is the
|
|
453
|
+
* app's own surface and is still mounted, so `'inapp'` is all it takes; a full-screen player is a
|
|
454
|
+
* route only the app can navigate to, so it hears `onPipRestore`. Set in the same event as the
|
|
455
|
+
* window's end, so no render ever sees `'none'` on the way back to the bubble: a card that claims
|
|
456
|
+
* on `'none'` would take the picture for one frame and leave a black one behind.
|
|
457
|
+
*/
|
|
458
|
+
const giveThePictureBack = () => {
|
|
459
|
+
const place = windowCameFromRef.current;
|
|
460
|
+
setPipMode(place);
|
|
461
|
+
if (place === 'none')
|
|
462
|
+
onPipRestore === null || onPipRestore === void 0 ? void 0 : onPipRestore(watching);
|
|
463
|
+
};
|
|
318
464
|
// Expanding and closing both end in `onPipStop`; `restored` is the only thing that tells them
|
|
319
|
-
// apart, and the expand path is handled by `onPipRestore` instead.
|
|
465
|
+
// apart, and the expand path is handled by `onPipRestore` instead. Closing stops the show, and it
|
|
466
|
+
// stays stopped until the app is back in the foreground (`stoppedWhileAwayRef`).
|
|
320
467
|
const offStop = sdk_apptile_live_selling_native_1.livePip.addListener('onPipStop', ({ restored }) => {
|
|
468
|
+
if (takingWindowDownRef.current) {
|
|
469
|
+
takingWindowDownRef.current = false;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
321
472
|
if (restored)
|
|
322
473
|
return;
|
|
323
474
|
if (pipModeRef.current !== 'system')
|
|
324
475
|
return;
|
|
476
|
+
stoppedWhileAwayRef.current = true;
|
|
325
477
|
setPipMode('none');
|
|
326
478
|
liveSelling_1.liveSelling.engine
|
|
327
479
|
.stopPlayback()
|
|
@@ -330,14 +482,35 @@ function useSessionValue(props) {
|
|
|
330
482
|
const offRestore = sdk_apptile_live_selling_native_1.livePip.addListener('onPipRestore', () => {
|
|
331
483
|
if (pipModeRef.current !== 'system')
|
|
332
484
|
return;
|
|
333
|
-
|
|
334
|
-
|
|
485
|
+
giveThePictureBack();
|
|
486
|
+
});
|
|
487
|
+
/* iOS: back in the app with the window still up (see `isWindowStillUpOnReturn`). The picture goes
|
|
488
|
+
back where it was, then the window is taken down; its `onPipStop` is ours, not a close. */
|
|
489
|
+
const offReturn = react_native_1.AppState.addEventListener('change', (next) => {
|
|
490
|
+
if (next !== 'active')
|
|
491
|
+
return;
|
|
492
|
+
if (!(0, pipWindow_1.isWindowStillUpOnReturn)(react_native_1.Platform.OS, pipModeRef.current))
|
|
493
|
+
return;
|
|
494
|
+
takingWindowDownRef.current = true;
|
|
495
|
+
giveThePictureBack();
|
|
496
|
+
void sdk_apptile_live_selling_native_1.livePip.stop();
|
|
497
|
+
});
|
|
498
|
+
/* "Not available on this device" is a fact about the device, reported once as a warning — not an
|
|
499
|
+
error filed every time the bubble opens (L21). */
|
|
500
|
+
const offError = sdk_apptile_live_selling_native_1.livePip.addListener('onPipError', ({ message }) => {
|
|
501
|
+
if ((0, pipWindow_1.isPipUnavailableMessage)(message)) {
|
|
502
|
+
if (!pipUnavailableOnDevice)
|
|
503
|
+
(0, client_1.logWarning)(new Error(message), { at: 'liveSession.pipUnavailable' });
|
|
504
|
+
pipUnavailableOnDevice = true;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
logger.error(new Error(message), { at: 'liveSession.pipError' });
|
|
335
508
|
});
|
|
336
|
-
const offError = sdk_apptile_live_selling_native_1.livePip.addListener('onPipError', ({ message }) => logger.error(new Error(message), { at: 'liveSession.pipError' }));
|
|
337
509
|
return () => {
|
|
338
510
|
offStart === null || offStart === void 0 ? void 0 : offStart();
|
|
339
511
|
offStop === null || offStop === void 0 ? void 0 : offStop();
|
|
340
512
|
offRestore === null || offRestore === void 0 ? void 0 : offRestore();
|
|
513
|
+
offReturn.remove();
|
|
341
514
|
offError === null || offError === void 0 ? void 0 : offError();
|
|
342
515
|
};
|
|
343
516
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -355,39 +528,45 @@ function useSessionValue(props) {
|
|
|
355
528
|
return () => subscription.remove();
|
|
356
529
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
357
530
|
}, []);
|
|
531
|
+
/**
|
|
532
|
+
* Sold cards, cart cards and reactions leave when their time is up (`SOLD_CARD_MS`, `CART_CARD_MS`,
|
|
533
|
+
* `REACTION_MS`): one timer, to the moment the next one runs out (`nextExpiryAt`), set again whenever
|
|
534
|
+
* that moment changes. Nothing runs with none on screen.
|
|
535
|
+
*
|
|
536
|
+
* The prune is told it is that moment even if the timer fires a millisecond early, so the card it
|
|
537
|
+
* was set for is always the one that goes — otherwise nothing would change, the moment would stay
|
|
538
|
+
* the same, and no timer would be set again.
|
|
539
|
+
*/
|
|
540
|
+
const nextExpiry = (0, session_1.nextExpiryAt)(state);
|
|
358
541
|
(0, react_1.useEffect)(() => {
|
|
359
|
-
if (
|
|
360
|
-
state.sold.length === 0 &&
|
|
361
|
-
state.cartAnnouncements.length === 0 &&
|
|
362
|
-
state.reactions.length === 0) {
|
|
542
|
+
if (nextExpiry === null)
|
|
363
543
|
return;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}, TICK_MS);
|
|
370
|
-
return () => clearInterval(timer);
|
|
371
|
-
}, [status, state.sold.length, state.cartAnnouncements.length, state.reactions.length]);
|
|
544
|
+
const timer = setTimeout(() => {
|
|
545
|
+
dispatch({ kind: 'prune', now: Math.max(Date.now(), nextExpiry) });
|
|
546
|
+
}, Math.max(0, nextExpiry - Date.now()));
|
|
547
|
+
return () => clearTimeout(timer);
|
|
548
|
+
}, [nextExpiry]);
|
|
372
549
|
/**
|
|
373
|
-
* The
|
|
374
|
-
*
|
|
375
|
-
*
|
|
550
|
+
* The round's fallback close. The host normally closes the round; when nothing arrives by the
|
|
551
|
+
* deadline plus the grace period, the round is over as far as this device is concerned. One timer to
|
|
552
|
+
* that moment, set again when a late bid moves the deadline, and gone when the round closes.
|
|
553
|
+
*
|
|
554
|
+
* Destructured rather than read off `state`, so the timer is keyed on exactly the three things that
|
|
555
|
+
* move the moment. An open round with no deadline yet (a bid heard before the opening, carrying
|
|
556
|
+
* none) closes at once, as it did on the old 200 ms clock: there is nothing to wait for.
|
|
376
557
|
*/
|
|
377
558
|
const { biddingOpen, endsAtMs, serverOffsetMs } = state;
|
|
378
559
|
(0, react_1.useEffect)(() => {
|
|
379
560
|
if (!biddingOpen)
|
|
380
561
|
return;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}, AUCTION_TICK_MS);
|
|
390
|
-
return () => clearInterval(timer);
|
|
562
|
+
const closeAt = endsAtMs - serverOffsetMs + AUCTION_GRACE_MS;
|
|
563
|
+
const wait = Math.max(0, closeAt - Date.now());
|
|
564
|
+
// A timer holds at most 2^31 − 1 ms (about 24 days) and fires at once past that. A deadline that
|
|
565
|
+
// far away is bad data, and the old clock never closed it early either.
|
|
566
|
+
if (wait > LONGEST_TIMER_MS)
|
|
567
|
+
return;
|
|
568
|
+
const timer = setTimeout(() => dispatch({ kind: 'auctionExpired' }), wait);
|
|
569
|
+
return () => clearTimeout(timer);
|
|
391
570
|
}, [biddingOpen, endsAtMs, serverOffsetMs]);
|
|
392
571
|
/**
|
|
393
572
|
* Giveaway state off the polled record.
|
|
@@ -397,7 +576,6 @@ function useSessionValue(props) {
|
|
|
397
576
|
* enter a giveaway they are already in.
|
|
398
577
|
*/
|
|
399
578
|
const activeGiveaway = (0, react_1.useMemo)(() => { var _a; return (_a = stream === null || stream === void 0 ? void 0 : stream.giveaways.find((giveaway) => giveaway.status === 'active')) !== null && _a !== void 0 ? _a : null; }, [stream]);
|
|
400
|
-
const bidderId = (_f = (_e = identity === null || identity === void 0 ? void 0 : identity.bidderId) === null || _e === void 0 ? void 0 : _e.call(identity)) !== null && _f !== void 0 ? _f : null;
|
|
401
579
|
const customerRef = (_h = (_g = identity === null || identity === void 0 ? void 0 : identity.customerRef) === null || _g === void 0 ? void 0 : _g.call(identity)) !== null && _h !== void 0 ? _h : null;
|
|
402
580
|
/**
|
|
403
581
|
* Closes the gap between the realtime giveaway start and the polled record it depends on.
|
|
@@ -414,6 +592,7 @@ function useSessionValue(props) {
|
|
|
414
592
|
void refreshStreamInfo();
|
|
415
593
|
}, [giveawayKnown, refreshStreamInfo]);
|
|
416
594
|
(0, react_1.useEffect)(() => {
|
|
595
|
+
var _a;
|
|
417
596
|
if (!activeGiveaway)
|
|
418
597
|
return;
|
|
419
598
|
const entries = activeGiveaway.entries.filter((entry) => entry.active);
|
|
@@ -423,8 +602,37 @@ function useSessionValue(props) {
|
|
|
423
602
|
entryCount: entries.length,
|
|
424
603
|
registered: !!bidderId && entries.some((entry) => entry.userId === bidderId),
|
|
425
604
|
bidderId,
|
|
605
|
+
entrantNames: activeGiveaway.entrantNames,
|
|
606
|
+
startedAtMs: (_a = activeGiveaway.startedAtMs) !== null && _a !== void 0 ? _a : null,
|
|
426
607
|
});
|
|
427
608
|
}, [activeGiveaway, bidderId]);
|
|
609
|
+
/**
|
|
610
|
+
* The draw's roster — ENG-705, moved here from Freckled Poppy's Live screen.
|
|
611
|
+
*
|
|
612
|
+
* `giveaway_end` names the winner and nobody else, and reaches the room before the gateway has
|
|
613
|
+
* written the entrants onto the record. When the roster latched while the lot was open is empty
|
|
614
|
+
* too, the record is asked again every 700 ms for up to 5 s; the first read with names is taken and
|
|
615
|
+
* kept (`giveawayRoster`), and running out of time lets the reveal go ahead without one.
|
|
616
|
+
*/
|
|
617
|
+
const rosterWait = state.giveawayResult && !state.giveawayResult.entrantNamesReady
|
|
618
|
+
? `${state.giveawayResult.storeProductId}\u0000${state.giveawayResult.winnerName}`
|
|
619
|
+
: null;
|
|
620
|
+
(0, react_1.useEffect)(() => {
|
|
621
|
+
if (!rosterWait)
|
|
622
|
+
return;
|
|
623
|
+
return (0, giveawayRoster_1.waitForGiveawayRoster)({
|
|
624
|
+
readRecord: () => void refreshStreamInfo(),
|
|
625
|
+
onGiveUp: () => dispatch({ kind: 'giveawayRosterTimedOut' }),
|
|
626
|
+
});
|
|
627
|
+
}, [rosterWait, refreshStreamInfo]);
|
|
628
|
+
(0, react_1.useEffect)(() => {
|
|
629
|
+
const result = state.giveawayResult;
|
|
630
|
+
if (!result || result.entrantNamesReady || !stream)
|
|
631
|
+
return;
|
|
632
|
+
const names = (0, giveawayRoster_1.rosterFromRecord)(stream.giveaways, result.storeProductId);
|
|
633
|
+
if (names.length)
|
|
634
|
+
dispatch({ kind: 'giveawayRoster', storeProductId: result.storeProductId, names });
|
|
635
|
+
}, [state.giveawayResult, stream]);
|
|
428
636
|
/**
|
|
429
637
|
* The poll off the RECORD, for one this device never heard open — DEL-2769.
|
|
430
638
|
*
|
|
@@ -483,29 +691,93 @@ function useSessionValue(props) {
|
|
|
483
691
|
}));
|
|
484
692
|
}, [stream, state.removedStoreProductIds]);
|
|
485
693
|
/**
|
|
486
|
-
*
|
|
694
|
+
* The host featured a lot (`PRODUCT_CHANGE`): read the record again, once per change.
|
|
695
|
+
*
|
|
696
|
+
* Two things live only on the record and change at exactly this moment:
|
|
487
697
|
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
698
|
+
* - **The lot number.** On a company with show-gated products the gateway issues `sell_id` when the
|
|
699
|
+
* host SHOWS the lot (DEL-2885), and nothing re-reads the record mid-watch — so `sellId` stayed
|
|
700
|
+
* null for the whole session. Freckled Poppy forced this read from its Live screen; it lives here
|
|
701
|
+
* now, for every app.
|
|
702
|
+
* - **A link lot added mid-stream.** Its name, thumbnail and URL exist nowhere else, so without the
|
|
703
|
+
* read it resolved to nothing and `activeProduct` fell back to a different product.
|
|
493
704
|
*
|
|
494
|
-
*
|
|
495
|
-
* must not turn into a poll every render.
|
|
705
|
+
* Skipped when the record already lists the lot with its number: the read would bring nothing new.
|
|
496
706
|
*/
|
|
497
|
-
const chasedRecordFor = (0, react_1.useRef)(null);
|
|
498
707
|
(0, react_1.useEffect)(() => {
|
|
708
|
+
var _a;
|
|
499
709
|
const active = state.activeStoreProductId;
|
|
500
|
-
if (!active
|
|
710
|
+
if (!active)
|
|
711
|
+
return;
|
|
712
|
+
const row = (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.products.find((product) => product.storeProductId === active);
|
|
713
|
+
if (row === null || row === void 0 ? void 0 : row.sellId)
|
|
501
714
|
return;
|
|
502
|
-
|
|
715
|
+
void refreshStreamInfo();
|
|
716
|
+
}, [state.activeStoreProductId, refreshStreamInfo]);
|
|
717
|
+
/**
|
|
718
|
+
* What the room said before this device arrived, put in front of the comments — once per show.
|
|
719
|
+
*
|
|
720
|
+
* Read when the show is known to be live, and dropped if the show changed while it was in flight.
|
|
721
|
+
* `mergeCommentHistory` (in the reducer) leaves out anything the room has already delivered and
|
|
722
|
+
* renames the host's stored "You" to the host.
|
|
723
|
+
*/
|
|
724
|
+
const liveStreamingId = (stream === null || stream === void 0 ? void 0 : stream.status) === 'live' ? stream.streamingId : null;
|
|
725
|
+
(0, react_1.useEffect)(() => {
|
|
726
|
+
if (!liveStreamingId || react_native_1.Platform.OS === 'web')
|
|
727
|
+
return;
|
|
728
|
+
let cancelled = false;
|
|
729
|
+
liveSelling_1.liveSelling.streams
|
|
730
|
+
.commentHistory(liveStreamingId)
|
|
731
|
+
.then((history) => {
|
|
732
|
+
var _a, _b;
|
|
733
|
+
if (cancelled || !history.length)
|
|
734
|
+
return;
|
|
735
|
+
dispatch({
|
|
736
|
+
kind: 'commentHistory',
|
|
737
|
+
history,
|
|
738
|
+
hostName: (_b = (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.hostName) !== null && _b !== void 0 ? _b : null,
|
|
739
|
+
ownName: namedByRef.current,
|
|
740
|
+
});
|
|
741
|
+
})
|
|
742
|
+
.catch((caught) => logger.error(caught, { at: 'liveSession.commentHistory', streamingId: liveStreamingId }));
|
|
743
|
+
return () => {
|
|
744
|
+
cancelled = true;
|
|
745
|
+
};
|
|
746
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
747
|
+
}, [liveStreamingId]);
|
|
748
|
+
/**
|
|
749
|
+
* The pinned comment, off the RECORD — for arrival, and for every reconnect.
|
|
750
|
+
*
|
|
751
|
+
* The room's `PINNED_COMMENT_CHANGED` reaches the reducer directly and wins until the record's own
|
|
752
|
+
* pin changes. Dispatched only when the record's pin CHANGES (keyed with the show), so a read
|
|
753
|
+
* taken before an unpin cannot bring the old pin back — the same precedence the record's poll has.
|
|
754
|
+
*
|
|
755
|
+
* A broadcast sent while the room was down never arrives, so a reconnect re-reads the record and
|
|
756
|
+
* the first record after it is taken whatever it says. Joining counts as a reconnect, so arriving
|
|
757
|
+
* costs one extra read.
|
|
758
|
+
*/
|
|
759
|
+
const recordPin = (_j = stream === null || stream === void 0 ? void 0 : stream.pinnedComment) !== null && _j !== void 0 ? _j : null;
|
|
760
|
+
const recordPinKey = `${watching !== null && watching !== void 0 ? watching : ''}\u0000${(_k = recordPin === null || recordPin === void 0 ? void 0 : recordPin.id) !== null && _k !== void 0 ? _k : ''}\u0000${(_l = recordPin === null || recordPin === void 0 ? void 0 : recordPin.text) !== null && _l !== void 0 ? _l : ''}`;
|
|
761
|
+
const lastRecordPinKey = (0, react_1.useRef)(null);
|
|
762
|
+
const takeNextRecordPin = (0, react_1.useRef)(false);
|
|
763
|
+
(0, react_1.useEffect)(() => {
|
|
764
|
+
if (!stream)
|
|
503
765
|
return;
|
|
504
|
-
if (
|
|
766
|
+
if (recordPinKey === lastRecordPinKey.current && !takeNextRecordPin.current)
|
|
505
767
|
return;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
768
|
+
lastRecordPinKey.current = recordPinKey;
|
|
769
|
+
takeNextRecordPin.current = false;
|
|
770
|
+
dispatch({ kind: 'pinnedCommentSynced', comment: recordPin });
|
|
771
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
772
|
+
}, [stream, recordPinKey]);
|
|
773
|
+
const wasConnected = (0, react_1.useRef)(false);
|
|
774
|
+
(0, react_1.useEffect)(() => {
|
|
775
|
+
if (state.connected && !wasConnected.current) {
|
|
776
|
+
takeNextRecordPin.current = true;
|
|
777
|
+
void refreshStreamInfo();
|
|
778
|
+
}
|
|
779
|
+
wasConnected.current = state.connected;
|
|
780
|
+
}, [state.connected, refreshStreamInfo]);
|
|
509
781
|
/**
|
|
510
782
|
* Resolved through the commerce adapter, keyed on the joined ids so a mid-stream product add
|
|
511
783
|
* refetches and an unchanged set does not. Link lots are held out: they have no id the catalogue
|
|
@@ -559,27 +831,15 @@ function useSessionValue(props) {
|
|
|
559
831
|
return ordered;
|
|
560
832
|
};
|
|
561
833
|
/*
|
|
562
|
-
* Sell codes live on the stream record, not in the catalogue — the commerce
|
|
563
|
-
* products by id and has no idea a lot carries
|
|
564
|
-
* `product.sellId` instead of holding both lists and
|
|
834
|
+
* Sell codes and opening bids live on the stream record, not in the catalogue — the commerce
|
|
835
|
+
* adapter is asked for products by id and has no idea a lot carries either. Joined on here so a
|
|
836
|
+
* call site reads `product.sellId` and `product.baseBidPrice` instead of holding both lists and
|
|
837
|
+
* matching them itself (`withLotFieldsFromRecord`; the opening bid since 0.9.0).
|
|
565
838
|
*
|
|
566
|
-
* A new array only when
|
|
567
|
-
* the adapter's own objects and the identity the memo already promised.
|
|
839
|
+
* A new array only when the record has either for some lot, so an app on a gateway that issues
|
|
840
|
+
* none keeps the adapter's own objects and the identity the memo already promised.
|
|
568
841
|
*/
|
|
569
|
-
|
|
570
|
-
const codes = new Map();
|
|
571
|
-
for (const row of (_a = stream === null || stream === void 0 ? void 0 : stream.products) !== null && _a !== void 0 ? _a : []) {
|
|
572
|
-
if (row.sellId)
|
|
573
|
-
codes.set(row.storeProductId, row.sellId);
|
|
574
|
-
}
|
|
575
|
-
if (!codes.size)
|
|
576
|
-
return base;
|
|
577
|
-
return base.map((product) => {
|
|
578
|
-
var _a;
|
|
579
|
-
return codes.has(product.storeProductId)
|
|
580
|
-
? { ...product, sellId: (_a = codes.get(product.storeProductId)) !== null && _a !== void 0 ? _a : null }
|
|
581
|
-
: product;
|
|
582
|
-
});
|
|
842
|
+
return (0, showLots_1.withLotFieldsFromRecord)(merge(), (_a = stream === null || stream === void 0 ? void 0 : stream.products) !== null && _a !== void 0 ? _a : []);
|
|
583
843
|
}, [catalogueProducts, externalProducts, storeProductIds, stream]);
|
|
584
844
|
const activeProduct = (0, react_1.useMemo)(() => {
|
|
585
845
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -607,11 +867,10 @@ function useSessionValue(props) {
|
|
|
607
867
|
return null;
|
|
608
868
|
return (_f = products[0]) !== null && _f !== void 0 ? _f : null;
|
|
609
869
|
}, [products, state.activeStoreProductId, stream]);
|
|
610
|
-
const multiplier = (
|
|
870
|
+
const multiplier = (_o = (_m = state.multiplierOverride) !== null && _m !== void 0 ? _m : stream === null || stream === void 0 ? void 0 : stream.viewerMultiplier) !== null && _o !== void 0 ? _o : 1;
|
|
611
871
|
/* Same precedence as the multiplier: the host's live word wins, the record stands until they
|
|
612
872
|
give one. Both fall back rather than throw, so a stream row missing `meta` still renders. */
|
|
613
|
-
const reactionStyle = (
|
|
614
|
-
const elapsedMs = (stream === null || stream === void 0 ? void 0 : stream.startedAtMs) ? Math.max(0, now - stream.startedAtMs) : 0;
|
|
873
|
+
const reactionStyle = (_q = (_p = state.reactionModeOverride) !== null && _p !== void 0 ? _p : stream === null || stream === void 0 ? void 0 : stream.reactionStyle) !== null && _q !== void 0 ? _q : 'single';
|
|
615
874
|
/**
|
|
616
875
|
* Percentages come off the raw counts, so the multiplier cannot skew them — it inflates only the
|
|
617
876
|
* total on display, exactly as the host's own dashboard does.
|
|
@@ -689,27 +948,28 @@ function useSessionValue(props) {
|
|
|
689
948
|
alive = false;
|
|
690
949
|
};
|
|
691
950
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
692
|
-
}, [(
|
|
951
|
+
}, [(_r = state.poll) === null || _r === void 0 ? void 0 : _r.pollUuid]);
|
|
693
952
|
const bindVideoView = (0, react_1.useCallback)((reactTag, force = false) => {
|
|
953
|
+
// Stopped because the app is away: a claim now would start it behind the app, with sound. The
|
|
954
|
+
// surface claims again once the app is back (`LiveVideoTarget`'s foreground re-claim).
|
|
955
|
+
if (stoppedWhileAwayRef.current)
|
|
956
|
+
return;
|
|
694
957
|
liveSelling_1.liveSelling.engine
|
|
695
958
|
.bindView(reactTag, force)
|
|
696
959
|
.catch((caught) => (0, client_1.getLogger)().error(caught, { at: 'liveSession.bindView' }));
|
|
697
960
|
}, []);
|
|
961
|
+
/**
|
|
962
|
+
* Sends now — or, for a signed-in shopper whose name has not loaded, holds it until the name lands
|
|
963
|
+
* (L6) and resolves once it has gone out. See `HeldComments`.
|
|
964
|
+
*/
|
|
698
965
|
const sendComment = (0, react_1.useCallback)(async (text, options) => {
|
|
699
|
-
var _a;
|
|
700
966
|
if (!text.trim())
|
|
701
967
|
return;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
dispatch({ kind: 'ownComment', comment: own });
|
|
708
|
-
}
|
|
709
|
-
catch (caught) {
|
|
710
|
-
(0, client_1.getLogger)().error(caught, { at: 'liveSession.sendComment' });
|
|
711
|
-
}
|
|
712
|
-
}, []);
|
|
968
|
+
const held = heldCommentsRef.current;
|
|
969
|
+
if (nameResolvingRef.current && held)
|
|
970
|
+
return held.hold({ text, options });
|
|
971
|
+
await deliverComment(text, options);
|
|
972
|
+
}, [deliverComment]);
|
|
713
973
|
const sendReaction = (0, react_1.useCallback)(async (reactionUrl) => {
|
|
714
974
|
try {
|
|
715
975
|
await liveSelling_1.liveSelling.engine.sendReaction(reactionUrl);
|
|
@@ -820,10 +1080,22 @@ function useSessionValue(props) {
|
|
|
820
1080
|
.bindGuestHostView(reactTag)
|
|
821
1081
|
.catch((caught) => (0, client_1.getLogger)().error(caught, { at: 'liveSession.bindGuestHostView' }));
|
|
822
1082
|
}, []);
|
|
1083
|
+
/**
|
|
1084
|
+
* Saves the name and gives it to the engine NOW, not in the effect (L6).
|
|
1085
|
+
*
|
|
1086
|
+
* The caller's next line is the comment that prompted for the name. The effect that pushes the name
|
|
1087
|
+
* belongs to this provider, a parent of that caller, and React runs parents' effects after
|
|
1088
|
+
* children's — so the comment went out under the join-time name, usually "Anonymous", into the
|
|
1089
|
+
* room and the stored history. Freckled Poppy set the engine's name itself to get round it. A name
|
|
1090
|
+
* passed as the `viewerName` prop still wins, as it does everywhere else.
|
|
1091
|
+
*/
|
|
823
1092
|
const chooseViewerName = (0, react_1.useCallback)(async (raw) => {
|
|
1093
|
+
var _a;
|
|
824
1094
|
const saved = await liveSelling_1.liveSelling.viewer.saveName(raw);
|
|
825
|
-
if (saved)
|
|
1095
|
+
if (saved) {
|
|
1096
|
+
liveSelling_1.liveSelling.engine.setViewerName((_a = accountNameRef.current) !== null && _a !== void 0 ? _a : saved);
|
|
826
1097
|
setChosenName(saved);
|
|
1098
|
+
}
|
|
827
1099
|
return saved;
|
|
828
1100
|
}, []);
|
|
829
1101
|
/** The speaker button, and nothing else. See `setOutputMuted` below for the other half. */
|
|
@@ -922,38 +1194,51 @@ function useSessionValue(props) {
|
|
|
922
1194
|
* into a cart. It is also what comes back on the host's broadcast, so `isLeading` compares like
|
|
923
1195
|
* with like. Null when signed out, which is exactly when bidding is not offered.
|
|
924
1196
|
*/
|
|
925
|
-
const bidderEmail = (
|
|
1197
|
+
const bidderEmail = (_t = (_s = identity === null || identity === void 0 ? void 0 : identity.customerEmail) === null || _s === void 0 ? void 0 : _s.call(identity)) !== null && _t !== void 0 ? _t : null;
|
|
926
1198
|
const bidder = (0, react_1.useMemo)(() => (bidderId ? { userId: bidderId, userName: viewerName, email: bidderEmail } : null), [bidderId, viewerName, bidderEmail]);
|
|
1199
|
+
/**
|
|
1200
|
+
* The hammer, with whether it is this shopper's (0.9.0): they led the round as it closed and the
|
|
1201
|
+
* winning bid is theirs (`isYourWin`). Its own memo, so it is a new object only when the win, the
|
|
1202
|
+
* round's last leader or the bidder change, never on a comment.
|
|
1203
|
+
*/
|
|
1204
|
+
const lastWin = (0, react_1.useMemo)(() => state.lastWin
|
|
1205
|
+
? { ...state.lastWin, isYou: (0, auctionRules_1.isYourWin)(state.lastWin, state.leaderWhenRoundClosed, bidderId) }
|
|
1206
|
+
: null, [state.lastWin, state.leaderWhenRoundClosed, bidderId]);
|
|
927
1207
|
const auction = (0, react_1.useMemo)(() => {
|
|
928
1208
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
929
|
-
// Falls back to the lot's opening
|
|
930
|
-
// and a slide-to-bid showing zero is worse than useless.
|
|
931
|
-
const nextBid = state.nextBid || (
|
|
1209
|
+
// Falls back to the lot's opening bid, else its bid step, else the show's (`openingBidFor`): until
|
|
1210
|
+
// the first bid lands the host sends no next bid, and a slide-to-bid showing zero is worse than useless.
|
|
1211
|
+
const nextBid = state.nextBid || (0, auctionRules_1.openingBidFor)(biddingLot, stream);
|
|
932
1212
|
return {
|
|
933
1213
|
biddingOpen: state.biddingOpen,
|
|
934
1214
|
highestBid: state.highestBid,
|
|
935
1215
|
nextBid,
|
|
936
|
-
|
|
1216
|
+
// A moment, not a countdown: it changes only when the host moves the deadline, so the session
|
|
1217
|
+
// never ticks. A component counts down to it itself (`useAuctionCountdown`).
|
|
1218
|
+
closesAt: (0, session_1.roundClosesAt)(state),
|
|
937
1219
|
isLeading: !!bidder && ((_a = state.bidLeader) === null || _a === void 0 ? void 0 : _a.userId) === bidder.userId,
|
|
938
1220
|
leaderName: (_c = (_b = state.bidLeader) === null || _b === void 0 ? void 0 : _b.userName) !== null && _c !== void 0 ? _c : null,
|
|
939
1221
|
extended: state.auctionExtended,
|
|
940
1222
|
storeProductId: (_e = (_d = state.biddingStoreProductId) !== null && _d !== void 0 ? _d : biddingLot === null || biddingLot === void 0 ? void 0 : biddingLot.storeProductId) !== null && _e !== void 0 ? _e : null,
|
|
941
|
-
lastWin
|
|
1223
|
+
lastWin,
|
|
942
1224
|
bidCap: (_f = stream === null || stream === void 0 ? void 0 : stream.bidCap) !== null && _f !== void 0 ? _f : null,
|
|
943
1225
|
extendedBiddingMs: (_g = stream === null || stream === void 0 ? void 0 : stream.extendedBiddingMs) !== null && _g !== void 0 ? _g : 0,
|
|
1226
|
+
wonLotIds: state.wonLotIds,
|
|
944
1227
|
};
|
|
945
|
-
}, [state,
|
|
1228
|
+
}, [state, biddingLot, bidder, stream, lastWin]);
|
|
946
1229
|
const giveaway = (0, react_1.useMemo)(() => {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1230
|
+
const drawn = state.giveawayResult;
|
|
1231
|
+
/**
|
|
1232
|
+
* The draw, with this shopper's part in it (0.9.0): whether they had entered (kept past the draw,
|
|
1233
|
+
* as `registered` goes when it lands) and whether they won: by the customer id the record names on
|
|
1234
|
+
* this draw's giveaway when a read has it, else by name among those who entered.
|
|
1235
|
+
*/
|
|
1236
|
+
const youEntered = !!drawn && !!bidderId && drawn.enteredBy === bidderId;
|
|
1237
|
+
const winnerId = drawn && stream ? (0, giveawayRoster_1.winnerIdFromRecord)(stream.giveaways, drawn.storeProductId, drawn.startedAtMs) : null;
|
|
954
1238
|
return {
|
|
955
|
-
|
|
956
|
-
|
|
1239
|
+
// The open giveaway for the lot, not an earlier one of the same product (`giveawayForLot`).
|
|
1240
|
+
active: state.giveawayStoreProductId && stream
|
|
1241
|
+
? (0, giveawayRoster_1.giveawayForLot)(stream.giveaways, state.giveawayStoreProductId)
|
|
957
1242
|
: null,
|
|
958
1243
|
// Inflated by the host's multiplier, exactly as `viewerCount` and poll totals are. State keeps
|
|
959
1244
|
// the raw count; only the read out of the session multiplies.
|
|
@@ -964,8 +1249,21 @@ function useSessionValue(props) {
|
|
|
964
1249
|
* still active and a re-render to fire.
|
|
965
1250
|
*/
|
|
966
1251
|
registered: !!bidderId && state.giveawayRegisteredBy === bidderId,
|
|
967
|
-
|
|
968
|
-
|
|
1252
|
+
/* The roster is held in the reducer once known, so a later read cannot change it mid-reveal. */
|
|
1253
|
+
result: drawn
|
|
1254
|
+
? {
|
|
1255
|
+
winnerName: drawn.winnerName,
|
|
1256
|
+
entrantNames: drawn.entrantNames,
|
|
1257
|
+
entrantNamesReady: drawn.entrantNamesReady,
|
|
1258
|
+
youEntered,
|
|
1259
|
+
isYou: (0, giveawayRoster_1.isYourGiveawayWin)({
|
|
1260
|
+
winnerId,
|
|
1261
|
+
winnerName: drawn.winnerName,
|
|
1262
|
+
youEntered,
|
|
1263
|
+
bidderId,
|
|
1264
|
+
viewerName: namedBy,
|
|
1265
|
+
}),
|
|
1266
|
+
}
|
|
969
1267
|
: null,
|
|
970
1268
|
};
|
|
971
1269
|
}, [
|
|
@@ -976,6 +1274,7 @@ function useSessionValue(props) {
|
|
|
976
1274
|
state.giveawayResult,
|
|
977
1275
|
stream,
|
|
978
1276
|
multiplier,
|
|
1277
|
+
namedBy,
|
|
979
1278
|
]);
|
|
980
1279
|
/**
|
|
981
1280
|
* The round, the stream and the bidder, for the actions to read when they fire.
|
|
@@ -1012,11 +1311,13 @@ function useSessionValue(props) {
|
|
|
1012
1311
|
const round = roundRef.current;
|
|
1013
1312
|
const current = streamRef.current;
|
|
1014
1313
|
const who = bidderRef.current;
|
|
1015
|
-
if (!current
|
|
1016
|
-
return;
|
|
1314
|
+
if (!current)
|
|
1315
|
+
return { sent: false, reason: 'noShow' };
|
|
1316
|
+
if (!who)
|
|
1317
|
+
return { sent: false, reason: 'signedOut' };
|
|
1017
1318
|
const amount = (_a = options === null || options === void 0 ? void 0 : options.amount) !== null && _a !== void 0 ? _a : round.auction.nextBid;
|
|
1018
1319
|
if (!amount || amount <= 0)
|
|
1019
|
-
return;
|
|
1320
|
+
return { sent: false, reason: 'noAmount' };
|
|
1020
1321
|
/**
|
|
1021
1322
|
* The lot's only variant, for a bid that names none.
|
|
1022
1323
|
*
|
|
@@ -1033,6 +1334,20 @@ function useSessionValue(props) {
|
|
|
1033
1334
|
*/
|
|
1034
1335
|
const lot = productsRef.current.find((product) => product.storeProductId === round.auction.storeProductId);
|
|
1035
1336
|
const soleVariantId = (lot === null || lot === void 0 ? void 0 : lot.variants.length) === 1 ? ((_c = (_b = lot.variants[0]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : null) : null;
|
|
1337
|
+
/**
|
|
1338
|
+
* No moderator, no bid (L3). Bids go to the moderator in charge and to nobody else, and a viewer
|
|
1339
|
+
* who missed the round's opening may not know one — see `lastModeratorId`. Said rather than
|
|
1340
|
+
* swallowed, and the ledger is left alone: before 0.9.0 the REST record still went out, for a bid
|
|
1341
|
+
* that never entered the round.
|
|
1342
|
+
*/
|
|
1343
|
+
if (!round.moderatorId) {
|
|
1344
|
+
(0, client_1.logWarning)(new Error('No moderator is known for this round, so the bid was not sent'), {
|
|
1345
|
+
at: 'liveSession.placeBid',
|
|
1346
|
+
amount,
|
|
1347
|
+
});
|
|
1348
|
+
return { sent: false, reason: 'noModerator' };
|
|
1349
|
+
}
|
|
1350
|
+
let result = { sent: true };
|
|
1036
1351
|
try {
|
|
1037
1352
|
await liveSelling_1.liveSelling.engine.sendBid({
|
|
1038
1353
|
bidAmount: amount,
|
|
@@ -1046,6 +1361,7 @@ function useSessionValue(props) {
|
|
|
1046
1361
|
}
|
|
1047
1362
|
catch (caught) {
|
|
1048
1363
|
(0, client_1.getLogger)().error(caught, { at: 'liveSession.placeBid', amount });
|
|
1364
|
+
result = { sent: false, reason: 'sendFailed' };
|
|
1049
1365
|
}
|
|
1050
1366
|
try {
|
|
1051
1367
|
await liveSelling_1.liveSelling.auctions.recordBid({
|
|
@@ -1059,14 +1375,17 @@ function useSessionValue(props) {
|
|
|
1059
1375
|
catch (caught) {
|
|
1060
1376
|
(0, client_1.getLogger)().error(caught, { at: 'liveSession.recordBid', amount });
|
|
1061
1377
|
}
|
|
1378
|
+
return result;
|
|
1062
1379
|
}, []);
|
|
1063
1380
|
const enterGiveaway = (0, react_1.useCallback)(async (cartId) => {
|
|
1064
1381
|
var _a, _b, _c;
|
|
1065
1382
|
const lot = giveawayLotRef.current;
|
|
1066
1383
|
const current = streamRef.current;
|
|
1067
1384
|
const customer = customerRefRef.current;
|
|
1068
|
-
if (!current || !lot
|
|
1069
|
-
return;
|
|
1385
|
+
if (!current || !lot)
|
|
1386
|
+
return { entered: false, reason: 'noGiveaway' };
|
|
1387
|
+
if (!customer)
|
|
1388
|
+
return { entered: false, reason: 'signedOut' };
|
|
1070
1389
|
try {
|
|
1071
1390
|
const viewerId = (_a = viewerIdRef.current) !== null && _a !== void 0 ? _a : (await liveSelling_1.liveSelling.viewer.id());
|
|
1072
1391
|
const entered = await liveSelling_1.liveSelling.auctions.enterGiveaway({
|
|
@@ -1079,11 +1398,14 @@ function useSessionValue(props) {
|
|
|
1079
1398
|
});
|
|
1080
1399
|
// Only on the host's confirmation. The entry count follows from its own push, so nothing here
|
|
1081
1400
|
// guesses at it.
|
|
1082
|
-
if (entered)
|
|
1083
|
-
|
|
1401
|
+
if (!entered)
|
|
1402
|
+
return { entered: false, reason: 'refused' };
|
|
1403
|
+
dispatch({ kind: 'giveawayRegistered', bidderId: (_c = (_b = bidderRef.current) === null || _b === void 0 ? void 0 : _b.userId) !== null && _c !== void 0 ? _c : null });
|
|
1404
|
+
return { entered: true };
|
|
1084
1405
|
}
|
|
1085
1406
|
catch (caught) {
|
|
1086
1407
|
(0, client_1.getLogger)().error(caught, { at: 'liveSession.enterGiveaway' });
|
|
1408
|
+
return { entered: false, reason: 'failed' };
|
|
1087
1409
|
}
|
|
1088
1410
|
}, []);
|
|
1089
1411
|
const dismissAuctionWin = (0, react_1.useCallback)(() => dispatch({ kind: 'dismissWin' }), []);
|
|
@@ -1140,10 +1462,35 @@ function useSessionValue(props) {
|
|
|
1140
1462
|
dismissAuctionWin,
|
|
1141
1463
|
dismissGiveawayResult,
|
|
1142
1464
|
]);
|
|
1143
|
-
|
|
1465
|
+
const guest = (0, react_1.useMemo)(() => ({
|
|
1466
|
+
mode: state.guestMode,
|
|
1467
|
+
status: state.guestStatus,
|
|
1468
|
+
slot: state.guestSlot,
|
|
1469
|
+
mutedByHost: state.guestMutedByHost,
|
|
1470
|
+
cameraOffByHost: state.guestCameraOffByHost,
|
|
1471
|
+
mixerLayout: state.mixerLayout,
|
|
1472
|
+
}), [
|
|
1473
|
+
state.guestMode,
|
|
1474
|
+
state.guestStatus,
|
|
1475
|
+
state.guestSlot,
|
|
1476
|
+
state.guestMutedByHost,
|
|
1477
|
+
state.guestCameraOffByHost,
|
|
1478
|
+
state.mixerLayout,
|
|
1479
|
+
]);
|
|
1480
|
+
const viewerCount = Math.round(state.rawViewerCount * multiplier);
|
|
1481
|
+
const pinnedComment = status === 'ended' ? null : state.pinnedComment;
|
|
1482
|
+
/**
|
|
1483
|
+
* The same object until one of its fields changes.
|
|
1484
|
+
*
|
|
1485
|
+
* Every component that reads the session is drawn again when this object changes, and the session is
|
|
1486
|
+
* app-wide. Built fresh on every render, it changed whenever the provider was drawn for any reason —
|
|
1487
|
+
* its parent passing the same props again, a record read that changed nothing a reader sees — and
|
|
1488
|
+
* drew every reader with it. Field by field, so a new object means something in it is new.
|
|
1489
|
+
*/
|
|
1490
|
+
return (0, react_1.useMemo)(() => ({
|
|
1144
1491
|
status,
|
|
1145
1492
|
stream: stream !== null && stream !== void 0 ? stream : null,
|
|
1146
|
-
viewerCount
|
|
1493
|
+
viewerCount,
|
|
1147
1494
|
rawViewerCount: state.rawViewerCount,
|
|
1148
1495
|
reactionStyle,
|
|
1149
1496
|
comments: state.comments,
|
|
@@ -1154,23 +1501,39 @@ function useSessionValue(props) {
|
|
|
1154
1501
|
reactions: state.reactions,
|
|
1155
1502
|
poll,
|
|
1156
1503
|
viewerName: namedBy,
|
|
1157
|
-
guest
|
|
1158
|
-
mode: state.guestMode,
|
|
1159
|
-
status: state.guestStatus,
|
|
1160
|
-
slot: state.guestSlot,
|
|
1161
|
-
mutedByHost: state.guestMutedByHost,
|
|
1162
|
-
cameraOffByHost: state.guestCameraOffByHost,
|
|
1163
|
-
mixerLayout: state.mixerLayout,
|
|
1164
|
-
},
|
|
1504
|
+
guest,
|
|
1165
1505
|
muted,
|
|
1166
1506
|
videoTag: state.videoTag,
|
|
1167
|
-
elapsedMs,
|
|
1168
1507
|
pipMode,
|
|
1169
1508
|
auction,
|
|
1170
1509
|
giveaway,
|
|
1510
|
+
pinnedComment,
|
|
1171
1511
|
error,
|
|
1172
1512
|
actions,
|
|
1173
|
-
}
|
|
1513
|
+
}), [
|
|
1514
|
+
status,
|
|
1515
|
+
stream,
|
|
1516
|
+
viewerCount,
|
|
1517
|
+
state.rawViewerCount,
|
|
1518
|
+
reactionStyle,
|
|
1519
|
+
state.comments,
|
|
1520
|
+
activeProduct,
|
|
1521
|
+
products,
|
|
1522
|
+
state.sold,
|
|
1523
|
+
state.cartAnnouncements,
|
|
1524
|
+
state.reactions,
|
|
1525
|
+
poll,
|
|
1526
|
+
namedBy,
|
|
1527
|
+
guest,
|
|
1528
|
+
muted,
|
|
1529
|
+
state.videoTag,
|
|
1530
|
+
pipMode,
|
|
1531
|
+
auction,
|
|
1532
|
+
giveaway,
|
|
1533
|
+
pinnedComment,
|
|
1534
|
+
error,
|
|
1535
|
+
actions,
|
|
1536
|
+
]);
|
|
1174
1537
|
}
|
|
1175
1538
|
/**
|
|
1176
1539
|
* Holds one live session for the whole app. Nothing is joined while no stream is live, so the engine
|
|
@@ -1198,18 +1561,20 @@ function LiveSessionProvider({ children, ...props }) {
|
|
|
1198
1561
|
the video — see the note on `actions` in `useSessionValue`. */
|
|
1199
1562
|
const watchedRef = (0, react_1.useRef)(undefined);
|
|
1200
1563
|
watchedRef.current = (_d = (_c = props.streamingId) !== null && _c !== void 0 ? _c : heldId) !== null && _d !== void 0 ? _d : (_e = session.stream) === null || _e === void 0 ? void 0 : _e.streamingId;
|
|
1201
|
-
const holdStream = (0, react_1.useCallback)(() => {
|
|
1202
|
-
/*
|
|
1203
|
-
the
|
|
1204
|
-
|
|
1564
|
+
const holdStream = (0, react_1.useCallback)((streamingId) => {
|
|
1565
|
+
/* A named show wins over whatever is on screen — see `holdStream` in `LiveSessionActions`. With
|
|
1566
|
+
none named and nothing to pin yet, the caller retries when a stream id arrives, so a player
|
|
1567
|
+
opened before the read resolved is not left following whatever turns up next. */
|
|
1568
|
+
const id = streamingId || watchedRef.current;
|
|
1205
1569
|
if (id)
|
|
1206
1570
|
setHeldId(id);
|
|
1207
1571
|
}, []);
|
|
1208
1572
|
const releaseStream = (0, react_1.useCallback)(() => setHeldId(null), []);
|
|
1209
|
-
/* Two memos, and the split matters: `session`
|
|
1210
|
-
|
|
1573
|
+
/* Two memos, and the split matters: `session` changes with every event, so folding the actions into
|
|
1574
|
+
the same one would rebuild `actions` on every event too — the exact thing the note in
|
|
1211
1575
|
`useSessionValue` warns about, and it live-locks a player that calls `holdStream` from an effect
|
|
1212
|
-
keyed on `actions`. `session.actions` is stable, so keyed on that this one is as well.
|
|
1576
|
+
keyed on `actions`. `session.actions` is stable, so keyed on that this one is as well. `session`
|
|
1577
|
+
is the same object while nothing in it changed, so `value` is too: no reader is drawn for nothing. */
|
|
1213
1578
|
const actions = (0, react_1.useMemo)(() => ({ ...session.actions, holdStream, releaseStream }), [session.actions, holdStream, releaseStream]);
|
|
1214
1579
|
const value = (0, react_1.useMemo)(() => ({ ...session, actions }), [session, actions]);
|
|
1215
1580
|
return (0, react_1.createElement)(LiveSessionContext.Provider, { value }, children);
|