@webority-technologies/mobile-ui 0.0.1 → 0.0.3
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/README.md +2 -1
- package/lib/commonjs/components/AuthedImage/AuthedImage.js +225 -0
- package/lib/commonjs/components/AuthedImage/authedImageCache.js +253 -0
- package/lib/commonjs/components/AuthedImage/index.js +45 -0
- package/lib/commonjs/components/BottomSheet/BottomSheet.js +10 -4
- package/lib/commonjs/components/Drawer/Drawer.js +6 -3
- package/lib/commonjs/components/ErrorBoundary/ErrorBoundary.js +136 -0
- package/lib/commonjs/components/ErrorBoundary/index.js +27 -0
- package/lib/commonjs/components/ErrorBoundary/useErrorHandler.js +24 -0
- package/lib/commonjs/components/ErrorBoundary/withErrorBoundary.js +28 -0
- package/lib/commonjs/components/FilePicker/FilePicker.js +1 -1
- package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +14 -1
- package/lib/commonjs/components/ImageGallery/ImageGallery.js +13 -0
- package/lib/commonjs/components/InlineSelect/InlineSelect.js +1 -1
- package/lib/commonjs/components/Input/Input.js +6 -0
- package/lib/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +62 -13
- package/lib/commonjs/components/LoaderOverlay/LoaderOverlay.js +203 -0
- package/lib/commonjs/components/LoaderOverlay/index.js +25 -0
- package/lib/commonjs/components/Markdown/Markdown.js +274 -0
- package/lib/commonjs/components/Markdown/index.js +13 -0
- package/lib/commonjs/components/Markdown/parse.js +229 -0
- package/lib/commonjs/components/OTPInput/OTPInput.js +5 -0
- package/lib/commonjs/components/OptionSheet/OptionSheet.js +3 -3
- package/lib/commonjs/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
- package/lib/commonjs/components/Radio/Radio.js +9 -0
- package/lib/commonjs/components/Rating/Rating.js +5 -0
- package/lib/commonjs/components/RichTextEditor/RichTextEditor.js +311 -0
- package/lib/commonjs/components/RichTextEditor/index.js +20 -0
- package/lib/commonjs/components/RichTextEditor/markdownActions.js +213 -0
- package/lib/commonjs/components/SearchBar/SearchBar.js +7 -0
- package/lib/commonjs/components/Select/Select.js +1 -1
- package/lib/commonjs/components/Skeleton/Skeleton.js +17 -36
- package/lib/commonjs/components/Slider/Slider.js +21 -5
- package/lib/commonjs/components/Swipeable/Swipeable.js +5 -2
- package/lib/commonjs/components/TimePicker/TimePicker.js +11 -0
- package/lib/commonjs/components/index.js +230 -141
- package/lib/commonjs/form/FormContext.js +13 -1
- package/lib/commonjs/form/useField.js +13 -1
- package/lib/commonjs/theme/Gradient.js +44 -41
- package/lib/commonjs/theme/index.js +24 -2
- package/lib/commonjs/utils/hapticUtils.js +38 -18
- package/lib/module/components/AuthedImage/AuthedImage.js +221 -0
- package/lib/module/components/AuthedImage/authedImageCache.js +239 -0
- package/lib/module/components/AuthedImage/index.js +5 -0
- package/lib/module/components/BottomSheet/BottomSheet.js +10 -4
- package/lib/module/components/Drawer/Drawer.js +6 -3
- package/lib/module/components/ErrorBoundary/ErrorBoundary.js +130 -0
- package/lib/module/components/ErrorBoundary/index.js +6 -0
- package/lib/module/components/ErrorBoundary/useErrorHandler.js +20 -0
- package/lib/module/components/ErrorBoundary/withErrorBoundary.js +23 -0
- package/lib/module/components/FilePicker/FilePicker.js +1 -1
- package/lib/module/components/FloatingActionButton/FloatingActionButton.js +14 -1
- package/lib/module/components/ImageGallery/ImageGallery.js +13 -0
- package/lib/module/components/InlineSelect/InlineSelect.js +1 -1
- package/lib/module/components/Input/Input.js +6 -0
- package/lib/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +63 -14
- package/lib/module/components/LoaderOverlay/LoaderOverlay.js +196 -0
- package/lib/module/components/LoaderOverlay/index.js +4 -0
- package/lib/module/components/Markdown/Markdown.js +268 -0
- package/lib/module/components/Markdown/index.js +4 -0
- package/lib/module/components/Markdown/parse.js +223 -0
- package/lib/module/components/OTPInput/OTPInput.js +5 -0
- package/lib/module/components/OptionSheet/OptionSheet.js +3 -3
- package/lib/module/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
- package/lib/module/components/Radio/Radio.js +9 -0
- package/lib/module/components/Rating/Rating.js +5 -0
- package/lib/module/components/RichTextEditor/RichTextEditor.js +306 -0
- package/lib/module/components/RichTextEditor/index.js +5 -0
- package/lib/module/components/RichTextEditor/markdownActions.js +207 -0
- package/lib/module/components/SearchBar/SearchBar.js +7 -0
- package/lib/module/components/Select/Select.js +1 -1
- package/lib/module/components/Skeleton/Skeleton.js +17 -36
- package/lib/module/components/Slider/Slider.js +21 -5
- package/lib/module/components/Swipeable/Swipeable.js +5 -2
- package/lib/module/components/TimePicker/TimePicker.js +11 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/form/FormContext.js +13 -1
- package/lib/module/form/useField.js +13 -1
- package/lib/module/theme/Gradient.js +41 -38
- package/lib/module/theme/index.js +17 -1
- package/lib/module/utils/hapticUtils.js +37 -18
- package/lib/typescript/commonjs/components/AuthedImage/AuthedImage.d.ts +22 -0
- package/lib/typescript/commonjs/components/AuthedImage/authedImageCache.d.ts +48 -0
- package/lib/typescript/commonjs/components/AuthedImage/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/Button/Button.d.ts +1 -3
- package/lib/typescript/commonjs/components/Card/Card.d.ts +1 -2
- package/lib/typescript/commonjs/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
- package/lib/typescript/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
- package/lib/typescript/commonjs/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
- package/lib/typescript/commonjs/components/LoaderOverlay/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Markdown/Markdown.d.ts +29 -0
- package/lib/typescript/commonjs/components/Markdown/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Markdown/parse.d.ts +58 -0
- package/lib/typescript/commonjs/components/OptionSheet/OptionSheet.d.ts +8 -1
- package/lib/typescript/commonjs/components/RichTextEditor/RichTextEditor.d.ts +48 -0
- package/lib/typescript/commonjs/components/RichTextEditor/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/RichTextEditor/markdownActions.d.ts +35 -0
- package/lib/typescript/commonjs/components/Skeleton/Skeleton.d.ts +1 -1
- package/lib/typescript/commonjs/components/index.d.ts +10 -0
- package/lib/typescript/commonjs/form/FormContext.d.ts +13 -3
- package/lib/typescript/commonjs/theme/Gradient.d.ts +18 -3
- package/lib/typescript/commonjs/theme/index.d.ts +16 -1
- package/lib/typescript/commonjs/theme/types.d.ts +2 -2
- package/lib/typescript/commonjs/utils/hapticUtils.d.ts +4 -0
- package/lib/typescript/module/components/AuthedImage/AuthedImage.d.ts +22 -0
- package/lib/typescript/module/components/AuthedImage/authedImageCache.d.ts +48 -0
- package/lib/typescript/module/components/AuthedImage/index.d.ts +5 -0
- package/lib/typescript/module/components/Button/Button.d.ts +1 -3
- package/lib/typescript/module/components/Card/Card.d.ts +1 -2
- package/lib/typescript/module/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
- package/lib/typescript/module/components/ErrorBoundary/index.d.ts +5 -0
- package/lib/typescript/module/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
- package/lib/typescript/module/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
- package/lib/typescript/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
- package/lib/typescript/module/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
- package/lib/typescript/module/components/LoaderOverlay/index.d.ts +3 -0
- package/lib/typescript/module/components/Markdown/Markdown.d.ts +29 -0
- package/lib/typescript/module/components/Markdown/index.d.ts +3 -0
- package/lib/typescript/module/components/Markdown/parse.d.ts +58 -0
- package/lib/typescript/module/components/OptionSheet/OptionSheet.d.ts +8 -1
- package/lib/typescript/module/components/RichTextEditor/RichTextEditor.d.ts +48 -0
- package/lib/typescript/module/components/RichTextEditor/index.d.ts +5 -0
- package/lib/typescript/module/components/RichTextEditor/markdownActions.d.ts +35 -0
- package/lib/typescript/module/components/Skeleton/Skeleton.d.ts +1 -1
- package/lib/typescript/module/components/index.d.ts +10 -0
- package/lib/typescript/module/form/FormContext.d.ts +13 -3
- package/lib/typescript/module/theme/Gradient.d.ts +18 -3
- package/lib/typescript/module/theme/index.d.ts +16 -1
- package/lib/typescript/module/theme/types.d.ts +2 -2
- package/lib/typescript/module/utils/hapticUtils.d.ts +4 -0
- package/package.json +20 -13
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ npm install @webority-technologies/mobile-ui
|
|
|
10
10
|
|
|
11
11
|
No external runtime dependencies beyond `@webority-technologies/mobile-core`. Requires
|
|
12
12
|
`react-native-gesture-handler`, `react-native-reanimated` and `react-native-safe-area-context` as
|
|
13
|
-
peers; `react-native-
|
|
13
|
+
peers; `react-native-haptic-feedback` is optional. Gradients use React Native's own
|
|
14
|
+
`experimental_backgroundImage`, so no gradient library is needed.
|
|
14
15
|
|
|
15
16
|
Full documentation lives in the repository's `docs/` directory.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = exports.AuthedImage = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _index = require("../../theme/index.js");
|
|
10
|
+
var _index2 = require("../Spinner/index.js");
|
|
11
|
+
var _authedImageCache = require("./authedImageCache.js");
|
|
12
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
|
+
const toError = value => value instanceof Error ? value : new Error(String(value));
|
|
14
|
+
const AuthedImage = props => {
|
|
15
|
+
const {
|
|
16
|
+
uri,
|
|
17
|
+
headers,
|
|
18
|
+
authenticated = true,
|
|
19
|
+
ttlMs = _authedImageCache.DEFAULT_TTL_MS,
|
|
20
|
+
placeholder,
|
|
21
|
+
fallback,
|
|
22
|
+
onLoadStart,
|
|
23
|
+
onLoadEnd,
|
|
24
|
+
onError,
|
|
25
|
+
cacheKey,
|
|
26
|
+
testID,
|
|
27
|
+
accessibilityLabel,
|
|
28
|
+
style,
|
|
29
|
+
...imageProps
|
|
30
|
+
} = props;
|
|
31
|
+
const theme = (0, _index.useTheme)();
|
|
32
|
+
const styles = (0, _react.useMemo)(() => buildStyles(theme), [theme]);
|
|
33
|
+
const [state, setState] = (0, _react.useState)({
|
|
34
|
+
status: 'loading'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Callbacks live behind refs so a caller passing inline arrows does not
|
|
38
|
+
// restart the fetch on every render.
|
|
39
|
+
const callbacks = (0, _react.useRef)({
|
|
40
|
+
onLoadStart,
|
|
41
|
+
onLoadEnd,
|
|
42
|
+
onError
|
|
43
|
+
});
|
|
44
|
+
callbacks.current = {
|
|
45
|
+
onLoadStart,
|
|
46
|
+
onLoadEnd,
|
|
47
|
+
onError
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Compared by value: a headers object literal is a new identity every render.
|
|
51
|
+
const headersKey = JSON.stringify(headers ?? {});
|
|
52
|
+
(0, _react.useEffect)(() => {
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
let acquired = null;
|
|
55
|
+
const finish = next => {
|
|
56
|
+
if (cancelled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setState(next);
|
|
60
|
+
callbacks.current.onLoadEnd?.();
|
|
61
|
+
};
|
|
62
|
+
const run = async () => {
|
|
63
|
+
if (!uri) {
|
|
64
|
+
const error = new Error('[@webority-technologies/mobile-ui] AuthedImage was given an empty `uri`. Pass the ' + 'image URL, or render nothing while the URL is still unknown.');
|
|
65
|
+
if (!cancelled) {
|
|
66
|
+
setState({
|
|
67
|
+
status: 'error'
|
|
68
|
+
});
|
|
69
|
+
callbacks.current.onError?.(error);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setState({
|
|
74
|
+
status: 'loading'
|
|
75
|
+
});
|
|
76
|
+
callbacks.current.onLoadStart?.();
|
|
77
|
+
const key = (0, _authedImageCache.deriveCacheKey)(uri, cacheKey);
|
|
78
|
+
const cached = await (0, _authedImageCache.readEntry)(key);
|
|
79
|
+
if (cancelled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (cached && (0, _authedImageCache.isFresh)(cached.meta, ttlMs)) {
|
|
83
|
+
void (0, _authedImageCache.touchEntry)(key, cached.meta);
|
|
84
|
+
finish({
|
|
85
|
+
status: 'ready',
|
|
86
|
+
dataUri: cached.dataUri
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const entry = (0, _authedImageCache.acquireFetch)(key, {
|
|
91
|
+
uri,
|
|
92
|
+
headers: JSON.parse(headersKey),
|
|
93
|
+
authenticated
|
|
94
|
+
});
|
|
95
|
+
acquired = {
|
|
96
|
+
key,
|
|
97
|
+
entry
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
const result = await entry.promise;
|
|
101
|
+
finish({
|
|
102
|
+
status: 'ready',
|
|
103
|
+
dataUri: result.dataUri
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (cancelled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// A stale image beats an empty box: the entry is only expired, and the
|
|
110
|
+
// refetch that would have replaced it failed.
|
|
111
|
+
if (cached) {
|
|
112
|
+
finish({
|
|
113
|
+
status: 'ready',
|
|
114
|
+
dataUri: cached.dataUri
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setState({
|
|
119
|
+
status: 'error'
|
|
120
|
+
});
|
|
121
|
+
callbacks.current.onError?.(toError(error));
|
|
122
|
+
callbacks.current.onLoadEnd?.();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
void run();
|
|
126
|
+
return () => {
|
|
127
|
+
cancelled = true;
|
|
128
|
+
if (acquired) {
|
|
129
|
+
(0, _authedImageCache.releaseFetch)(acquired.key, acquired.entry);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}, [uri, cacheKey, ttlMs, authenticated, headersKey]);
|
|
133
|
+
|
|
134
|
+
// The base `testID` sits on the root in EVERY state, with the state-specific
|
|
135
|
+
// suffixes nested inside it. A consumer that can only find the component while
|
|
136
|
+
// it happens to be loaded cannot wait for it to load.
|
|
137
|
+
if (state.status === 'loading') {
|
|
138
|
+
if (placeholder !== undefined) {
|
|
139
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
140
|
+
style: style,
|
|
141
|
+
testID: testID,
|
|
142
|
+
children: placeholder
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
146
|
+
style: [styles.placeholder, style],
|
|
147
|
+
testID: testID,
|
|
148
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
149
|
+
style: styles.stateFill,
|
|
150
|
+
testID: testID ? `${testID}-placeholder` : undefined,
|
|
151
|
+
pointerEvents: "none",
|
|
152
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Spinner, {
|
|
153
|
+
size: "sm",
|
|
154
|
+
accessibilityLabel: accessibilityLabel ?? 'Loading image'
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (state.status === 'error') {
|
|
160
|
+
if (fallback !== undefined) {
|
|
161
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
162
|
+
style: style,
|
|
163
|
+
testID: testID,
|
|
164
|
+
children: fallback
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
168
|
+
style: [styles.fallback, style],
|
|
169
|
+
testID: testID,
|
|
170
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
171
|
+
accessible: true,
|
|
172
|
+
accessibilityRole: "image",
|
|
173
|
+
accessibilityLabel: accessibilityLabel ? `${accessibilityLabel}, image unavailable` : 'Image unavailable',
|
|
174
|
+
testID: testID ? `${testID}-fallback` : undefined,
|
|
175
|
+
style: styles.stateFill,
|
|
176
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
177
|
+
style: styles.fallbackText,
|
|
178
|
+
children: "!"
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
184
|
+
...imageProps,
|
|
185
|
+
accessible: true,
|
|
186
|
+
accessibilityRole: "image",
|
|
187
|
+
accessibilityLabel: accessibilityLabel,
|
|
188
|
+
testID: testID,
|
|
189
|
+
style: style,
|
|
190
|
+
source: {
|
|
191
|
+
uri: state.dataUri
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
exports.AuthedImage = AuthedImage;
|
|
196
|
+
AuthedImage.displayName = 'AuthedImage';
|
|
197
|
+
const buildStyles = theme => _reactNative.StyleSheet.create({
|
|
198
|
+
placeholder: {
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
backgroundColor: theme.colors.background.secondary,
|
|
202
|
+
borderRadius: theme.radius.md
|
|
203
|
+
},
|
|
204
|
+
fallback: {
|
|
205
|
+
alignItems: 'center',
|
|
206
|
+
justifyContent: 'center',
|
|
207
|
+
backgroundColor: theme.colors.background.secondary,
|
|
208
|
+
borderRadius: theme.radius.md,
|
|
209
|
+
borderWidth: theme.colors.border.width,
|
|
210
|
+
borderColor: theme.colors.border.secondary,
|
|
211
|
+
padding: theme.spacing.sm
|
|
212
|
+
},
|
|
213
|
+
stateFill: {
|
|
214
|
+
alignItems: 'center',
|
|
215
|
+
flex: 1,
|
|
216
|
+
justifyContent: 'center'
|
|
217
|
+
},
|
|
218
|
+
fallbackText: {
|
|
219
|
+
color: theme.colors.text.tertiary,
|
|
220
|
+
fontSize: theme.typography.fontSize.lg,
|
|
221
|
+
fontWeight: theme.typography.fontWeight.bold
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
var _default = exports.default = AuthedImage;
|
|
225
|
+
//# sourceMappingURL=AuthedImage.js.map
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.touchEntry = exports.setAuthedImageCacheLimit = exports.releaseFetch = exports.readEntry = exports.isFresh = exports.getAuthedImageCacheLimit = exports.deriveCacheKey = exports.clearAuthedImageCache = exports.authedImageCacheStats = exports.acquireFetch = exports.DEFAULT_TTL_MS = void 0;
|
|
7
|
+
var _mobileCore = require("@webority-technologies/mobile-core");
|
|
8
|
+
const KEY_PREFIX = '@webority/authed-image/v1/';
|
|
9
|
+
const DATA_PREFIX = `${KEY_PREFIX}d/`;
|
|
10
|
+
const META_PREFIX = `${KEY_PREFIX}m/`;
|
|
11
|
+
const DEFAULT_BYTE_CEILING = 50 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_TTL_MS = exports.DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
|
+
let byteCeiling = DEFAULT_BYTE_CEILING;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ceiling for the total stored size of cached images. Exceeding it evicts the
|
|
17
|
+
* least recently used entries. An image cache with no ceiling grows until the
|
|
18
|
+
* OS reclaims the app's storage, which is the defect this replaces.
|
|
19
|
+
*/
|
|
20
|
+
const setAuthedImageCacheLimit = bytes => {
|
|
21
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
22
|
+
throw new Error('[@webority-technologies/mobile-ui] setAuthedImageCacheLimit() needs a positive number of ' + `bytes, received ${String(bytes)}. Pass e.g. 50 * 1024 * 1024 for 50 MB.`);
|
|
23
|
+
}
|
|
24
|
+
byteCeiling = bytes;
|
|
25
|
+
};
|
|
26
|
+
exports.setAuthedImageCacheLimit = setAuthedImageCacheLimit;
|
|
27
|
+
const getAuthedImageCacheLimit = () => byteCeiling;
|
|
28
|
+
exports.getAuthedImageCacheLimit = getAuthedImageCacheLimit;
|
|
29
|
+
const hashString = input => {
|
|
30
|
+
let h1 = 0x811c9dc5;
|
|
31
|
+
let h2 = 0x1000193;
|
|
32
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
33
|
+
const code = input.charCodeAt(i);
|
|
34
|
+
h1 = Math.imul(h1 ^ code, 16777619) >>> 0;
|
|
35
|
+
h2 = (Math.imul(h2 + code, 2654435761) ^ h2 >>> 7) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
return `${h1.toString(16)}${h2.toString(16)}${input.length.toString(16)}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hex only by construction, so it is always a safe storage key segment.
|
|
42
|
+
*/
|
|
43
|
+
const deriveCacheKey = (uri, override) => hashString(override ?? uri);
|
|
44
|
+
exports.deriveCacheKey = deriveCacheKey;
|
|
45
|
+
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
46
|
+
const encodeBase64 = buffer => {
|
|
47
|
+
const bytes = new Uint8Array(buffer);
|
|
48
|
+
let out = '';
|
|
49
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
50
|
+
const b0 = bytes[i];
|
|
51
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
|
|
52
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
|
|
53
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
54
|
+
out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
|
|
55
|
+
out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
|
|
56
|
+
out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The cache is an OPTIMISATION, so a failing store must never stop an image
|
|
63
|
+
* rendering. That used to mean guarding on whether a backend existed at all,
|
|
64
|
+
* warning once and skipping the cache; under Expo-first the backend ships with
|
|
65
|
+
* the library, so "no backend" is no longer a state an app can be in and that
|
|
66
|
+
* branch was unreachable.
|
|
67
|
+
*
|
|
68
|
+
* What remains is a store that exists and FAILS, which the per-operation error
|
|
69
|
+
* handling below already covers: each read returns null and each write returns
|
|
70
|
+
* false, the image still fetches, and nothing is logged per image.
|
|
71
|
+
*/
|
|
72
|
+
const cacheUsable = () => (0, _mobileCore.isStorageAvailable)();
|
|
73
|
+
const readEntry = async cacheKey => {
|
|
74
|
+
if (!cacheUsable()) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const values = await _mobileCore.Storage.multiGet([`${DATA_PREFIX}${cacheKey}`, `${META_PREFIX}${cacheKey}`]);
|
|
78
|
+
const dataUri = values[`${DATA_PREFIX}${cacheKey}`];
|
|
79
|
+
const rawMeta = values[`${META_PREFIX}${cacheKey}`];
|
|
80
|
+
if (!dataUri || !rawMeta) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return {
|
|
85
|
+
dataUri,
|
|
86
|
+
meta: JSON.parse(rawMeta)
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
exports.readEntry = readEntry;
|
|
93
|
+
const isFresh = (meta, ttlMs, now = Date.now()) => now - meta.fetchedAt < ttlMs;
|
|
94
|
+
exports.isFresh = isFresh;
|
|
95
|
+
const touchEntry = async (cacheKey, meta) => {
|
|
96
|
+
if (!cacheUsable()) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await _mobileCore.Storage.setItem(`${META_PREFIX}${cacheKey}`, JSON.stringify({
|
|
100
|
+
...meta,
|
|
101
|
+
lastAccessedAt: Date.now()
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
104
|
+
exports.touchEntry = touchEntry;
|
|
105
|
+
const listMeta = async () => {
|
|
106
|
+
if (!cacheUsable()) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const keys = await _mobileCore.Storage.getAllKeys();
|
|
110
|
+
const metaKeys = keys.filter(key => key.startsWith(META_PREFIX));
|
|
111
|
+
if (metaKeys.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const values = await _mobileCore.Storage.multiGet(metaKeys);
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const key of metaKeys) {
|
|
117
|
+
const raw = values[key];
|
|
118
|
+
if (!raw) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
out.push({
|
|
123
|
+
cacheKey: key.slice(META_PREFIX.length),
|
|
124
|
+
meta: JSON.parse(raw)
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
// A meta entry we cannot parse is unusable; dropping it from the ledger
|
|
128
|
+
// lets the next eviction pass remove its orphaned data entry.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
};
|
|
133
|
+
const evictIfNeeded = async () => {
|
|
134
|
+
const entries = await listMeta();
|
|
135
|
+
let total = entries.reduce((sum, entry) => sum + entry.meta.bytes, 0);
|
|
136
|
+
if (total <= byteCeiling) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const ordered = [...entries].sort((a, b) => a.meta.lastAccessedAt - b.meta.lastAccessedAt);
|
|
140
|
+
const doomed = [];
|
|
141
|
+
for (const entry of ordered) {
|
|
142
|
+
if (total <= byteCeiling) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
doomed.push(`${DATA_PREFIX}${entry.cacheKey}`, `${META_PREFIX}${entry.cacheKey}`);
|
|
146
|
+
total -= entry.meta.bytes;
|
|
147
|
+
}
|
|
148
|
+
if (doomed.length > 0) {
|
|
149
|
+
await _mobileCore.Storage.multiRemove(doomed);
|
|
150
|
+
_mobileCore.Logger.info(`[AuthedImage] evicted ${doomed.length / 2} cached image(s) over the byte ceiling`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const clearAuthedImageCache = async () => {
|
|
154
|
+
if (!cacheUsable()) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const keys = await _mobileCore.Storage.getAllKeys();
|
|
158
|
+
const ours = keys.filter(key => key.startsWith(KEY_PREFIX));
|
|
159
|
+
if (ours.length > 0) {
|
|
160
|
+
await _mobileCore.Storage.multiRemove(ours);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
exports.clearAuthedImageCache = clearAuthedImageCache;
|
|
164
|
+
const authedImageCacheStats = async () => {
|
|
165
|
+
const entries = await listMeta();
|
|
166
|
+
return {
|
|
167
|
+
entries: entries.length,
|
|
168
|
+
bytes: entries.reduce((sum, entry) => sum + entry.meta.bytes, 0)
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
exports.authedImageCacheStats = authedImageCacheStats;
|
|
172
|
+
const fetchAndStore = async (cacheKey, options, signal) => {
|
|
173
|
+
const headers = {
|
|
174
|
+
...options.headers
|
|
175
|
+
};
|
|
176
|
+
if (options.authenticated) {
|
|
177
|
+
const stored = await (0, _mobileCore.getToken)(_mobileCore.ACCESS_TOKEN_KEY);
|
|
178
|
+
if (stored?.token) {
|
|
179
|
+
headers.Authorization = `Bearer ${stored.token}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const response = await fetch(options.uri, {
|
|
183
|
+
headers,
|
|
184
|
+
signal
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`[@webority-technologies/mobile-ui] AuthedImage could not load "${options.uri}": the server ` + `answered ${response.status}. Check the URL and whether the request needs a bearer token ` + '(pass authenticated={false} for public media).');
|
|
188
|
+
}
|
|
189
|
+
const buffer = await response.arrayBuffer();
|
|
190
|
+
const rawType = response.headers?.get?.('content-type');
|
|
191
|
+
const contentType = (rawType ? rawType.split(';')[0].trim() : '') || 'image/jpeg';
|
|
192
|
+
const dataUri = `data:${contentType};base64,${encodeBase64(buffer)}`;
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
// `bytes` measures the stored data URI, not the decoded image: the ceiling
|
|
195
|
+
// exists to bound what sits in storage, and base64 costs about a third more.
|
|
196
|
+
const meta = {
|
|
197
|
+
fetchedAt: now,
|
|
198
|
+
bytes: dataUri.length,
|
|
199
|
+
contentType,
|
|
200
|
+
lastAccessedAt: now
|
|
201
|
+
};
|
|
202
|
+
if (cacheUsable()) {
|
|
203
|
+
await _mobileCore.Storage.multiSet({
|
|
204
|
+
[`${DATA_PREFIX}${cacheKey}`]: dataUri,
|
|
205
|
+
[`${META_PREFIX}${cacheKey}`]: JSON.stringify(meta)
|
|
206
|
+
});
|
|
207
|
+
await evictIfNeeded();
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
dataUri,
|
|
211
|
+
meta
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const inFlight = new Map();
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Two mounts of the same image share one request. The controller is aborted
|
|
218
|
+
* only when the last waiter releases, so one unmount cannot cancel the fetch
|
|
219
|
+
* another mount is still waiting on.
|
|
220
|
+
*/
|
|
221
|
+
const acquireFetch = (cacheKey, options) => {
|
|
222
|
+
const existing = inFlight.get(cacheKey);
|
|
223
|
+
if (existing) {
|
|
224
|
+
existing.waiters += 1;
|
|
225
|
+
return existing;
|
|
226
|
+
}
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const entry = {
|
|
229
|
+
controller,
|
|
230
|
+
waiters: 1,
|
|
231
|
+
promise: Promise.resolve()
|
|
232
|
+
};
|
|
233
|
+
entry.promise = fetchAndStore(cacheKey, options, controller.signal).finally(() => {
|
|
234
|
+
if (inFlight.get(cacheKey) === entry) {
|
|
235
|
+
inFlight.delete(cacheKey);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
inFlight.set(cacheKey, entry);
|
|
239
|
+
return entry;
|
|
240
|
+
};
|
|
241
|
+
exports.acquireFetch = acquireFetch;
|
|
242
|
+
const releaseFetch = (cacheKey, entry) => {
|
|
243
|
+
entry.waiters -= 1;
|
|
244
|
+
if (entry.waiters > 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (inFlight.get(cacheKey) === entry) {
|
|
248
|
+
inFlight.delete(cacheKey);
|
|
249
|
+
}
|
|
250
|
+
entry.controller.abort();
|
|
251
|
+
};
|
|
252
|
+
exports.releaseFetch = releaseFetch;
|
|
253
|
+
//# sourceMappingURL=authedImageCache.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "AuthedImage", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _AuthedImage.AuthedImage;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "authedImageCacheStats", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _authedImageCache.authedImageCacheStats;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "clearAuthedImageCache", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _authedImageCache.clearAuthedImageCache;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "default", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _AuthedImage.default;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "getAuthedImageCacheLimit", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _authedImageCache.getAuthedImageCacheLimit;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "setAuthedImageCacheLimit", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _authedImageCache.setAuthedImageCacheLimit;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
var _AuthedImage = _interopRequireWildcard(require("./AuthedImage.js"));
|
|
43
|
+
var _authedImageCache = require("./authedImageCache.js");
|
|
44
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -126,6 +126,12 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
126
126
|
|
|
127
127
|
// Convert a snap-point index → translateY position. -1 = closed.
|
|
128
128
|
const yForIndex = (0, _react.useCallback)(idx => {
|
|
129
|
+
// NOT dead code, despite every ordinary call site clamping first.
|
|
130
|
+
// `expand()` has no empty-snapPoints guard (unlike `collapse`), so with
|
|
131
|
+
// snapPoints={[]} it arrives here out of range — and without this,
|
|
132
|
+
// `screenHeight - resolvedSnapPoints[idx]` is `screenHeight - undefined`,
|
|
133
|
+
// i.e. NaN into the sheet position, which breaks layout silently rather
|
|
134
|
+
// than degrading. Covered by the "degenerate snapPoints" tests.
|
|
129
135
|
if (idx < 0 || idx >= resolvedSnapPoints.length) {
|
|
130
136
|
return closedY;
|
|
131
137
|
}
|
|
@@ -262,7 +268,7 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
262
268
|
const closed = closedY;
|
|
263
269
|
const top = minTopY;
|
|
264
270
|
const canPanDownToClose = enablePanDownToClose;
|
|
265
|
-
return _reactNativeGestureHandler.Gesture.Pan().activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
|
|
271
|
+
return _reactNativeGestureHandler.Gesture.Pan().withTestId(testID ? `${testID}-pan-gesture` : 'bottom-sheet-pan-gesture').activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
|
|
266
272
|
'worklet';
|
|
267
273
|
|
|
268
274
|
dragStartY.value = translateY.value;
|
|
@@ -312,7 +318,7 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
312
318
|
}
|
|
313
319
|
(0, _reactNativeReanimated.runOnJS)(handleSnapEnd)(nearestIdx, targets[nearestIdx]);
|
|
314
320
|
});
|
|
315
|
-
}, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, translateY]);
|
|
321
|
+
}, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, testID, translateY]);
|
|
316
322
|
|
|
317
323
|
// ───────── Keyboard tracking ─────────
|
|
318
324
|
// keyboardOffset is added to translateY in sheetStyle so we don't disturb
|
|
@@ -384,14 +390,14 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
384
390
|
// gesture-handler/reanimated sheet doesn't fire reliably on RN 0.85 / Fabric
|
|
385
391
|
// (the gesture system swallows the touch sequence). gorhom/bottom-sheet uses
|
|
386
392
|
// the same Gesture.Tap() pattern for the same reason.
|
|
387
|
-
const backdropTapGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Tap().maxDuration(250).onEnd((_e, success) => {
|
|
393
|
+
const backdropTapGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Tap().withTestId(testID ? `${testID}-backdrop-gesture` : 'bottom-sheet-backdrop-gesture').maxDuration(250).onEnd((_e, success) => {
|
|
388
394
|
'worklet';
|
|
389
395
|
|
|
390
396
|
if (!success) {
|
|
391
397
|
return;
|
|
392
398
|
}
|
|
393
399
|
(0, _reactNativeReanimated.runOnJS)(handleBackdropPress)();
|
|
394
|
-
}), [handleBackdropPress]);
|
|
400
|
+
}), [handleBackdropPress, testID]);
|
|
395
401
|
const contextValue = (0, _react.useMemo)(() => ({
|
|
396
402
|
snapIndex: currentIndex,
|
|
397
403
|
snapPoints: resolvedSnapPoints,
|
|
@@ -82,8 +82,12 @@ const Drawer = exports.Drawer = /*#__PURE__*/(0, _react.forwardRef)((props, ref)
|
|
|
82
82
|
return sheetSize;
|
|
83
83
|
}
|
|
84
84
|
}, [side, sheetSize]);
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
// Uncontrolled defaultVisible starts already open — the offset must start at the
|
|
87
|
+
// open position (0) too, or the sheet is logically open while visually still
|
|
88
|
+
// pinned at the closed edge until the next explicit open()/close()/drag.
|
|
89
|
+
const offset = (0, _reactNativeReanimated.useSharedValue)(defaultVisible ? 0 : closedOffset);
|
|
90
|
+
const dragStart = (0, _reactNativeReanimated.useSharedValue)(defaultVisible ? 0 : closedOffset);
|
|
87
91
|
const [isOpen, setIsOpen] = (0, _react.useState)(defaultVisible);
|
|
88
92
|
const isOpenShared = (0, _reactNativeReanimated.useSharedValue)(defaultVisible);
|
|
89
93
|
|
|
@@ -335,7 +339,6 @@ const Drawer = exports.Drawer = /*#__PURE__*/(0, _react.forwardRef)((props, ref)
|
|
|
335
339
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
336
340
|
accessibilityRole: "alert",
|
|
337
341
|
accessibilityLabel: accessibilityLabel ?? 'Drawer',
|
|
338
|
-
accessibilityViewIsModal: isOpen,
|
|
339
342
|
style: [sideLayout, {
|
|
340
343
|
backgroundColor: theme.colors.background.elevated,
|
|
341
344
|
...theme.shadows.xl
|