@applicaster/zapp-react-native-ui-components 13.0.0-alpha.1199723621 → 13.0.0-alpha.1240837114

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.
Files changed (52) hide show
  1. package/Components/AudioPlayer/AudioPlayer.tsx +7 -0
  2. package/Components/AudioPlayer/helpers.tsx +1 -0
  3. package/Components/Cell/Cell.tsx +3 -13
  4. package/Components/Cell/CellWithFocusable.tsx +18 -14
  5. package/Components/Cell/CellWrapper.ts +5 -0
  6. package/Components/Cell/styles.ts +17 -0
  7. package/Components/ErrorScreen/index.tsx +8 -0
  8. package/Components/FocusableCell/index.tsx +1 -1
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +3 -6
  10. package/Components/FocusableGroup/index.tsx +0 -1
  11. package/Components/HandlePlayable/HandlePlayable.tsx +14 -27
  12. package/Components/Layout/TV/ScreenContainer.tsx +8 -1
  13. package/Components/Layout/TV/ScreenLayoutContextProvider.tsx +5 -0
  14. package/Components/Layout/TV/__tests__/ScreenContainer.test.tsx +2 -1
  15. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +4 -3
  16. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +11 -7
  17. package/Components/MasterCell/utils/behaviorProvider.ts +136 -0
  18. package/Components/MasterCell/utils/index.ts +8 -134
  19. package/Components/ModalComponent/Button/assets.ts +1 -1
  20. package/Components/OfflineHandler/utils/index.ts +1 -1
  21. package/Components/PlayerContainer/PlayerContainer.tsx +9 -5
  22. package/Components/PlayerImageBackground/index.tsx +4 -24
  23. package/Components/River/ComponentsMap/ComponentsMap.tsx +3 -0
  24. package/Components/River/ComponentsMap/hooks/useLoadingState.ts +3 -3
  25. package/Components/Screen/TV/index.web.tsx +5 -6
  26. package/Components/Screen/hooks.ts +3 -6
  27. package/Components/TrackedView/index.tsx +1 -0
  28. package/Components/Transitioner/AnimationManager.js +15 -15
  29. package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -0
  30. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  31. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +1 -1
  32. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +33 -19
  33. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +3 -3
  34. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +17 -4
  35. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +2 -2
  36. package/Components/VideoModal/VideoModal.tsx +2 -2
  37. package/Components/ZappUIComponent/index.tsx +4 -4
  38. package/Contexts/FocusableGroupContext/withFocusableContext.tsx +4 -10
  39. package/Contexts/HeaderOffsetContext/index.tsx +4 -6
  40. package/Contexts/ScreenContext/index.tsx +3 -10
  41. package/Contexts/ScreenLayoutContext/index.tsx +5 -3
  42. package/Decorators/RiverResolver/index.tsx +5 -7
  43. package/Decorators/ZappPipesDataConnector/index.tsx +4 -30
  44. package/package.json +5 -9
  45. package/tsconfig.json +2 -3
  46. package/.babelrc +0 -8
  47. package/Components/Cell/CellStyles/FallbackCellStyle/index.js +0 -157
  48. package/Components/Cell/CellStyles/Hero/index.js +0 -111
  49. package/Components/Cell/CellStyles/ScreenSelector/index.js +0 -68
  50. package/Components/Cell/CellStyles/cellStylesResolver.ts +0 -19
  51. package/Components/Cell/CellStyles/colors.js +0 -40
  52. package/Components/Cell/CellStyles/index.js +0 -15
@@ -24,6 +24,7 @@ type Props = {
24
24
  audio_player_rtl?: boolean;
25
25
  magic_background?: boolean;
26
26
  audio_player_background_image_query?: string;
27
+ audio_player_background_image_default_color?: string;
27
28
  start_time?: string;
28
29
  end_time?: string;
29
30
  };
@@ -35,6 +36,7 @@ type Props = {
35
36
  audio_player_rtl?: string;
36
37
  magic_background?: string;
37
38
  audio_player_background_image_query?: string;
39
+ audio_player_background_image_default_color?: string;
38
40
  audio_player_background_image?: string;
39
41
  audio_player_artwork_aspect_ratio?: string;
40
42
  lg_tv_audio_player_title_font_family?: string;
@@ -80,6 +82,10 @@ export function AudioPlayer(props: Props) {
80
82
  "audio_player_background_image_query"
81
83
  );
82
84
 
85
+ const audioPlayerBackgroundImageDefaultColor = getProp(
86
+ "audio_player_background_image_default_color"
87
+ );
88
+
83
89
  const isRTL = rtlFlag === "1" || rtlFlag === "true" || rtlFlag === true;
84
90
 
85
91
  const titleFontFamily = getProp(
@@ -158,6 +164,7 @@ export function AudioPlayer(props: Props) {
158
164
  channelIcon,
159
165
  magicBackground,
160
166
  audioPlayerBackgroundImageQuery,
167
+ audioPlayerBackgroundImageDefaultColor,
161
168
  };
162
169
  }, [getProp]);
