-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
Hooks.js
225 lines (200 loc) · 7.83 KB
/
Hooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
'use strict';
import React, {useState, useEffect, useMemo, useCallback, useRef, useContext} from 'react';
import {GlobalStateContext, DispatchContext, STATE_ACTIONS, getTheme} from './StateManager';
import {Keyboard} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export const useAsyncVariable = (initIsLoaded, loadVariable, onLoad, initOnLoadValue) => {
/*
Loads a variable asynchronously and returns status of load
Useful for determining when a variable from the Sefaria object is available, e.g. Sefaria.calendar
onLoad - optional function to run on load. will be passed initOnLoadValue when initIsLoaded is true
*/
const [isLoaded, setIsLoaded] = useState(initIsLoaded);
const loadWrapper = useCallback(() => {
if (!isLoaded) {
return loadVariable();
}
return Promise.resolve(initOnLoadValue);
}, [isLoaded, loadVariable]);
const onLoadWrapper = useCallback((data) => {
setIsLoaded(true);
if (onLoad) {
onLoad(data);
}
}, [onLoad]);
useAsync(loadWrapper, onLoadWrapper);
return isLoaded;
};
export const useAsync = (asyncFn, onSuccess) => {
useEffect(() => {
let isMounted = true;
asyncFn().then(data => {
if (isMounted) onSuccess(data);
});
return () => {
isMounted = false
};
}, [asyncFn, onSuccess]);
};
export function usePaginatedLoad(fetchDataByPage, setter, identityElement, numPages, resetValue = false) {
/*
See `useIncrementalLoad` docs
*/
const [page, setPage] = useState(0);
const [isCanceled, setCanceled] = useState({}); // dict {idElem: Bool}
const [valueQueue, setValueQueue] = useState(null);
const [finishedLoading, setFinishedLoading] = useState(false);
// When identityElement changes:
// Set current identityElement to not canceled
// Sets previous identityElement to canceled.
// Removes old items by calling setter(false);
// Resets page to 0
useEffect(() => {
setCanceled(d => {
d[identityElement] = false;
return Object.assign({}, d);
});
return () => {
setCanceled(d => {
d[identityElement] = true;
return Object.assign({}, d);
});
setter(resetValue);
setPage(0);
setFinishedLoading(false);
}
}, [identityElement]);
const fetchPage = useCallback(() => fetchDataByPage(page), [page, fetchDataByPage]);
// make sure value setting callback and page procession get short circuited when id_elem has been canceled
// clear value queue on success
const setResult = useCallback((id_elem, val) => {
if (isCanceled[id_elem]) {
setValueQueue(null);
setFinishedLoading(true);
return;
}
setter(val);
setValueQueue(null);
if (page === numPages - 1 || numPages === 0) {
setFinishedLoading(true);
return;
}
setPage(prevPage => prevPage + 1);
}, [isCanceled, setter, numPages, page, identityElement]);
// Make sure that current value is processed with latest setResult function
// if this is called from within the fetchPage effect, it will have stale canceled data
useEffect(() => {
if (valueQueue) {
setResult(...valueQueue);
}
}, [valueQueue, setResult]);
// Put value returned and originating identity element into value queue
useEffect(() => {
fetchPage()
.then((val, err) => setValueQueue([identityElement, val])).catch(error => {
if (error.error !== 'input not array') {
throw error;
}
});
}, [fetchPage]);
return finishedLoading;
}
export function useIncrementalLoad(fetchData, input, pageSize, setter, identityElement, resetValue = false) {
/*
Loads all items in `input` in `pageSize` chunks.
Each input chunk is passed to `fetchData`
fetchData: (data) => Promise(). Takes subarray from `input` and returns promise.
input: array of input data for `fetchData`
pageSize: int, chunk size
setter: (data) => null. Sets paginated data on component. setter(false) clears data.
identityElement: a string identifying a invocation of this effect. When it changes, pagination and processing will restart. Old calls in processes will be dropped on landing.
resetValue: value to pass to `setter` to indicate that it should forget previous values and reset.
*/
// When input changes, creates function to fetch data by page, computes number of pages
const [fetchDataByPage, numPages] = useMemo(() => {
const fetchDataByPage = (page) => {
if (!input) {
return Promise.reject({error: "input not array", input});
}
const pagedInput = input.slice(page * pageSize, (page + 1) * pageSize);
return fetchData(pagedInput);
};
const numPages = Math.ceil(input.length / pageSize);
return [fetchDataByPage, numPages];
}, [input]);
return usePaginatedLoad(fetchDataByPage, setter, identityElement, numPages, resetValue);
}
export function useGlobalState() {
// exposes global state context along with menu_language and theme which are derived from state
const state = useContext(GlobalStateContext);
const {interfaceLanguage, textLanguage, themeStr} = state;
const menuLanguage = Sefaria.util.get_menu_language(interfaceLanguage, textLanguage);
const theme = getTheme(themeStr);
return {
...state,
menuLanguage,
theme,
};
}
export function useRtlFlexDir(lang, dir = 'row', reverse) {
// return proper flexDirection depending on if lang is RTL or not
// reverse is boolean
// dir is either 'column' or 'row'
const isRTL = lang === 'hebrew';
const langReverse = isRTL ^ reverse; // rare situation where XOR makes sense
return `${dir}${langReverse ? '-reverse' : ''}`;
}
export function useRenderersProps(onPressATag) {
/**
* Used for the `renderersProps` prop of `RenderHTML`. Currently only supports setting a-tag onPress.
* @param {*} onPressATag function to run when pressing a-tag
* @returns
*/
return useMemo(() => {
return {
a: {
onPress: (event, url) => {
onPressATag(url);
}
}
};
}, [onPressATag]);
};
export const useGetUserSettingsObj = () => {
const {emailFrequency, interfaceLanguage, preferredCustom, readingHistory} = useGlobalState();
return async () => { // returns a funtion because thats the only way to get it to work, so the caller of this hook needs to await the functions resolving.
let time_stamp = parseInt(await AsyncStorage.getItem('lastSettingsUpdateTime'));
if (!time_stamp) {
time_stamp = 0;
}
return ({
email_notifications: emailFrequency,
interface_language: interfaceLanguage,
textual_custom: preferredCustom,
reading_history: readingHistory,
time_stamp,
});
}
}
export const useIsKeyboardVisible = () => {
const [isKeyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
() => {
setKeyboardVisible(true); // or some other action
}
);
const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
() => {
setKeyboardVisible(false); // or some other action
}
);
return () => {
keyboardDidHideListener.remove();
keyboardDidShowListener.remove();
};
}, []);
return isKeyboardVisible;
}