@applicaster/zapp-react-native-ui-components 15.1.16-rc.0 → 15.1.16

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.
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import { StyleSheet, Text, View } from "react-native";
3
3
  import {
4
4
  AudioPlayerTrackItem,
@@ -7,8 +7,9 @@ import {
7
7
  } from "./Item";
8
8
  import { AudioPlayerHeader, VARIANT_SECTION_HEADER } from "./Header";
9
9
  import type { MenuItem } from "@applicaster/zapp-react-native-utils/modalState/types";
10
- import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
11
- import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
10
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
11
+ import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
12
+ import type { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
12
13
 
13
14
  const DEFAULT_EMPTY_MESSAGE = "Queued items will be added here";
14
15
 
@@ -30,6 +31,27 @@ const styles = StyleSheet.create({
30
31
  },
31
32
  });
32
33
 
34
+ const isPrimaryOrCastingPlayer = (player?: Player | null): boolean => {
35
+ if (!player) {
36
+ return false;
37
+ }
38
+
39
+ return (
40
+ player.playerRole === PlayerRole.Primary ||
41
+ player.playerRole === PlayerRole.Chromecast
42
+ );
43
+ };
44
+
45
+ const getTargetPlayer = (): Player | null => {
46
+ if (playerManager.isCasting()) {
47
+ return playerManager.getCastingReceiver() || null;
48
+ }
49
+
50
+ const active = playerManager.getActivePlayer();
51
+
52
+ return isPrimaryOrCastingPlayer(active) ? active : null;
53
+ };
54
+
33
55
  type NowPlayingHeaderSectionProps = {
34
56
  onClear?: () => void;
35
57
  nextUpTitle?: string;
@@ -47,18 +69,100 @@ export function NowPlayingHeaderSection({
47
69
  emptyMessage = DEFAULT_EMPTY_MESSAGE,
48
70
  clearLabel,
49
71
  }: NowPlayingHeaderSectionProps) {
50
- const player = usePlayer();
51
- const playerState = usePlayerState("queue-now-playing-section");
72
+ const [activePlayer, setActivePlayer] = useState<Player | null>(
73
+ getTargetPlayer
74
+ );
75
+
76
+ const [currentEntry, setCurrentEntry] = useState<ZappEntry | null>(
77
+ () => getTargetPlayer()?.entry || null
78
+ );
79
+
80
+ const [isPaused, setIsPaused] = useState<boolean>(() =>
81
+ Boolean(getTargetPlayer()?.isPaused())
82
+ );
83
+
84
+ useEffect(() => {
85
+ const handleActivePlayerUpdate = () => {
86
+ const player = getTargetPlayer();
87
+
88
+ if (player) {
89
+ setActivePlayer(player);
90
+
91
+ if (player.entry) {
92
+ setCurrentEntry(player.entry);
93
+ }
94
+
95
+ setIsPaused(Boolean(player.isPaused()));
96
+ }
97
+ };
98
+
99
+ handleActivePlayerUpdate();
100
+
101
+ const removeLifecycle = playerManager.addLifecycleListener({
102
+ onRegistered: (registeredPlayer) => {
103
+ if (isPrimaryOrCastingPlayer(registeredPlayer)) {
104
+ handleActivePlayerUpdate();
105
+ }
106
+ },
107
+ onCastingChanged: handleActivePlayerUpdate,
108
+ });
109
+
110
+ return () => {
111
+ removeLifecycle?.();
112
+ };
113
+ }, []);
114
+
115
+ useEffect(() => {
116
+ if (!activePlayer) {
117
+ return;
118
+ }
119
+
120
+ const syncPlayerState = () => {
121
+ if (activePlayer.entry) {
122
+ setCurrentEntry(activePlayer.entry);
123
+ }
124
+
125
+ setIsPaused(Boolean(activePlayer.isPaused()));
126
+ };
127
+
128
+ syncPlayerState();
129
+
130
+ const removeListener = activePlayer.addListener({
131
+ id: "queue-now-playing-header",
132
+ listener: {
133
+ onPlayerPause: () => setIsPaused(true),
134
+ onPlayerResume: () => setIsPaused(false),
135
+ onVideoLoad: syncPlayerState,
136
+ onLoad: syncPlayerState,
137
+ },
138
+ });
139
+
140
+ return () => {
141
+ removeListener?.();
142
+ };
143
+ }, [activePlayer]);
144
+
145
+ const handlePlayPause = () => {
146
+ if (!activePlayer) {
147
+ return;
148
+ }
149
+
150
+ if (isPaused) {
151
+ activePlayer.play();
152
+ setIsPaused(false);
153
+ } else {
154
+ activePlayer.pause();
155
+ setIsPaused(true);
156
+ }
157
+ };
52
158
 
53
159
  const renderNowPlayingTrack = () => {
54
- if (!player?.entry) {
160
+ if (!currentEntry) {
55
161
  return null;
56
162
  }
57
163
 
58
- const mediaGroup = player.entry.media_group;
59
-
60
- const imageGroup = Array.isArray(mediaGroup)
61
- ? mediaGroup.find((g: any) => g.type === "image")
164
+ const imageGroup = Array.isArray(currentEntry.media_group)
165
+ ? currentEntry.media_group.find((g: any) => g.type === "image")
62
166
  : null;
63
167
 
64
168
  const firstImage = Array.isArray(imageGroup?.media_item)
@@ -66,26 +170,18 @@ export function NowPlayingHeaderSection({
66
170
  : imageGroup?.media_item?.src;
67
171
 
68
172
  const nowPlayingItem: MenuItem = {
69
- id: String(player.entry.id ?? "now_playing"),
70
- title: (player.entry.title as string) || String(player.entry.id),
71
- summary: (player.entry.summary as string) ?? "",
173
+ id: String(currentEntry.id ?? "now_playing"),
174
+ title: (currentEntry.title as string) || String(currentEntry.id),
175
+ summary: (currentEntry.summary as string) ?? "",
72
176
  icon: firstImage,
73
177
  };
74
178
 
75
179
  const data = mapMenuItemToNowPlayingData(
76
180
  nowPlayingItem,
77
- playerState?.isPaused,
181
+ isPaused,
78
182
  nowPlayingTitle
79
183
  );
80
184
 
81
- const handlePlayPause = () => {
82
- if (playerState?.isPaused) {
83
- player?.play();
84
- } else {
85
- player?.pause();
86
- }
87
- };
88
-
89
185
  return (
90
186
  <AudioPlayerTrackItem
91
187
  configuration={VARIANT_NOW_PLAYING}
@@ -87,9 +87,7 @@ export const SortableList = ({
87
87
  const [localData, setLocalData] = useState(sortableData);
88
88
 
89
89
  useEffect(() => {
90
- setLocalData((localData) =>
91
- localData !== sortableData ? sortableData : localData
92
- );
90
+ setLocalData(sortableData);
93
91
  }, [sortableData]);
94
92
 
95
93
  const handleDragEnd = useCallback(
@@ -142,6 +140,7 @@ export const SortableList = ({
142
140
  >
143
141
  {renderHeader}
144
142
  <Sortable.Grid
143
+ key={`sortable-grid-${localData.length}`}
145
144
  scrollableRef={scrollableRef}
146
145
  customHandle
147
146
  columns={columns}
@@ -403,10 +403,17 @@ const PlayerContainerComponent = (props: Props) => {
403
403
  const { eventType } = event;
404
404
 
405
405
  if (!isLanguageOverlayVisible && eventType === "menu") {
406
+ // First Back dismisses Play Next, the next one closes the player
407
+ if (playNextOverlayState) {
408
+ playNextOverlayState.handleUserCancelPlayNext();
409
+
410
+ return;
411
+ }
412
+
406
413
  close();
407
414
  }
408
415
  },
409
- [close]
416
+ [close, playNextOverlayState]
410
417
  );
411
418
 
412
419
  // Effects
@@ -12,6 +12,12 @@ export const withTvEventHandler = (Component) => {
12
12
  const isStandaloneFullscreen = useIsStandaloneFullscreen();
13
13
 
14
14
  const remoteHandler = (event) => {
15
+ // A nested River (e.g. a ScreenPicker tab) shares the screen with its parent
16
+ // River, which already handles Back. Handling it here too would go back twice.
17
+ if (props.isInsideContainer) {
18
+ return;
19
+ }
20
+
15
21
  const { eventType } = event;
16
22
 
17
23
  const canGoBack = navigator.canGoBack();
@@ -129,8 +129,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
129
129
  * The last scene in the array is removed in the "backTransitionEnd" callback,
130
130
  * only after the animation is complete.
131
131
  */
132
- backTransitionStart() {
133
- const previousRoute = this.props.navigator?.currentRoute;
132
+ backTransitionStart(previousRoute: string) {
134
133
  const currentRoute = this.props.navigator?.currentRoute;
135
134
 
136
135
  const diffLengthRoutes =
@@ -242,7 +241,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
242
241
  break;
243
242
 
244
243
  case NAV_ACTION_BACK: {
245
- this.backTransitionStart();
244
+ this.backTransitionStart(previousRoute);
246
245
  break;
247
246
  }
248
247
 
@@ -1,8 +1,13 @@
1
1
  import React from "react";
2
2
  import { Text, View } from "react-native";
3
3
 
4
- import { TransitionerComponent } from "../Transitioner";
5
- import { render } from "@testing-library/react-native";
4
+ import { AnimationManager } from "../AnimationManager";
5
+ import {
6
+ NAV_ACTION_BACK,
7
+ NAV_ACTION_PUSH,
8
+ TransitionerComponent,
9
+ } from "../Transitioner";
10
+ import { act, render } from "@testing-library/react-native";
6
11
 
7
12
  jest.mock("../Scene", () => {
8
13
  const View = jest.requireActual("react-native").View;
@@ -40,6 +45,27 @@ const transitionerComponentProps = {
40
45
 
41
46
  jest.mock("uuid");
42
47
 
48
+ const sceneTransitionConfig = () => ({
49
+ push: {
50
+ duration: 0,
51
+ easing: jest.fn(),
52
+ from: { style: {} },
53
+ to: { style: {} },
54
+ },
55
+ back: {
56
+ duration: 0,
57
+ easing: jest.fn(),
58
+ from: { style: {} },
59
+ to: { style: {} },
60
+ },
61
+ });
62
+
63
+ const sceneFor = (route) => (
64
+ <View pathname={route}>
65
+ <Text>{route}</Text>
66
+ </View>
67
+ );
68
+
43
69
  describe("<TransitionerComponent />", () => {
44
70
  it("renders correctly", () => {
45
71
  const wrapper = render(
@@ -50,4 +76,59 @@ describe("<TransitionerComponent />", () => {
50
76
 
51
77
  expect(wrapper.toJSON()).toMatchSnapshot();
52
78
  });
79
+
80
+ it("removes both intermediate scenes when backing out to the tab root", () => {
81
+ jest
82
+ .spyOn(AnimationManager.prototype, "animate")
83
+ .mockImplementation((_, done) => done());
84
+
85
+ const ref = React.createRef();
86
+ const root = "/river/root";
87
+ const child = `${root}/river/child`;
88
+ const grandchild = `${child}/river/grandchild`;
89
+
90
+ const propsFor = (route, previousAction) => ({
91
+ navigator: {
92
+ previousAction,
93
+ currentRoute: route,
94
+ screenData: {},
95
+ },
96
+ transitionConfig: sceneTransitionConfig,
97
+ layoutData: {
98
+ isTablet: false,
99
+ isTabletPortrait: false,
100
+ },
101
+ children: sceneFor(route),
102
+ });
103
+
104
+ const { rerender } = render(
105
+ <TransitionerComponent ref={ref} {...propsFor(root, "REPLACE")} />
106
+ );
107
+
108
+ act(() => {
109
+ rerender(
110
+ <TransitionerComponent
111
+ ref={ref}
112
+ {...propsFor(child, NAV_ACTION_PUSH)}
113
+ />
114
+ );
115
+ });
116
+
117
+ act(() => {
118
+ rerender(
119
+ <TransitionerComponent
120
+ ref={ref}
121
+ {...propsFor(grandchild, NAV_ACTION_PUSH)}
122
+ />
123
+ );
124
+ });
125
+
126
+ act(() => {
127
+ rerender(
128
+ <TransitionerComponent ref={ref} {...propsFor(root, NAV_ACTION_BACK)} />
129
+ );
130
+ });
131
+
132
+ expect(ref.current.state.scenes.map((scene) => scene.key)).toEqual([root]);
133
+ });
53
134
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.1.16-rc.0",
3
+ "version": "15.1.16",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.1.16-rc.0",
32
- "@applicaster/zapp-react-native-bridge": "15.1.16-rc.0",
33
- "@applicaster/zapp-react-native-redux": "15.1.16-rc.0",
34
- "@applicaster/zapp-react-native-utils": "15.1.16-rc.0",
31
+ "@applicaster/applicaster-types": "15.1.16",
32
+ "@applicaster/zapp-react-native-bridge": "15.1.16",
33
+ "@applicaster/zapp-react-native-redux": "15.1.16",
34
+ "@applicaster/zapp-react-native-utils": "15.1.16",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "react-native-sortables": "1.7.1",