@applicaster/zapp-react-native-ui-components 15.0.0-rc.136 → 15.0.0-rc.137
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/Components/River/ComponentsMap/ComponentsMap.tsx +2 -16
- package/Components/River/RefreshControl.tsx +19 -106
- package/Components/River/River.tsx +9 -82
- package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
- package/Components/River/hooks/index.ts +1 -0
- package/Components/River/hooks/usePullToRefresh.ts +51 -0
- package/package.json +5 -5
|
@@ -39,8 +39,6 @@ type Props = {
|
|
|
39
39
|
scrollViewExtraProps?: {};
|
|
40
40
|
riverId?: string;
|
|
41
41
|
getStaticComponentFeed: any;
|
|
42
|
-
pullToRefreshPipesV1RefreshingStateUpdater: () => boolean;
|
|
43
|
-
refreshingPipesV1?: boolean;
|
|
44
42
|
stickyHeaderIndices?: number[];
|
|
45
43
|
};
|
|
46
44
|
|
|
@@ -65,10 +63,6 @@ function ComponentsMapComponent(props: Props) {
|
|
|
65
63
|
groupId,
|
|
66
64
|
riverId,
|
|
67
65
|
getStaticComponentFeed,
|
|
68
|
-
// Method added to keep pipes v1 logic up to date with the pullToRefresh state.
|
|
69
|
-
// TODO: Remove when pipes v1 is deprecated.
|
|
70
|
-
pullToRefreshPipesV1RefreshingStateUpdater,
|
|
71
|
-
refreshingPipesV1,
|
|
72
66
|
stickyHeaderIndices,
|
|
73
67
|
} = props;
|
|
74
68
|
|
|
@@ -163,16 +157,8 @@ function ComponentsMapComponent(props: Props) {
|
|
|
163
157
|
usePipesCacheReset(riverId, riverComponents);
|
|
164
158
|
|
|
165
159
|
const refreshControl = React.useMemo(
|
|
166
|
-
() =>
|
|
167
|
-
|
|
168
|
-
<RefreshControl
|
|
169
|
-
pullToRefreshPipesV1RefreshingStateUpdater={
|
|
170
|
-
pullToRefreshPipesV1RefreshingStateUpdater
|
|
171
|
-
}
|
|
172
|
-
refreshingPipesV1={refreshingPipesV1}
|
|
173
|
-
/>
|
|
174
|
-
) : null,
|
|
175
|
-
[]
|
|
160
|
+
() => (pullToRefreshEnabled ? <RefreshControl /> : null),
|
|
161
|
+
[pullToRefreshEnabled]
|
|
176
162
|
);
|
|
177
163
|
|
|
178
164
|
const navBarStore = useScreenContextV2()._navBarStore;
|
|
@@ -4,9 +4,7 @@ import {
|
|
|
4
4
|
RefreshControl as RNRefreshControl,
|
|
5
5
|
StyleSheet,
|
|
6
6
|
} from "react-native";
|
|
7
|
-
import
|
|
8
|
-
import { path } from "@applicaster/zapp-react-native-utils/utils";
|
|
9
|
-
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
7
|
+
import { get } from "@applicaster/zapp-react-native-utils/utils";
|
|
10
8
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
11
9
|
import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
12
10
|
import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
|
|
@@ -15,8 +13,7 @@ import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/react
|
|
|
15
13
|
import { useShallow } from "zustand/react/shallow";
|
|
16
14
|
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
17
15
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
18
|
-
import {
|
|
19
|
-
import { useGetUrlInflater } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
|
|
16
|
+
import { usePullToRefresh } from "./hooks";
|
|
20
17
|
|
|
21
18
|
const BRIGHTNESS_THRESHOLD = 160;
|
|
22
19
|
const ABOVE_DEFAULT_COLOR = "gray";
|
|
@@ -56,82 +53,6 @@ const getBrightness = (RGBcolor) => {
|
|
|
56
53
|
);
|
|
57
54
|
};
|
|
58
55
|
|
|
59
|
-
export const usePullToRefresh = (
|
|
60
|
-
riverComponents,
|
|
61
|
-
pullToRefreshPipesV1RefreshingStateUpdater,
|
|
62
|
-
refreshingPipesV1
|
|
63
|
-
) => {
|
|
64
|
-
const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater;
|
|
65
|
-
|
|
66
|
-
const [refreshing, setRefreshing] = React.useState(false);
|
|
67
|
-
|
|
68
|
-
const feedSources = React.useMemo(
|
|
69
|
-
() =>
|
|
70
|
-
(riverComponents || [])
|
|
71
|
-
.map((riverComponent) => ({
|
|
72
|
-
source: path(["data", "source"], riverComponent),
|
|
73
|
-
mapping: path(["data", "mapping"], riverComponent),
|
|
74
|
-
}))
|
|
75
|
-
.filter(({ source }) => !isNilOrEmpty(source)),
|
|
76
|
-
[riverComponents]
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const feedsLength = feedSources.length;
|
|
80
|
-
|
|
81
|
-
const [requestsCompletedCounter, setRequestsCompletedCounter] =
|
|
82
|
-
React.useState(0);
|
|
83
|
-
|
|
84
|
-
const loadPipesDataDispatcher = useLoadPipesDataDispatch();
|
|
85
|
-
const urlInflater = useGetUrlInflater();
|
|
86
|
-
|
|
87
|
-
React.useEffect(() => {
|
|
88
|
-
// will not work for pipes v1 on 1st level screens
|
|
89
|
-
if (refreshing && !isPipesV1) {
|
|
90
|
-
feedSources.forEach(({ source, mapping }) => {
|
|
91
|
-
const inflatedUrl = urlInflater(source, mapping);
|
|
92
|
-
|
|
93
|
-
if (inflatedUrl) {
|
|
94
|
-
loadPipesDataDispatcher(inflatedUrl, {
|
|
95
|
-
silentRefresh: true,
|
|
96
|
-
clearCache: true,
|
|
97
|
-
callback: () => {
|
|
98
|
-
setRequestsCompletedCounter(R.inc);
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
} else {
|
|
102
|
-
setRequestsCompletedCounter(R.inc);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}, [
|
|
107
|
-
refreshing,
|
|
108
|
-
isPipesV1,
|
|
109
|
-
feedSources,
|
|
110
|
-
loadPipesDataDispatcher,
|
|
111
|
-
urlInflater,
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
React.useEffect(() => {
|
|
115
|
-
if (requestsCompletedCounter === feedsLength) {
|
|
116
|
-
setRefreshing(false);
|
|
117
|
-
}
|
|
118
|
-
}, [requestsCompletedCounter, feedsLength]);
|
|
119
|
-
|
|
120
|
-
const onRefresh = React.useCallback(() => {
|
|
121
|
-
if (isPipesV1) {
|
|
122
|
-
pullToRefreshPipesV1RefreshingStateUpdater(true);
|
|
123
|
-
} else {
|
|
124
|
-
setRefreshing(true);
|
|
125
|
-
setRequestsCompletedCounter(0);
|
|
126
|
-
}
|
|
127
|
-
}, [isPipesV1]);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
refreshing: isPipesV1 ? refreshingPipesV1 : refreshing,
|
|
131
|
-
onRefresh,
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
|
|
135
56
|
/** Returns the offset for the progress view of the RefreshControl component
|
|
136
57
|
* based on navbar content position */
|
|
137
58
|
export const useGetProgressViewOffset = () => {
|
|
@@ -160,16 +81,12 @@ export const useGetProgressViewOffset = () => {
|
|
|
160
81
|
}
|
|
161
82
|
};
|
|
162
83
|
|
|
163
|
-
export function RefreshControl(
|
|
164
|
-
pullToRefreshPipesV1RefreshingStateUpdater?: (refreshing: boolean) => void;
|
|
165
|
-
refreshingPipesV1?: boolean;
|
|
166
|
-
}) {
|
|
84
|
+
export function RefreshControl() {
|
|
167
85
|
const screenData = useCurrentScreenData();
|
|
168
86
|
|
|
169
87
|
const { refreshing, onRefresh } = usePullToRefresh(
|
|
170
|
-
screenData.
|
|
171
|
-
|
|
172
|
-
props.refreshingPipesV1
|
|
88
|
+
screenData.id,
|
|
89
|
+
screenData.ui_components
|
|
173
90
|
);
|
|
174
91
|
|
|
175
92
|
const { app_background_color: themeBackgroundColor } = useTheme();
|
|
@@ -187,29 +104,26 @@ export function RefreshControl(props: {
|
|
|
187
104
|
displayTitleIOS,
|
|
188
105
|
} = React.useMemo(
|
|
189
106
|
() => ({
|
|
190
|
-
indicatorColor:
|
|
191
|
-
|
|
192
|
-
screenData.styles
|
|
193
|
-
|
|
194
|
-
titleUnderIndicatorColor: R.prop(
|
|
195
|
-
"pull_to_refresh_title_color_under_indicator",
|
|
196
|
-
screenData.styles
|
|
107
|
+
indicatorColor: get(screenData.styles, "pull_to_refresh_indicator_color"),
|
|
108
|
+
titleUnderIndicatorColor: get(
|
|
109
|
+
screenData.styles,
|
|
110
|
+
"pull_to_refresh_title_color_under_indicator"
|
|
197
111
|
),
|
|
198
|
-
indicatorBackgroundColor:
|
|
199
|
-
|
|
200
|
-
|
|
112
|
+
indicatorBackgroundColor: get(
|
|
113
|
+
screenData.styles,
|
|
114
|
+
"pull_to_refresh_indicator_bg_color"
|
|
201
115
|
),
|
|
202
116
|
indicatorSize:
|
|
203
|
-
|
|
117
|
+
get(screenData.styles, "pull_to_refresh_indicator_size") === "large"
|
|
204
118
|
? "large"
|
|
205
119
|
: "default",
|
|
206
|
-
generalContentBackgroungColor:
|
|
207
|
-
|
|
208
|
-
|
|
120
|
+
generalContentBackgroungColor: get(
|
|
121
|
+
screenData.styles,
|
|
122
|
+
"screen_background_color"
|
|
209
123
|
),
|
|
210
|
-
displayTitleIOS:
|
|
211
|
-
|
|
212
|
-
|
|
124
|
+
displayTitleIOS: get(
|
|
125
|
+
screenData.styles,
|
|
126
|
+
"pull_to_refresh_display_title_ios"
|
|
213
127
|
),
|
|
214
128
|
}),
|
|
215
129
|
[screenData]
|
|
@@ -276,7 +190,6 @@ export function RefreshControl(props: {
|
|
|
276
190
|
|
|
277
191
|
return (
|
|
278
192
|
<RNRefreshControl
|
|
279
|
-
{...props}
|
|
280
193
|
refreshing={refreshing}
|
|
281
194
|
onRefresh={onRefreshHandler}
|
|
282
195
|
colors={[preparedIndicatorColor]}
|
|
@@ -2,7 +2,6 @@ import * as React from "react";
|
|
|
2
2
|
import * as R from "ramda";
|
|
3
3
|
import { GeneralContentScreen } from "@applicaster/zapp-react-native-ui-components/Components/GeneralContentScreen";
|
|
4
4
|
|
|
5
|
-
import { FeedLoader } from "../FeedLoader";
|
|
6
5
|
import { ScreenResolver } from "../ScreenResolver";
|
|
7
6
|
|
|
8
7
|
type Props = ZappScreenProps & {
|
|
@@ -23,24 +22,13 @@ type Props = ZappScreenProps & {
|
|
|
23
22
|
river: ZappRiver;
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
refreshing: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export class RiverComponent extends React.Component<Props, State> {
|
|
25
|
+
export class RiverComponent extends React.Component<Props> {
|
|
31
26
|
private currentScreenTitle: string;
|
|
32
27
|
private currentScreenSummary: string;
|
|
33
28
|
constructor(props: Props) {
|
|
34
29
|
super(props);
|
|
35
30
|
|
|
36
|
-
this.state = {
|
|
37
|
-
refreshing: false,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
31
|
this.applyContexts();
|
|
41
|
-
|
|
42
|
-
this.pullToRefreshPipesV1RefreshingStateUpdater =
|
|
43
|
-
this.pullToRefreshPipesV1RefreshingStateUpdater.bind(this);
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
applyContexts() {
|
|
@@ -69,20 +57,9 @@ export class RiverComponent extends React.Component<Props, State> {
|
|
|
69
57
|
}
|
|
70
58
|
}
|
|
71
59
|
|
|
72
|
-
usesPipesV1Layout() {
|
|
73
|
-
return this.props?.appData?.layoutVersion === "v1";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Method added to keep pipes v1 logic up to date with the pullToRefresh state.
|
|
77
|
-
// TODO: Remove when pipes v1 is deprecated.
|
|
78
|
-
pullToRefreshPipesV1RefreshingStateUpdater(refreshing) {
|
|
79
|
-
this.setState({ refreshing });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
60
|
render() {
|
|
83
61
|
const {
|
|
84
62
|
river,
|
|
85
|
-
feedUrl,
|
|
86
63
|
screenData,
|
|
87
64
|
isInsideContainer,
|
|
88
65
|
groupId,
|
|
@@ -91,22 +68,10 @@ export class RiverComponent extends React.Component<Props, State> {
|
|
|
91
68
|
|
|
92
69
|
const { id, type } = river;
|
|
93
70
|
|
|
94
|
-
const connectedFeedURL = R.path(["content", "src"], screenData);
|
|
95
|
-
const _feedUrl = feedUrl || connectedFeedURL;
|
|
96
|
-
|
|
97
71
|
if (type !== "general_content") {
|
|
98
|
-
let riverWithConnectedDatasource;
|
|
99
|
-
|
|
100
|
-
if (_feedUrl && this.usesPipesV1Layout()) {
|
|
101
|
-
riverWithConnectedDatasource = {
|
|
102
|
-
...river,
|
|
103
|
-
data: R.merge(river.data || {}, { source: _feedUrl }),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
72
|
return (
|
|
108
73
|
<ScreenResolver
|
|
109
|
-
screenData={R.mergeLeft(
|
|
74
|
+
screenData={R.mergeLeft(river, {
|
|
110
75
|
groupId,
|
|
111
76
|
...screenData,
|
|
112
77
|
})}
|
|
@@ -116,53 +81,15 @@ export class RiverComponent extends React.Component<Props, State> {
|
|
|
116
81
|
);
|
|
117
82
|
}
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
this.currentScreenTitle = (screenData && screenData.title) || null;
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<GeneralContentScreen
|
|
124
|
-
screenId={id}
|
|
125
|
-
isScreenWrappedInContainer={isInsideContainer}
|
|
126
|
-
groupId={groupId}
|
|
127
|
-
scrollViewExtraProps={scrollViewExtraProps}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
84
|
+
this.currentScreenTitle = (screenData && screenData.title) || null;
|
|
131
85
|
|
|
132
86
|
return (
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
>
|
|
140
|
-
{(feed) => {
|
|
141
|
-
if (!feed) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.currentScreenSummary = (feed && feed.summary) || null;
|
|
146
|
-
|
|
147
|
-
this.currentScreenTitle =
|
|
148
|
-
(feed && feed.title) || (screenData && screenData.title) || null;
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<GeneralContentScreen
|
|
152
|
-
screenId={id}
|
|
153
|
-
groupId={groupId}
|
|
154
|
-
feed={feed}
|
|
155
|
-
isScreenWrappedInContainer={isInsideContainer}
|
|
156
|
-
scrollViewExtraProps={scrollViewExtraProps}
|
|
157
|
-
componentsMapExtraProps={{
|
|
158
|
-
pullToRefreshPipesV1RefreshingStateUpdater:
|
|
159
|
-
this.pullToRefreshPipesV1RefreshingStateUpdater,
|
|
160
|
-
refreshingPipesV1: this.state.refreshing,
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
}}
|
|
165
|
-
</FeedLoader>
|
|
87
|
+
<GeneralContentScreen
|
|
88
|
+
screenId={id}
|
|
89
|
+
isScreenWrappedInContainer={isInsideContainer}
|
|
90
|
+
groupId={groupId}
|
|
91
|
+
scrollViewExtraProps={scrollViewExtraProps}
|
|
92
|
+
/>
|
|
166
93
|
);
|
|
167
94
|
}
|
|
168
95
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
|
|
3
|
+
|
|
4
|
+
import { usePullToRefresh } from "../usePullToRefresh";
|
|
5
|
+
|
|
6
|
+
jest.mock(
|
|
7
|
+
"@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator",
|
|
8
|
+
() => ({
|
|
9
|
+
refreshCoordinator: {
|
|
10
|
+
triggerRefresh: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
jest.useFakeTimers();
|
|
16
|
+
|
|
17
|
+
describe("usePullToRefresh", () => {
|
|
18
|
+
const screenId = "test-screen";
|
|
19
|
+
const components = [{ id: "comp1" }, { id: "comp2" }] as any;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should initialize with refreshing=false", () => {
|
|
26
|
+
const { result } = renderHook(() => usePullToRefresh(screenId, components));
|
|
27
|
+
|
|
28
|
+
expect(result.current.refreshing).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should trigger refresh and set refreshing=true", () => {
|
|
32
|
+
const { result } = renderHook(() => usePullToRefresh(screenId, components));
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
result.current.onRefresh();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.current.refreshing).toBe(true);
|
|
39
|
+
|
|
40
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
|
|
41
|
+
components,
|
|
42
|
+
screenId
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should set refreshing=false after spinner duration", () => {
|
|
47
|
+
const { result } = renderHook(() => usePullToRefresh(screenId, components));
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
result.current.onRefresh();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.current.refreshing).toBe(true);
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
jest.advanceTimersByTime(1500);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.current.refreshing).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should not stop refreshing before spinner duration", () => {
|
|
63
|
+
const { result } = renderHook(() => usePullToRefresh(screenId, components));
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
result.current.onRefresh();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
jest.advanceTimersByTime(1000);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.refreshing).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should clear timeout on unmount", () => {
|
|
77
|
+
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
|
|
78
|
+
|
|
79
|
+
const { result, unmount } = renderHook(() =>
|
|
80
|
+
usePullToRefresh(screenId, components)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
act(() => {
|
|
84
|
+
result.current.onRefresh();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
unmount();
|
|
88
|
+
|
|
89
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should handle multiple refresh calls correctly", () => {
|
|
93
|
+
const { result } = renderHook(() => usePullToRefresh(screenId, components));
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
result.current.onRefresh();
|
|
97
|
+
result.current.onRefresh();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(result.current.refreshing).toBe(true);
|
|
102
|
+
|
|
103
|
+
act(() => {
|
|
104
|
+
jest.advanceTimersByTime(1500);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.current.refreshing).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should use latest props in callback", () => {
|
|
111
|
+
const { result, rerender } = renderHook(
|
|
112
|
+
({ screenId, components }) => usePullToRefresh(screenId, components),
|
|
113
|
+
{
|
|
114
|
+
initialProps: { screenId, components },
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const newComponents = [{ id: "new-comp" }] as any;
|
|
119
|
+
const newScreenId = "new-screen";
|
|
120
|
+
|
|
121
|
+
rerender({ screenId: newScreenId, components: newComponents });
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
result.current.onRefresh();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
|
|
128
|
+
newComponents,
|
|
129
|
+
newScreenId
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePullToRefresh } from "./usePullToRefresh";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
|
|
3
|
+
|
|
4
|
+
const SPINNER_DURATION_MS = 1500;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pull-to-refresh hook.
|
|
8
|
+
*
|
|
9
|
+
* Triggers a refresh for all screen components via RefreshCoordinator.
|
|
10
|
+
* Each component's UrlFeedResolver already subscribes to refresh$ events
|
|
11
|
+
* and calls reloadData() when triggered — so this hook only needs to:
|
|
12
|
+
* 1. Push events into the refresh bus
|
|
13
|
+
* 2. Show a fixed-duration spinner as UX feedback
|
|
14
|
+
*
|
|
15
|
+
* Data updates arrive reactively via Redux (silentRefresh: true keeps
|
|
16
|
+
* old data visible while loading).
|
|
17
|
+
*/
|
|
18
|
+
export const usePullToRefresh = (
|
|
19
|
+
screenId: string,
|
|
20
|
+
components: ZappUIComponent[] = []
|
|
21
|
+
) => {
|
|
22
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
23
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
|
+
|
|
25
|
+
// Cleanup timer on unmount
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
if (timerRef.current) {
|
|
29
|
+
clearTimeout(timerRef.current);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const onRefresh = useCallback(() => {
|
|
35
|
+
setRefreshing(true);
|
|
36
|
+
refreshCoordinator.triggerRefresh(components, screenId);
|
|
37
|
+
|
|
38
|
+
// Spinner is UX feedback for the gesture.
|
|
39
|
+
// Data updates arrive reactively via Redux (silentRefresh: true).
|
|
40
|
+
if (timerRef.current) {
|
|
41
|
+
clearTimeout(timerRef.current);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
timerRef.current = setTimeout(
|
|
45
|
+
() => setRefreshing(false),
|
|
46
|
+
SPINNER_DURATION_MS
|
|
47
|
+
);
|
|
48
|
+
}, [components, screenId]);
|
|
49
|
+
|
|
50
|
+
return { refreshing, onRefresh };
|
|
51
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.0.0-rc.
|
|
3
|
+
"version": "15.0.0-rc.137",
|
|
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.0.0-rc.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.0.0-rc.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.0.0-rc.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.0.0-rc.
|
|
31
|
+
"@applicaster/applicaster-types": "15.0.0-rc.137",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.0.0-rc.137",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.0.0-rc.137",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.0.0-rc.137",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|