163
170
 
@@ -6,6 +6,7 @@ const defaults = {
6
6
  audio_player_rtl: false,
7
7
  magic_background: false,
8
8
  audio_player_background_image_query: "",
9
+ audio_player_background_image_default_color: "",
9
10
  };
10
11
 
11
12
  export function getPropertyFromEntryOrConfig({ entry, plugin_configuration }) {
@@ -12,6 +12,7 @@ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
12
12
  import { CellWithFocusable } from "./CellWithFocusable";
13
13
  import { BaseFocusable } from "../BaseFocusable";
14
14
  import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
15
+ import { styles } from "./styles";
15
16
 
16
17
  type Props = {
17
18
  item: ZappEntry;
@@ -68,17 +69,6 @@ type State = {
68
69
  hasFocusableInside: boolean;
69
70
  };
70
71
 
71
- const baseCellStyles = {
72
- height: "100%",
73
- display: "flex",
74
- flex: 1,
75
- } as const;
76
-
77
- const touchableStyles = {
78
- flex: 1,
79
- height: "100%",
80
- } as const;
81
-
82
72
  export class CellComponent extends React.Component<Props, State> {
83
73
  constructor(props) {
84
74
  super(props);
@@ -252,7 +242,7 @@ export class CellComponent extends React.Component<Props, State> {
252
242
  <View
253
243
  testID={`${component?.id}-${id}`}
254
244
  accessible={false}
255
- style={touchableStyles}
245
+ style={styles.touchableCell}
256
246
  >
257
247
  <Focusable
258
248
  id={focusableId}
@@ -264,7 +254,7 @@ export class CellComponent extends React.Component<Props, State> {
264
254
  hasReceivedFocus={hasReceivedFocus || this.hasReceivedFocus}
265
255
  preferredFocus={preferredFocus}
266
256
  offsetUpdater={offsetUpdater}
267
- style={baseCellStyles}
257
+ style={styles.baseCell}
268
258
  isFocusable={isFocusable}
269
259
  skipFocusManagerRegistration={skipFocusManagerRegistration}
270
260
  >
@@ -5,6 +5,8 @@ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/
5
5
 
6
6
  import { useCellState } from "../MasterCell/utils";
7
7
  import { FocusableGroup } from "../FocusableGroup";
8
+ import { CellWrapper } from "./CellWrapper";
9
+ import { styles } from "./styles";
8
10
 
9
11
  type Props = {
10
12
  item: ZappEntry;
@@ -40,7 +42,7 @@ export function CellWithFocusable(props: Props) {
40
42
  const [isFocused, setIsFocused] = React.useState(false);
41
43
 
42
44
  const state = useCellState({
43
- id: item.id,
45
+ item,
44
46
  behavior,
45
47
  focused: isFocused || toBooleanWithDefaultFalse(focused),
46
48
  });
@@ -88,19 +90,21 @@ export function CellWithFocusable(props: Props) {
88
90
  onBlur={onGroupBlur}
89
91
  skipFocusManagerRegistration={skipFocusManagerRegistration}
90
92
  >
91
- <CellRenderer
92
- testID={"cell-with-focusable-cell-renderer"}
93
- item={item}
94
- groupId={`focusable-cell-wrapper-${id}`}
95
- onToggleFocus={handleToggleFocus}
96
- state={state}
97
- prefixId={id}
98
- focusedButtonId={focusedButtonId}
99
- preferredFocus={true}
100
- skipFocusManagerRegistration={skipFocusManagerRegistration}
101
- isFocusable={isFocusable}
102
- focused={focused}
103
- />
93
+ <CellWrapper style={styles.cellWrapper}>
94
+ <CellRenderer
95
+ testID={"cell-with-focusable-cell-renderer"}
96
+ item={item}
97
+ groupId={`focusable-cell-wrapper-${id}`}
98
+ onToggleFocus={handleToggleFocus}
99
+ state={state}
100
+ prefixId={id}
101
+ focusedButtonId={focusedButtonId}
102
+ preferredFocus={true}
103
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
104
+ isFocusable={isFocusable}
105
+ focused={focused}
106
+ />
107
+ </CellWrapper>
104
108
  </FocusableGroup>
105
109
  );
106
110
  }
@@ -0,0 +1,5 @@
1
+ import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
2
+ import React from "react";
3
+ import { View } from "react-native";
4
+
5
+ export const CellWrapper = isTvOSPlatform() ? View : React.Fragment;
@@ -0,0 +1,17 @@
1
+ import { StyleSheet } from "react-native";
2
+
3
+ export const styles = StyleSheet.create({
4
+ cellWrapper: {
5
+ flex: 0,
6
+ height: "100%",
7
+ },
8
+ baseCell: {
9
+ height: "100%",
10
+ display: "flex",
11
+ flex: 1,
12
+ },
13
+ touchableCell: {
14
+ flex: 1,
15
+ height: "100%",
16
+ },
17
+ });
@@ -105,6 +105,11 @@ const defaultProps = {
105
105
  alignItems: "center",
106
106
  justifyContent: "center",
107
107
  },
108
+ focusedButton: {
109
+ backgroundColor: "#3a3a3a",
110
+ borderColor: "#ffffff",
111
+ borderWidth: 2,
112
+ },
108
113
  buttonText: {
109
114
  fontFamily: platformSelect({
110
115
  ios: "SFProText-Medium",
@@ -116,6 +121,9 @@ const defaultProps = {
116
121
  color: "rgba(255, 255, 255, 1)",
117
122
  textTransform: "uppercase",
118
123
  },
124
+ focusedText: {
125
+ color: "#ffffff",
126
+ },
119
127
  },
120
128
  };
121
129
 
@@ -23,7 +23,7 @@ export function FocusableCell(props: Props) {
23
23
  behavior,
24
24
  } = props;
25
25
 
26
- const state = useCellState({ id: item.id, behavior, focused });
26
+ const state = useCellState({ item, behavior, focused });
27
27
 
28
28
  React.useEffect(() => {
29
29
  if (focused && scrollTo && !isAndroid) {
@@ -24,8 +24,7 @@ type Props = {
24
24
  id: string;
25
25
  children: (arg1: boolean) => React.ComponentType<any>;
26
26
  isFocusDisabled: boolean;
27
- initialItemId: string;
28
- isPreferredFocusDisabled: boolean;
27
+ isWithMemory: boolean;
29
28
  focusGroupRef: React.Component;
30
29
  shouldUsePreferredFocus: boolean;
31
30
  groupId: string;
@@ -40,8 +39,7 @@ export class FocusableGroup extends BaseFocusable<Props> {
40
39
  children,
41
40
  id,
42
41
  isFocusDisabled,
43
- initialItemId,
44
- isPreferredFocusDisabled,
42
+ isWithMemory,
45
43
  style = {},
46
44
  groupId,
47
45
  ...otherProps
@@ -63,8 +61,7 @@ export class FocusableGroup extends BaseFocusable<Props> {
63
61
  itemId={id}
64
62
  ref={this.ref}
65
63
  isFocusDisabled={isFocusDisabled}
66
- initialItemId={initialItemId}
67
- isPreferredFocusDisabled={isPreferredFocusDisabled}
64
+ isWithMemory={isWithMemory}
68
65
  onGroupFocus={onGroupFocus}
69
66
  onGroupBlur={onGroupBlur}
70
67
  style={style}
@@ -16,7 +16,6 @@ type Props = {
16
16
  prioritiseFocusOn?: number;
17
17
  groupId?: string;
18
18
  hasTVPreferredFocus?: boolean;
19
- isPreferredFocusDisabled?: boolean;
20
19
  onFocus?: () => void;
21
20
  onBlur?: () => void;
22
21
  willReceiveFocus?: (event?: any) => void;
@@ -1,14 +1,14 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { ViewStyle } from "react-native";
4
3
  import {
5
4
  findPluginByType,
6
5
  findPluginByIdentifier,
7
6
  } from "@applicaster/zapp-react-native-utils/pluginUtils";
8
- import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout";
9
7
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
10
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
11
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import {
9
+ useDimensions,
10
+ useNavigation,
11
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
 
13
13
  import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
14
14
  import { PlayerContainer } from "../PlayerContainer";
@@ -78,12 +78,6 @@ const getPlayer = (
78
78
  return getPlayerWithModuleProperties(PlayerModule);
79
79
  };
80
80
 
81
- const dimensionsContext: "window" | "screen" = platformSelect({
82
- android_tv: "window",
83
- amazon: "window",
84
- default: "screen",
85
- });
86
-
87
81
  type PlayableComponent = {
88
82
  Component: React.ComponentType<any>;
89
83
  };
@@ -91,16 +85,11 @@ type PlayableComponent = {
91
85
  export function HandlePlayable({
92
86
  item,
93
87
  isModal,
94
- modalHeight,
95
- modalWidth,
96
88
  mode,
97
89
  groupId,
98
90
  }: Props): React.ReactElement | null {
99
91
  const state = usePickFromState();
100
92
 
101
- const { width: screenWidth, height: screenHeight } =
102
- useDimensions(dimensionsContext);
103
-
104
93
  const { closeVideoModal } = useNavigation();
105
94
 
106
95
  const [Player, playerModuleProperties] = getPlayer(item, state);
@@ -118,9 +107,7 @@ export function HandlePlayable({
118
107
  if (!isModal) {
119
108
  closeVideoModal();
120
109
  }
121
- }, []);
122
110
 
123
- React.useEffect(() => {
124
111
  const castStateHandler = async () => {
125
112
  const castState = await CastPlugin?.getCastState();
126
113
  setCasting(castState === "Connected");
@@ -142,23 +129,23 @@ export function HandlePlayable({
142
129
  }, []);
143
130
 
144
131
  React.useEffect(() => {
145
- if (casting) {
146
- setPlayable({ Component: CastPlugin?.CastHandler });
147
- } else {
148
- setPlayable({ Component: PlayerContainer });
149
- }
132
+ setPlayable({
133
+ Component: casting ? CastPlugin?.CastHandler : PlayerContainer,
134
+ });
150
135
  }, [casting]);
151
136
 
152
- const Component = playable?.Component;
137
+ const { width: screenWidth, height: screenHeight } = useDimensions("window");
153
138
 
154
139
  const style = React.useMemo(
155
140
  () => ({
156
- width: isModal ? modalWidth : mode === "PIP" ? "100%" : screenWidth,
157
- height: isModal ? modalHeight : mode === "PIP" ? "100%" : screenHeight,
141
+ width: isModal ? "100%" : mode === "PIP" ? "100%" : screenWidth,
142
+ height: isModal ? "100%" : mode === "PIP" ? "100%" : screenHeight,
158
143
  }),
159
- [screenWidth, screenHeight, modalHeight, modalWidth, isModal, mode]
144
+ [screenWidth, screenHeight, isModal, mode]
160
145
  );
161
146
 
147
+ const Component = playable?.Component;
148
+
162
149
  if (Component) {
163
150
  return (
164
151
  <Component
@@ -174,5 +161,5 @@ export function HandlePlayable({
174
161
  );
175
162
  }
176
163
 
177
- return !isModal ? <BufferAnimation videoStyle={style as ViewStyle} /> : null;
164
+ return !isModal ? <BufferAnimation videoStyle={style} /> : null;
178
165
  }
@@ -55,8 +55,11 @@ export const ScreenContainer = React.memo(function ScreenContainer({
55
55
  ComponentsExtraProps,
56
56
  NavBar,
57
57
  }: Props) {
58
- const { screenMarginBottom } = React.useContext(ScreenLayoutContext);
58
+ const { screenMarginBottom, resetScreenLayout } =
59
+ React.useContext(ScreenLayoutContext);
60
+
59
61
  const { currentRoute, screenData } = useNavigation();
62
+
60
63
  const screen = useCurrentScreenData();
61
64
 
62
65
  const { marginBottom, backgroundColor, paddingTop, paddingBottom } =
@@ -65,6 +68,10 @@ export const ScreenContainer = React.memo(function ScreenContainer({
65
68
  const theme = useTheme();
66
69
  const getThemeValue = React.useCallback(R.propOr(0, R.__, theme), [theme]);
67
70
 
71
+ React.useEffect(() => {
72
+ resetScreenLayout();
73
+ }, [currentRoute]);
74
+
68
75
  const fullscreen = React.useCallback(
69
76
  displayFullScreen({ currentRoute, screenData }),
70
77
  [currentRoute, screenData]
@@ -60,10 +60,15 @@ export function ScreenLayoutContextProvider(props: {
60
60
  initialState
61
61
  );
62
62
 
63
+ const resetScreenLayout = React.useCallback(() => {
64
+ setScreenLayout(initialState);
65
+ }, [initialState]);
66
+
63
67
  const screenLayoutValue = React.useMemo(
64
68
  () => ({
65
69
  ...screenLayout,
66
70
  setScreenLayout,
71
+ resetScreenLayout,
67
72
  }),
68
73
  [screenLayout]
69
74
  );
@@ -56,11 +56,12 @@ describe("ScreenContainer", () => {
56
56
  };
57
57
 
58
58
  const setScreenLayout = () => {};
59
+ const resetScreenLayout = () => {};
59
60
 
60
61
  const wrapper = ({ children }) => (
61
62
  <WrappedWithProviders>
62
63
  <ScreenLayoutContext.Provider
63
- value={{ ...screenLayout, setScreenLayout }}
64
+ value={{ ...screenLayout, setScreenLayout, resetScreenLayout }}
64
65
  >
65
66
  <ScreenContainer NavBar={View}>{children}</ScreenContainer>
66
67
  </ScreenLayoutContext.Provider>
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { Image as RnImage, ImageStyle } from "react-native";
3
- import * as R from "ramda";
3
+ import { equals, omit } from "ramda";
4
4
 
5
5
  import { useImageSource } from "./hooks";
6
6
 
@@ -17,7 +17,7 @@ type Props = Record<string, any> & {
17
17
  withDimensions: (source: Source) => Source;
18
18
  };
19
19
 
20
- const MemoizedImage = React.memo(RnImage, R.equals);
20
+ const MemoizedImage = React.memo(RnImage, equals);
21
21
 
22
22
  function Image({
23
23
  style,
@@ -44,11 +44,12 @@ function Image({
44
44
 
45
45
  return (
46
46
  <MemoizedImage
47
+ key={_source?.uri || "no-image"}
47
48
  style={style as ImageStyle}
48
49
  onError={React.useCallback(() => setErrorState(true), [])}
49
50
  // as we have defaults as "" for placeholder image, we need to pass undefined to source to not throw warnings
50
51
  source={_source?.uri ? _source : undefined}
51
- {...R.omit(["source"], otherProps)}
52
+ {...omit(["source"], otherProps)}
52
53
  />
53
54
  );
54
55
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
2
+ import { path } from "ramda";
3
3
 
4
4
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
5
5
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
@@ -7,9 +7,13 @@ import { extractAsset } from "./utils";
7
7
 
8
8
  type Return = { uri: string } | null;
9
9
 
10
+ const getSourceContext = path(["source", "context"]);
11
+ const getSourceUri = path(["source", "uri"]);
12
+ const getState = path(["state"]);
13
+
10
14
  export const useImageSource = ({ uri, entry, otherProps }): Return => {
11
- const uriContext = R.path(["source", "context"], otherProps);
12
- const uriState = R.path(["state"], otherProps);
15
+ const uriContext = getSourceContext(otherProps);
16
+ const uriState = getState(otherProps);
13
17
 
14
18
  const action = useActions(uriContext);
15
19
 
@@ -34,7 +38,7 @@ export const useImageSource = ({ uri, entry, otherProps }): Return => {
34
38
  return { uri };
35
39
  }
36
40
 
37
- const uriFromSource = R.path(["source", "uri"], otherProps);
41
+ const uriFromSource = getSourceUri(otherProps);
38
42
 
39
43
  if (uriFromSource) {
40
44
  return { uri: uriFromSource };
@@ -56,7 +60,7 @@ const getSource = (uri, showDefault, placeholderImage, otherProps) => {
56
60
  return { uri };
57
61
  }
58
62
 
59
- const uriFromSource = R.path(["source", "uri"], otherProps);
63
+ const uriFromSource = getSourceUri(otherProps);
60
64
 
61
65
  if (uriFromSource) {
62
66
  return { uri: uriFromSource };
@@ -72,8 +76,8 @@ export const useImageSourceWithDefault = ({
72
76
  placeholderImage,
73
77
  otherProps,
74
78
  }): Return => {
75
- const uriContext = R.path(["source", "context"], otherProps);
76
- const uriState = R.path(["state"], otherProps);
79
+ const uriContext = getSourceContext(otherProps);
80
+ const uriState = getState(otherProps);
77
81
 
78
82
  const action = useActions(uriContext);
79
83
 
@@ -0,0 +1,136 @@
1
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
2
+ import { StorageSingleValueProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageSingleSelectProvider";
3
+ import { PushTopicManager } from "@applicaster/zapp-react-native-bridge/PushNotifications/PushTopicManager";
4
+ import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
5
+ import React, { useEffect } from "react";
6
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
7
+ import { BehaviorSubject } from "rxjs";
8
+ import { masterCellLogger } from "../logger";
9
+ import get from "lodash/get";
10
+
11
+ const parseContextKey = (key: string): string | null => {
12
+ if (!key?.startsWith("@{ctx/")) return null;
13
+
14
+ return key.substring("@{ctx/".length, key.length - 1);
15
+ };
16
+
17
+ const getDataSourceProvider = (
18
+ behavior: Behavior
19
+ ): BehaviorSubject<string[] | string> | null => {
20
+ if (!behavior) return null;
21
+
22
+ const selection = String(behavior.current_selection);
23
+ const contextKey = parseContextKey(selection);
24
+
25
+ if (contextKey) {
26
+ if (behavior.select_mode === "multi") {
27
+ return StorageMultiSelectProvider.getProvider(contextKey).getObservable();
28
+ }
29
+
30
+ if (behavior.select_mode === "single") {
31
+ return StorageSingleValueProvider.getProvider(contextKey).getObservable();
32
+ }
33
+ }
34
+
35
+ if (behavior.selection_source === "@{push/topics}") {
36
+ return PushTopicManager.getInstance().getEntryObservable();
37
+ }
38
+
39
+ return null;
40
+ };
41
+
42
+ export const useBehaviorUpdate = (behavior: Behavior) => {
43
+ const [lastUpdate, setLastUpdate] = React.useState<number | null>(null);
44
+ const player = usePlayer();
45
+
46
+ const triggerUpdate = () => setLastUpdate(Date.now());
47
+
48
+ useEffect(() => {
49
+ if (!behavior) return;
50
+
51
+ const dataSource = getDataSourceProvider(behavior);
52
+
53
+ if (dataSource) {
54
+ const subscription = dataSource.subscribe(triggerUpdate);
55
+
56
+ return () => subscription.unsubscribe();
57
+ }
58
+ }, [behavior]);
59
+
60
+ useEffect(() => {
61
+ if (!behavior || !player || behavior.selection_source !== "now_playing") {
62
+ return;
63
+ }
64
+
65
+ const subscription = player.getEntryObservable().subscribe(triggerUpdate);
66
+
67
+ return () => subscription.unsubscribe();
68
+ }, [behavior, player]);
69
+
70
+ return lastUpdate;
71
+ };
72
+
73
+ // We cant use async in this function (its inside render),
74
+ // so we rely on useBehaviorUpdate to update current value and trigger re-render
75
+ export const isCellSelected = (
76
+ item: ZappEntry,
77
+ behavior?: Behavior
78
+ ): boolean => {
79
+ if (!behavior) return false;
80
+
81
+ const id = behavior.selector ? get(item, behavior.selector) : item.id;
82
+
83
+ if (behavior.selection_source === "now_playing") {
84
+ const player = playerManager.getActivePlayer();
85
+
86
+ return player?.entry?.id === id;
87
+ }
88
+
89
+ if (behavior.selection_source === "@{push/topics}") {
90
+ if (behavior.select_mode === "single") {
91
+ masterCellLogger.warning(
92
+ "Unexpected single selection mode for push topics"
93
+ );
94
+ }
95
+
96
+ const tags = PushTopicManager.getInstance().getRegisteredTags();
97
+
98
+ return tags.includes(String(id));
99
+ }
100
+
101
+ const selection = String(behavior.current_selection);
102
+ const contextKey = parseContextKey(selection);
103
+
104
+ if (contextKey) {
105
+ if (behavior.select_mode === "single") {
106
+ const selectedItem =
107
+ StorageSingleValueProvider.getProvider(contextKey)?.getValue();
108
+
109
+ return selectedItem === String(id);
110
+ }
111
+
112
+ if (behavior.select_mode === "multi") {
113
+ const selectedItems =
114
+ StorageMultiSelectProvider.getProvider(contextKey)?.getSelectedItems();
115
+
116
+ return selectedItems?.includes(String(id));
117
+ }
118
+ }
119
+
120
+ if (behavior.select_mode === "single") {
121
+ return behavior.current_selection === id;
122
+ }
123
+
124
+ if (
125
+ behavior.select_mode === "multi" &&
126
+ Array.isArray(behavior.current_selection)
127
+ ) {
128
+ const currentSelection: string[] = behavior.current_selection.map(
129
+ (item): string => String(item)
130
+ );
131
+
132
+ return currentSelection.includes(String(id));
133
+ }
134
+
135
+ return false;
136
+ };