@applicaster/zapp-react-native-ui-components 14.0.0-rc.51 → 14.0.0-rc.52

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,12 +1,13 @@
1
1
  import { View } from "react-native";
2
2
  import React from "react";
3
- import { act, render } from "@testing-library/react-native";
3
+ import { act } from "@testing-library/react-native";
4
4
  import { CellWithFocusable } from "../CellWithFocusable.tsx";
5
5
 
6
6
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
7
+ import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils/index.tsx";
7
8
 
8
9
  const renderWith = (props) => {
9
- return render(<CellWithFocusable {...props} />);
10
+ return renderWithProviders(<CellWithFocusable {...props} />);
10
11
  };
11
12
 
12
13
  describe("CellWithFocusable", () => {
@@ -1,25 +1,16 @@
1
1
  import React from "react";
2
2
  import * as R from "ramda";
3
+ import { selectZappPipes } from "@applicaster/zapp-react-native-redux";
3
4
 
4
5
  type Props = {
5
- zappPipes: ZappPipesData;
6
- loadPipesData: (
7
- feed: string,
8
- options?: Partial<{
9
- clearCache: boolean;
10
- meta: any;
11
- loadLocalFavorites: boolean;
12
- silentRefresh: boolean;
13
- parentFeed: ZappFeed;
14
- callback: () => void;
15
- bodyParams: any;
16
- riverId: string;
17
- }>
18
- ) => void;
6
+ zappPipes: ReturnType<typeof selectZappPipes>;
7
+ loadPipesData: ReturnType<
8
+ typeof import("@applicaster/zapp-react-native-utils/reactHooks/feed").useLoadPipesDataDispatch
9
+ >;
19
10
  feedUrl: string;
20
11
  children: (feed: ZappFeed) => React.ComponentType<any>;
21
12
  onFeedLoaded: (feed: ZappFeed) => {};
22
- onError: (feed: ZappFeed) => {};
13
+ onError: (error: ZappPipesData["error"]) => {};
23
14
  refreshing: boolean;
24
15
  refreshCallback: () => void;
25
16
  };
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import {
3
+ selectZappPipes,
4
+ useAppSelector,
5
+ } from "@applicaster/zapp-react-native-redux";
6
+ import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks/feed";
7
+
8
+ export const FeedLoaderHOC = (_Component: any) => {
9
+ return function FeedLoaderHOC(props: any) {
10
+ const zappPipes = useAppSelector(selectZappPipes);
11
+ const loadPipesData = useLoadPipesDataDispatch();
12
+
13
+ return (
14
+ <_Component
15
+ {...props}
16
+ zappPipes={zappPipes}
17
+ loadPipesData={loadPipesData}
18
+ />
19
+ );
20
+ };
21
+ };
@@ -1,10 +1,4 @@
1
- import * as R from "ramda";
2
-
3
- import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
4
- import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
5
-
6
1
  import { FeedLoaderComponent } from "./FeedLoader";
2
+ import { FeedLoaderHOC } from "./FeedLoaderHOC";
7
3
 
8
- export const FeedLoader = connectToStore(R.pick(["zappPipes"]), {
9
- loadPipesData,
10
- })(FeedLoaderComponent);
4
+ export const FeedLoader = FeedLoaderHOC(FeedLoaderComponent);
@@ -3,8 +3,6 @@ import { all, equals, path, prop, isEmpty, pluck, values } from "ramda";
3
3
  import { useEffect, useMemo } from "react";
4
4
 
5
5
  import {
6
- ZappPipes,
7
- useAppDispatch,
8
6
  useLayoutPresets,
9
7
  useZappPipesFeed,
10
8
  } from "@applicaster/zapp-react-native-redux";
@@ -25,6 +23,7 @@ import {
25
23
  } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
26
24
 
27
25
  import { produce } from "immer";
26
+ import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
28
27
  // types reference
29
28
 
30
29
  declare interface CurationEntry {
@@ -126,8 +125,6 @@ export const getFinalComponents = (
126
125
  export const useCurationAPI = (
127
126
  components: Array<ZappUIComponent>
128
127
  ): ZappUIComponent[] => {
129
- const dispatch = useAppDispatch();
130
-
131
128
  const smartComponents = useMemo(
132
129
  () => components?.filter?.(isSmartComponent) ?? [],
133
130
  [components]
@@ -163,17 +160,19 @@ export const useCurationAPI = (
163
160
 
164
161
  const urls = useMemo<string[]>(() => Object.values(urlsMap), [urlsMap]);
165
162
 
163
+ const loadPipesDataDispatcher = useLoadPipesDataDispatch();
164
+
166
165
  useEffect(() => {
167
166
  urls.forEach((url, index) => {
168
167
  if (url) {
169
- dispatch(ZappPipes.loadPipesData(url, { clearCache: false }));
168
+ loadPipesDataDispatcher(url, { clearCache: false });
170
169
  } else {
171
170
  logger.log_error("Curation url is empty", {
172
171
  componentId: smartComponents?.[index]?.id,
173
172
  });
174
173
  }
175
174
  });
176
- }, [urls]);
175
+ }, [urls, loadPipesDataDispatcher]);
177
176
 
178
177
  const feeds = useZappPipesFeed(urls);
179
178
  const layoutPresets = useLayoutPresets();
@@ -23,6 +23,7 @@ type Props = {
23
23
  style: ViewStyle;
24
24
  testID?: string;
25
25
  accessibilityLabel?: string;
26
+ accessibilityHint?: string;
26
27
  cellUUID?: string;
27
28
  extraProps?: Record<string, any>;
28
29
  };
@@ -98,6 +99,7 @@ export function ActionButton(props: Props) {
98
99
  onPress={onPress}
99
100
  testID={props?.testID || `${item?.id}`}
100
101
  accessibilityLabel={props?.accessibilityLabel || `${item?.id}`}
102
+ accessibilityHint={props?.accessibilityHint}
101
103
  accessible={!!(props?.testID || props?.accessibilityLabel)}
102
104
  style={props?.style}
103
105
  >
@@ -1,28 +1,58 @@
1
1
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
2
- import { StorageSingleValueProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageSingleSelectProvider";
2
+ import { StorageSingleValueProvider } from "@applicaster/zapp-react-native-utils/storage/StorageSingleSelectProvider";
3
3
  import { PushTopicManager } from "@applicaster/zapp-react-native-bridge/PushNotifications/PushTopicManager";
4
- import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
4
+ import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-utils/storage/StorageMultiSelectProvider";
5
5
  import React, { useEffect } from "react";
6
6
  import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
7
7
  import { BehaviorSubject } from "rxjs";
8
8
  import { masterCellLogger } from "../logger";
9
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);
10
+ import { ScreenMultiSelectProvider } from "@applicaster/zapp-react-native-utils/storage/ScreenStateMultiSelectProvider";
11
+ import { ScreenSingleValueProvider } from "@applicaster/zapp-react-native-utils/storage/ScreenSingleValueProvider";
12
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
13
+ import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
14
+
15
+ const parseContextKey = (
16
+ key: string,
17
+ context: string = "ctx"
18
+ ): string | null => {
19
+ if (!key?.startsWith(`@{${context}/`)) return null;
20
+
21
+ return key.substring(`@{${context}/`.length, key.length - 1);
15
22
  };
16
23
 
17
24
  const getDataSourceProvider = (
18
- behavior: Behavior
25
+ behavior: Behavior,
26
+ screenRoute: string,
27
+ screenStateStore: ReturnType<typeof useScreenStateStore>
19
28
  ): BehaviorSubject<string[] | string> | null => {
20
29
  if (!behavior) return null;
21
30
 
22
31
  const selection = String(behavior.current_selection);
32
+ const screenKey = parseContextKey(selection, "screen");
33
+
34
+ if (screenKey) {
35
+ if (behavior.select_mode === "multi") {
36
+ return ScreenMultiSelectProvider.getProvider(
37
+ screenKey,
38
+ screenRoute,
39
+ screenStateStore
40
+ ).getObservable();
41
+ }
42
+
43
+ if (behavior.select_mode === "single") {
44
+ return ScreenSingleValueProvider.getProvider(
45
+ screenKey,
46
+ screenRoute,
47
+ screenStateStore
48
+ ).getObservable();
49
+ }
50
+ }
51
+
23
52
  const contextKey = parseContextKey(selection);
24
53
 
25
54
  if (contextKey) {
55
+ // TODO: Add storage scope to behavior
26
56
  if (behavior.select_mode === "multi") {
27
57
  return StorageMultiSelectProvider.getProvider(contextKey).getObservable();
28
58
  }
@@ -41,6 +71,8 @@ const getDataSourceProvider = (
41
71
 
42
72
  export const useBehaviorUpdate = (behavior: Behavior) => {
43
73
  const [lastUpdate, setLastUpdate] = React.useState<number | null>(null);
74
+ const screenRoute = useRoute()?.pathname || "";
75
+ const screenStateStore = useScreenStateStore();
44
76
  const player = usePlayer();
45
77
 
46
78
  const triggerUpdate = () => setLastUpdate(Date.now());
@@ -48,7 +80,11 @@ export const useBehaviorUpdate = (behavior: Behavior) => {
48
80
  useEffect(() => {
49
81
  if (!behavior) return;
50
82
 
51
- const dataSource = getDataSourceProvider(behavior);
83
+ const dataSource = getDataSourceProvider(
84
+ behavior,
85
+ screenRoute,
86
+ screenStateStore
87
+ );
52
88
 
53
89
  if (dataSource) {
54
90
  const subscription = dataSource.subscribe(triggerUpdate);
@@ -72,10 +108,17 @@ export const useBehaviorUpdate = (behavior: Behavior) => {
72
108
 
73
109
  // We cant use async in this function (its inside render),
74
110
  // 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 => {
111
+ export const isCellSelected = ({
112
+ item,
113
+ screenRoute,
114
+ screenStateStore,
115
+ behavior,
116
+ }: {
117
+ item: ZappEntry;
118
+ screenRoute: string;
119
+ screenStateStore: ReturnType<typeof useScreenStateStore>;
120
+ behavior?: Behavior;
121
+ }): boolean => {
79
122
  if (!behavior) return false;
80
123
 
81
124
  const id = behavior.selector ? get(item, behavior.selector) : item.id;
@@ -99,7 +142,32 @@ export const isCellSelected = (
99
142
  }
100
143
 
101
144
  const selection = String(behavior.current_selection);
102
- const contextKey = parseContextKey(selection);
145
+
146
+ const screenKey = parseContextKey(selection, "screen");
147
+
148
+ if (screenKey) {
149
+ if (behavior.select_mode === "single") {
150
+ const selectedItem = ScreenSingleValueProvider.getProvider(
151
+ screenKey,
152
+ screenRoute,
153
+ screenStateStore
154
+ ).getValue();
155
+
156
+ return selectedItem === String(id);
157
+ }
158
+
159
+ if (behavior.select_mode === "multi") {
160
+ const selectedItems = ScreenMultiSelectProvider.getProvider(
161
+ screenKey,
162
+ screenRoute,
163
+ screenStateStore
164
+ ).getSelectedItems();
165
+
166
+ return selectedItems?.includes(String(id));
167
+ }
168
+ }
169
+
170
+ const contextKey = parseContextKey(selection, "ctx");
103
171
 
104
172
  if (contextKey) {
105
173
  if (behavior.select_mode === "single") {
@@ -8,6 +8,8 @@ import { masterCellLogger } from "../logger";
8
8
  import { getCellState } from "../../Cell/utils";
9
9
  import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
10
10
  import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
11
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
12
+ import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
11
13
 
12
14
  const hasElementSpecificViewType = (viewType) => (element) => {
13
15
  if (R.isNil(element)) {
@@ -190,10 +192,6 @@ export const getFocusedButtonId = (focusable) => {
190
192
  });
191
193
  };
192
194
 
193
- export const isSelected = (item: ZappEntry, behavior?: Behavior) => {
194
- return isCellSelected(item, behavior);
195
- };
196
-
197
195
  export const useCellState = ({
198
196
  item,
199
197
  behavior,
@@ -204,9 +202,17 @@ export const useCellState = ({
204
202
  focused: boolean;
205
203
  }): CellState => {
206
204
  const lastUpdate = useBehaviorUpdate(behavior);
205
+ const router = useRoute();
206
+ const screenStateStore = useScreenStateStore();
207
207
 
208
208
  const _isSelected = useMemo(
209
- () => isSelected(item, behavior),
209
+ () =>
210
+ isCellSelected({
211
+ item,
212
+ screenRoute: router?.pathname,
213
+ screenStateStore,
214
+ behavior,
215
+ }),
210
216
  [behavior, item, lastUpdate]
211
217
  );
212
218
 
@@ -13,10 +13,7 @@ import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/react
13
13
  import { useShallow } from "zustand/react/shallow";
14
14
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
15
15
  import { useSafeAreaInsets } from "react-native-safe-area-context";
16
- import {
17
- ZappPipes,
18
- useAppDispatch,
19
- } from "@applicaster/zapp-react-native-redux";
16
+ import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
20
17
 
21
18
  const BRIGHTNESS_THRESHOLD = 160;
22
19
  const ABOVE_DEFAULT_COLOR = "gray";
@@ -63,38 +60,33 @@ export const usePullToRefresh = (
63
60
  ) => {
64
61
  const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater;
65
62
 
66
- const dispatch = useAppDispatch();
67
-
68
63
  const [refreshing, setRefreshing] = React.useState(false);
69
64
 
70
65
  const feeds: string[] =
71
66
  riverComponents?.map(R.path(["data", "source"])).filter((feed) => !!feed) ??
72
67
  [];
73
68
 
74
- const screenData = useCurrentScreenData();
75
-
76
69
  const feedsLength = feeds.length;
77
70
 
78
71
  const [requestsCompletedCounter, setRequestsCompletedCounter] =
79
72
  React.useState(0);
80
73
 
74
+ const loadPipesDataDispatcher = useLoadPipesDataDispatch();
75
+
81
76
  React.useEffect(() => {
82
77
  // will not work for pipes v1 on 1st level screens
83
78
  if (refreshing && !isPipesV1) {
84
79
  feeds.forEach((feed) => {
85
- dispatch(
86
- ZappPipes.loadPipesData(feed, {
87
- silentRefresh: true,
88
- clearCache: true,
89
- callback: () => {
90
- setRequestsCompletedCounter(R.inc);
91
- },
92
- riverId: screenData.id,
93
- })
94
- );
80
+ loadPipesDataDispatcher(feed, {
81
+ silentRefresh: true,
82
+ clearCache: true,
83
+ callback: () => {
84
+ setRequestsCompletedCounter(R.inc);
85
+ },
86
+ });
95
87
  });
96
88
  }
97
- }, [refreshing, isPipesV1]);
89
+ }, [refreshing, isPipesV1, feeds, loadPipesDataDispatcher]);
98
90
 
99
91
  React.useEffect(() => {
100
92
  if (requestsCompletedCounter === feedsLength) {
@@ -1,20 +1,10 @@
1
1
  import React from "react";
2
- import { render, screen } from "@testing-library/react-native";
3
- import configureStore from "redux-mock-store";
2
+ import { screen } from "@testing-library/react-native";
4
3
  import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
5
- import { Provider } from "react-redux";
6
4
 
7
- import { River } from "../";
8
5
  import { ScreenResolver } from "../../ScreenResolver";
9
6
  import { RiverComponent } from "../River";
10
7
 
11
- jest.mock(
12
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive",
13
- () => ({
14
- useIsScreenActive: jest.fn(() => true),
15
- })
16
- );
17
-
18
8
  jest.mock(
19
9
  "@applicaster/zapp-react-native-ui-components/Components/GeneralContentScreen",
20
10
  () => {
@@ -46,6 +36,13 @@ jest.mock(
46
36
  })
47
37
  );
48
38
 
39
+ jest.mock(
40
+ "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive",
41
+ () => ({
42
+ useIsScreenActive: jest.fn(() => true),
43
+ })
44
+ );
45
+
49
46
  const river = {
50
47
  home: true,
51
48
  id: "A1234",
@@ -89,28 +86,17 @@ const riverProps = {
89
86
 
90
87
  const appData = { layoutVersion: "v1" };
91
88
 
92
- const store = configureStore()({ rivers, appData });
89
+ const store = { rivers, appData };
93
90
 
94
91
  describe("When River has a general_content type", () => {
95
92
  it("renders GeneralContentScreen correctly", () => {
96
- const { getByTestId } = render(
97
- <Provider store={store}>
98
- <RiverComponent {...riverProps} />
99
- </Provider>
93
+ const { getByTestId } = renderWithProviders(
94
+ <RiverComponent {...riverProps} />,
95
+ store
100
96
  );
101
97
 
102
98
  expect(getByTestId("general-content-screen")).toBeTruthy();
103
99
  });
104
-
105
- it("renders River component correctly", () => {
106
- const wrapper = render(
107
- <Provider store={store}>
108
- <River screenId="A1234" />
109
- </Provider>
110
- );
111
-
112
- expect(wrapper.toJSON()).toMatchSnapshot();
113
- });
114
100
  });
115
101
 
116
102
  describe("When River has any other type other than general_content", () => {
@@ -13,7 +13,8 @@ import {
13
13
  } from "@applicaster/zapp-react-native-utils/reactHooks";
14
14
  import { useModalNavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ModalNavigationContext";
15
15
  import { useNestedNavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NestedNavigationContext";
16
- import { create, StoreApi, UseBoundStore } from "zustand";
16
+ import { create } from "zustand";
17
+ import { subscribeWithSelector } from "zustand/middleware";
17
18
  import { useShallow } from "zustand/react/shallow";
18
19
  import { Animated } from "react-native";
19
20
 
@@ -39,11 +40,26 @@ interface NavBarState {
39
40
  setSummary: (subtitle: string) => void;
40
41
  }
41
42
 
42
- type ScreenContextType = {
43
- _navBarStore: UseBoundStore<StoreApi<NavBarStoreState>>;
44
- navBar: NavBarState;
45
- legacyFormatScreenData: LegacyNavigationScreenData | null;
46
- };
43
+ const createStateStore = () =>
44
+ create(
45
+ subscribeWithSelector<ScreenStateStore>((set) => ({
46
+ data: {},
47
+ setValue: (key, value) =>
48
+ set((state) => ({
49
+ data: {
50
+ ...state.data,
51
+ [key]: value,
52
+ },
53
+ })),
54
+ removeValue: (key) =>
55
+ set((state) => {
56
+ const newData = { ...state.data };
57
+ delete newData[key];
58
+
59
+ return { data: newData };
60
+ }),
61
+ }))
62
+ );
47
63
 
48
64
  const createStore = () =>
49
65
  create<NavBarStoreState>((set) => ({
@@ -65,7 +81,15 @@ const createStore = () =>
65
81
  },
66
82
  }));
67
83
 
84
+ type ScreenContextType = {
85
+ _navBarStore: ReturnType<typeof createStore>;
86
+ _stateStore: ReturnType<typeof createStateStore>;
87
+ navBar: NavBarState;
88
+ legacyFormatScreenData: LegacyNavigationScreenData | null;
89
+ };
90
+
68
91
  export const ScreenContext = createContext<ScreenContextType>({
92
+ _stateStore: createStateStore(),
69
93
  _navBarStore: createStore(),
70
94
  navBar: {
71
95
  visible: true,
@@ -103,6 +127,21 @@ export function ScreenContextProvider({
103
127
  null
104
128
  );
105
129
 
130
+ const screenStateRef = useRef<null | ReturnType<typeof createStateStore>>(
131
+ null
132
+ );
133
+
134
+ const getScreenState = useCallback(() => {
135
+ if (screenStateRef.current !== null) {
136
+ return screenStateRef.current;
137
+ }
138
+
139
+ const stateStore = createStateStore();
140
+ screenStateRef.current = stateStore;
141
+
142
+ return stateStore;
143
+ }, []);
144
+
106
145
  const getScreenNavBarState = useCallback(() => {
107
146
  if (screenNavBarStateRef.current !== null) {
108
147
  return screenNavBarStateRef.current;
@@ -163,6 +202,7 @@ export function ScreenContextProvider({
163
202
  value={useMemo(
164
203
  () => ({
165
204
  _navBarStore: getScreenNavBarState(),
205
+ _stateStore: getScreenState(),
166
206
  navBar: navBarState,
167
207
  legacyFormatScreenData: routeScreenData,
168
208
  }),