@momo-kits/foundation 0.167.1-duo.2 → 0.167.1-duo.4
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/Application/BottomSheet.tsx +111 -10
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +462 -0
- package/Application/BottomTab/index.tsx +145 -123
- package/Application/Components/DuoGlassBackground.tsx +179 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderLeft.tsx +8 -2
- package/Application/Components/HeaderRight.tsx +47 -2
- package/Application/Components/OptionalGlassView.ts +11 -0
- package/Application/ModalScreen.tsx +14 -0
- package/Application/NavigationContainer.tsx +14 -11
- package/Application/device.ts +29 -0
- package/Application/types.ts +5 -0
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +187 -32
- package/Layout/utils.ts +76 -8
- package/Popup/PopupNotify.tsx +31 -20
- package/Popup/PopupPromotion.tsx +20 -8
- package/Popup/utils.ts +32 -0
- package/package.json +1 -1
|
@@ -27,6 +27,8 @@ import StackScreen from '../StackScreen';
|
|
|
27
27
|
import { BottomTabProps } from '../types';
|
|
28
28
|
import { getOptions, getStackOptions, useAppState } from '../utils';
|
|
29
29
|
import BottomTabBar from './BottomTabBar';
|
|
30
|
+
import { DuoTabBar } from './DuoTabBar';
|
|
31
|
+
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
30
32
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
31
33
|
import { useScreenLayout } from '../../Layout';
|
|
32
34
|
|
|
@@ -204,8 +206,10 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
204
206
|
navigation,
|
|
205
207
|
initialRouteName,
|
|
206
208
|
floatingButton,
|
|
209
|
+
compressionBehavior,
|
|
207
210
|
}) => {
|
|
208
211
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
212
|
+
const { isIPhoneDuo } = useScreenLayout();
|
|
209
213
|
const dimensions = useWindowDimensions();
|
|
210
214
|
const insets = useSafeAreaInsets();
|
|
211
215
|
const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
|
|
@@ -279,138 +283,156 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
279
283
|
};
|
|
280
284
|
|
|
281
285
|
return (
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
headerBackground: undefined,
|
|
287
|
-
tabBarLabelPosition: 'below-icon',
|
|
288
|
-
tabBarBackground: () => (
|
|
289
|
-
<View
|
|
290
|
-
style={[
|
|
291
|
-
styles.container,
|
|
292
|
-
{
|
|
293
|
-
backgroundColor: theme.colors.background.surface,
|
|
294
|
-
},
|
|
295
|
-
]}
|
|
296
|
-
>
|
|
297
|
-
<Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
|
|
298
|
-
<View
|
|
299
|
-
style={[
|
|
300
|
-
styles.indicator,
|
|
301
|
-
{ backgroundColor: theme.colors.primary },
|
|
302
|
-
]}
|
|
303
|
-
/>
|
|
304
|
-
</Animated.View>
|
|
305
|
-
</View>
|
|
306
|
-
),
|
|
307
|
-
}}
|
|
308
|
-
tabBar={props => (
|
|
309
|
-
<BottomTabBar
|
|
310
|
-
{...props}
|
|
311
|
-
floatingButton={floatingButton}
|
|
312
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
313
|
-
activeTintColor={theme.colors.primary}
|
|
314
|
-
style={[
|
|
315
|
-
{
|
|
316
|
-
height:
|
|
317
|
-
64 +
|
|
318
|
-
(Platform.OS === 'ios'
|
|
319
|
-
? Math.min(insets.bottom, 21)
|
|
320
|
-
: insets.bottom),
|
|
321
|
-
},
|
|
322
|
-
]}
|
|
323
|
-
/>
|
|
324
|
-
)}
|
|
325
|
-
initialRouteName={initialRouteName}
|
|
286
|
+
<DuoBarCompressionProvider
|
|
287
|
+
behavior={compressionBehavior}
|
|
288
|
+
tabCount={tabs.length}
|
|
289
|
+
hasQr={!!floatingButton}
|
|
326
290
|
>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
291
|
+
<Tab.Navigator
|
|
292
|
+
screenOptions={{
|
|
293
|
+
headerShown: false,
|
|
294
|
+
headerTransparent: true,
|
|
295
|
+
headerBackground: undefined,
|
|
296
|
+
tabBarLabelPosition: 'below-icon',
|
|
297
|
+
tabBarBackground: () => (
|
|
298
|
+
<View
|
|
299
|
+
style={[
|
|
300
|
+
styles.container,
|
|
301
|
+
{
|
|
302
|
+
backgroundColor: theme.colors.background.surface,
|
|
303
|
+
},
|
|
304
|
+
]}
|
|
305
|
+
>
|
|
306
|
+
<Animated.View
|
|
307
|
+
style={[styles.indicatorContainer, indicatorStyle]}
|
|
308
|
+
>
|
|
309
|
+
<View
|
|
310
|
+
style={[
|
|
311
|
+
styles.indicator,
|
|
312
|
+
{ backgroundColor: theme.colors.primary },
|
|
313
|
+
]}
|
|
314
|
+
/>
|
|
315
|
+
</Animated.View>
|
|
316
|
+
</View>
|
|
317
|
+
),
|
|
318
|
+
}}
|
|
319
|
+
tabBar={props =>
|
|
320
|
+
isIPhoneDuo ? (
|
|
321
|
+
<DuoTabBar
|
|
322
|
+
{...props}
|
|
323
|
+
floatingButton={floatingButton}
|
|
324
|
+
inactiveTintColor={theme.colors.text.default}
|
|
325
|
+
activeTintColor={theme.colors.primary}
|
|
326
|
+
/>
|
|
327
|
+
) : (
|
|
328
|
+
<BottomTabBar
|
|
329
|
+
{...props}
|
|
330
|
+
floatingButton={floatingButton}
|
|
331
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
332
|
+
activeTintColor={theme.colors.primary}
|
|
333
|
+
style={[
|
|
334
|
+
{
|
|
335
|
+
height:
|
|
336
|
+
64 +
|
|
337
|
+
(Platform.OS === 'ios'
|
|
338
|
+
? Math.min(insets.bottom, 21)
|
|
339
|
+
: insets.bottom),
|
|
340
|
+
},
|
|
341
|
+
]}
|
|
342
|
+
/>
|
|
343
|
+
)
|
|
335
344
|
}
|
|
345
|
+
initialRouteName={initialRouteName}
|
|
346
|
+
>
|
|
347
|
+
{tabs.map((item, index) => {
|
|
348
|
+
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
349
|
+
const isDot =
|
|
350
|
+
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
351
|
+
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
336
352
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
width?: number;
|
|
342
|
-
} = {
|
|
343
|
-
marginRight: -8,
|
|
344
|
-
top: -4,
|
|
345
|
-
backgroundColor: Colors.orange_03,
|
|
346
|
-
};
|
|
353
|
+
let badgeLabel = item?.badgeLabel;
|
|
354
|
+
if (isNumZero) {
|
|
355
|
+
badgeLabel = undefined;
|
|
356
|
+
}
|
|
347
357
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
let badgeStyle: {
|
|
359
|
+
marginRight: number;
|
|
360
|
+
top: number;
|
|
361
|
+
backgroundColor: string;
|
|
362
|
+
width?: number;
|
|
363
|
+
} = {
|
|
364
|
+
marginRight: -8,
|
|
351
365
|
top: -4,
|
|
352
|
-
backgroundColor: Colors.red_03,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
if (isDot) {
|
|
356
|
-
badgeStyle = {
|
|
357
|
-
marginRight: -2,
|
|
358
|
-
top: -2,
|
|
359
366
|
backgroundColor: Colors.orange_03,
|
|
360
|
-
width: 12,
|
|
361
367
|
};
|
|
362
|
-
}
|
|
363
368
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
alignSelf: 'center',
|
|
401
|
-
fontFamily: exportFontFamily('bold'),
|
|
369
|
+
if (isNum) {
|
|
370
|
+
badgeStyle = {
|
|
371
|
+
marginRight: -4,
|
|
372
|
+
top: -4,
|
|
373
|
+
backgroundColor: Colors.red_03,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (isDot) {
|
|
377
|
+
badgeStyle = {
|
|
378
|
+
marginRight: -2,
|
|
379
|
+
top: -2,
|
|
380
|
+
backgroundColor: Colors.orange_03,
|
|
381
|
+
width: 12,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<Tab.Screen
|
|
387
|
+
key={`BottomTab-${item.name}`}
|
|
388
|
+
name={item.name}
|
|
389
|
+
component={TabScreen}
|
|
390
|
+
initialParams={{ ...item, nested, index }}
|
|
391
|
+
listeners={handler}
|
|
392
|
+
options={{
|
|
393
|
+
tabBarLabel: ({ focused, color }) => {
|
|
394
|
+
return (
|
|
395
|
+
<Text
|
|
396
|
+
numberOfLines={1}
|
|
397
|
+
typography={
|
|
398
|
+
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
399
|
+
}
|
|
400
|
+
color={color}
|
|
401
|
+
>
|
|
402
|
+
{item.label}
|
|
403
|
+
</Text>
|
|
404
|
+
);
|
|
402
405
|
},
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
title: item.label,
|
|
407
|
+
tabBarStyle: [
|
|
408
|
+
{
|
|
409
|
+
height: 64,
|
|
410
|
+
},
|
|
411
|
+
styles.tabStyle,
|
|
412
|
+
],
|
|
413
|
+
tabBarBadge: badgeLabel,
|
|
414
|
+
tabBarBadgeStyle: [
|
|
415
|
+
{
|
|
416
|
+
borderColor: 'white',
|
|
417
|
+
borderWidth: 2,
|
|
418
|
+
fontSize: 10,
|
|
419
|
+
lineHeight: 14,
|
|
420
|
+
fontWeight: 'bold',
|
|
421
|
+
alignSelf: 'center',
|
|
422
|
+
fontFamily: exportFontFamily('bold'),
|
|
423
|
+
},
|
|
424
|
+
badgeStyle,
|
|
425
|
+
],
|
|
426
|
+
tabBarIcon: ({ color }) => (
|
|
427
|
+
<Icon source={item.icon} color={color} size={28} />
|
|
428
|
+
),
|
|
429
|
+
...item,
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
432
|
+
);
|
|
433
|
+
})}
|
|
434
|
+
</Tab.Navigator>
|
|
435
|
+
</DuoBarCompressionProvider>
|
|
414
436
|
);
|
|
415
437
|
};
|
|
416
438
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { useContext, useState } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
import Animated, {
|
|
5
|
+
Extrapolation,
|
|
6
|
+
interpolate,
|
|
7
|
+
runOnJS,
|
|
8
|
+
useAnimatedReaction,
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { ApplicationContext } from '../../Context';
|
|
12
|
+
import { Shadow } from '../../Consts';
|
|
13
|
+
import { Image } from '../../Image';
|
|
14
|
+
import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
|
|
15
|
+
import { GlassView } from './OptionalGlassView';
|
|
16
|
+
|
|
17
|
+
// extended headers run their gradient this far below the header, as off Duo
|
|
18
|
+
const EXTENDED_GRADIENT_EXTRA = 52;
|
|
19
|
+
// scroll distance over which the extended gradient collapses to the header
|
|
20
|
+
const EXTENDED_COLLAPSE_DISTANCE = 100;
|
|
21
|
+
// scroll distance over which the bar material fades in
|
|
22
|
+
const FADE_DISTANCE = 16;
|
|
23
|
+
const TRANSLUCENT_ALPHA = 'CC';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* iPhone Duo header background: glass running edge to edge, behind the status bar
|
|
27
|
+
* column too, shown once content scrolls under it
|
|
28
|
+
*/
|
|
29
|
+
const DuoGlassBackground: React.FC<{
|
|
30
|
+
animatedValue?: AnimatedCompatValue;
|
|
31
|
+
// scroll offset where the bar starts to show, e.g. below an animated banner
|
|
32
|
+
fadeStart?: number;
|
|
33
|
+
}> = ({ animatedValue, fadeStart = 0 }) => {
|
|
34
|
+
const { theme } = useContext(ApplicationContext);
|
|
35
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
36
|
+
const [scrolled, setScrolled] = useState(false);
|
|
37
|
+
|
|
38
|
+
useAnimatedReaction(
|
|
39
|
+
() => sharedValue.value > fadeStart,
|
|
40
|
+
(isScrolled, wasScrolled) => {
|
|
41
|
+
if (GlassView && isScrolled !== wasScrolled) {
|
|
42
|
+
runOnJS(setScrolled)(isScrolled);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
[fadeStart],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const fadeStyle = useAnimatedStyle(() => ({
|
|
49
|
+
opacity: interpolate(
|
|
50
|
+
sharedValue.value,
|
|
51
|
+
[fadeStart, fadeStart + FADE_DISTANCE],
|
|
52
|
+
[0, 1],
|
|
53
|
+
Extrapolation.CLAMP,
|
|
54
|
+
),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View pointerEvents={'none'} style={StyleSheet.absoluteFill}>
|
|
59
|
+
{GlassView ? (
|
|
60
|
+
// UIVisualEffectView breaks under alpha, so glass toggles its effect instead
|
|
61
|
+
<GlassView
|
|
62
|
+
effect={scrolled ? 'glass' : 'none'}
|
|
63
|
+
useShadow
|
|
64
|
+
style={StyleSheet.absoluteFill}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<Animated.View
|
|
68
|
+
style={[
|
|
69
|
+
StyleSheet.absoluteFill,
|
|
70
|
+
styles.translucentBar,
|
|
71
|
+
{
|
|
72
|
+
backgroundColor:
|
|
73
|
+
theme.colors.background.surface + TRANSLUCENT_ALPHA,
|
|
74
|
+
borderBottomColor: theme.colors.border.default,
|
|
75
|
+
},
|
|
76
|
+
fadeStyle,
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* pink header gradient drawn under the content, fading out on scroll as main's extended
|
|
86
|
+
* header does: header-high for default headers, 52 taller (collapsing) for extended
|
|
87
|
+
*/
|
|
88
|
+
const DuoScrollGradient: React.FC<{
|
|
89
|
+
animatedValue?: AnimatedCompatValue;
|
|
90
|
+
gradientColor?: string;
|
|
91
|
+
headerBackground?: string;
|
|
92
|
+
heightHeader: number;
|
|
93
|
+
extended?: boolean;
|
|
94
|
+
// quick return header: the gradient leaves with the content instead of fading
|
|
95
|
+
followScroll?: boolean;
|
|
96
|
+
}> = ({
|
|
97
|
+
animatedValue,
|
|
98
|
+
gradientColor,
|
|
99
|
+
headerBackground,
|
|
100
|
+
heightHeader,
|
|
101
|
+
extended,
|
|
102
|
+
followScroll,
|
|
103
|
+
}) => {
|
|
104
|
+
const { theme } = useContext(ApplicationContext);
|
|
105
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
106
|
+
const color = gradientColor ?? theme.colors.gradient;
|
|
107
|
+
const image = headerBackground ?? theme.assets?.headerBackground;
|
|
108
|
+
|
|
109
|
+
const scrollStyle = useAnimatedStyle(() => {
|
|
110
|
+
if (followScroll) {
|
|
111
|
+
return {
|
|
112
|
+
height: heightHeader + (extended ? EXTENDED_GRADIENT_EXTRA : 0),
|
|
113
|
+
transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
height: extended
|
|
118
|
+
? interpolate(
|
|
119
|
+
sharedValue.value,
|
|
120
|
+
[0, EXTENDED_COLLAPSE_DISTANCE],
|
|
121
|
+
[heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
|
|
122
|
+
Extrapolation.CLAMP,
|
|
123
|
+
)
|
|
124
|
+
: heightHeader,
|
|
125
|
+
opacity: interpolate(
|
|
126
|
+
sharedValue.value,
|
|
127
|
+
[0, EXTENDED_GRADIENT_EXTRA],
|
|
128
|
+
[1, 0],
|
|
129
|
+
Extrapolation.CLAMP,
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!color) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Animated.View
|
|
140
|
+
pointerEvents={'none'}
|
|
141
|
+
style={[styles.gradient, scrollStyle]}
|
|
142
|
+
>
|
|
143
|
+
<LinearGradient
|
|
144
|
+
colors={[color, color + '00']}
|
|
145
|
+
style={StyleSheet.absoluteFill}
|
|
146
|
+
>
|
|
147
|
+
{!!image && (
|
|
148
|
+
<Image
|
|
149
|
+
style={styles.gradientFill}
|
|
150
|
+
source={{ uri: image }}
|
|
151
|
+
loading={false}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</LinearGradient>
|
|
155
|
+
</Animated.View>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const styles = StyleSheet.create({
|
|
160
|
+
translucentBar: {
|
|
161
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
162
|
+
...Shadow.Light,
|
|
163
|
+
},
|
|
164
|
+
gradient: {
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
top: 0,
|
|
167
|
+
left: 0,
|
|
168
|
+
right: 0,
|
|
169
|
+
overflow: 'hidden',
|
|
170
|
+
},
|
|
171
|
+
// banner image keeps its 375:154 shape, the gradient box clips it
|
|
172
|
+
gradientFill: {
|
|
173
|
+
width: '100%',
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
aspectRatio: 375 / 154,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export { DuoGlassBackground, DuoScrollGradient };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { StackHeaderProps, Header } from '@react-navigation/stack';
|
|
3
|
+
import Animated, {
|
|
4
|
+
SharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
// how far the header is pushed above the top edge
|
|
9
|
+
const DuoHeaderOffsetContext = createContext<SharedValue<number> | undefined>(
|
|
10
|
+
undefined,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* iPhone Duo header that leaves with the content and comes back when scrolling back
|
|
15
|
+
*/
|
|
16
|
+
const DuoQuickReturnHeader: React.FC<
|
|
17
|
+
StackHeaderProps & { offset: SharedValue<number> }
|
|
18
|
+
> = ({ offset, ...props }) => {
|
|
19
|
+
const style = useAnimatedStyle(() => ({
|
|
20
|
+
transform: [{ translateY: -offset.value }],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<DuoHeaderOffsetContext.Provider value={offset}>
|
|
25
|
+
<Animated.View pointerEvents={'box-none'} style={style}>
|
|
26
|
+
<Header {...props} />
|
|
27
|
+
</Animated.View>
|
|
28
|
+
</DuoHeaderOffsetContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* side bar controls render inside the header; this cancels its offset so they stay put
|
|
34
|
+
*/
|
|
35
|
+
const useSideBarCounterStyle = () => {
|
|
36
|
+
const offset = useContext(DuoHeaderOffsetContext);
|
|
37
|
+
return useAnimatedStyle(() => ({
|
|
38
|
+
transform: [{ translateY: offset ? offset.value : 0 }],
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { DuoQuickReturnHeader, useSideBarCounterStyle };
|
|
@@ -5,7 +5,9 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
|
5
5
|
import { PopupNotify } from '../../Popup';
|
|
6
6
|
import { NavigationButton } from './NavigationButton';
|
|
7
7
|
import { SideBarButton } from './SideBarButton';
|
|
8
|
+
import Animated from 'react-native-reanimated';
|
|
8
9
|
import { useSideBarItemStyle } from '../../Layout/utils';
|
|
10
|
+
import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* default header left used for nav
|
|
@@ -20,6 +22,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
|
|
|
20
22
|
const context = useContext<any>(MiniAppContext);
|
|
21
23
|
const { navigator } = useContext(ApplicationContext);
|
|
22
24
|
const sideBarStyle = useSideBarItemStyle('left', 0);
|
|
25
|
+
const counterStyle = useSideBarCounterStyle();
|
|
23
26
|
|
|
24
27
|
const goBack = useCallback(() => {
|
|
25
28
|
const canGoBack = navigator?.ref?.current?.canGoBack?.();
|
|
@@ -78,14 +81,17 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
|
|
|
78
81
|
|
|
79
82
|
if (floating && sideBarStyle) {
|
|
80
83
|
return (
|
|
81
|
-
<View
|
|
84
|
+
<Animated.View
|
|
85
|
+
style={[sideBarStyle, counterStyle]}
|
|
86
|
+
pointerEvents={'box-none'}
|
|
87
|
+
>
|
|
82
88
|
<SideBarButton
|
|
83
89
|
accessibilityLabel="btn_navigation_back"
|
|
84
90
|
icon="arrow-back"
|
|
85
91
|
tintColor={tintColor}
|
|
86
92
|
onPress={goBackSafe}
|
|
87
93
|
/>
|
|
88
|
-
</View>
|
|
94
|
+
</Animated.View>
|
|
89
95
|
);
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -13,8 +13,12 @@ import { PopupNotify } from '../../Popup';
|
|
|
13
13
|
import { Tool, ToolGroup } from '../types';
|
|
14
14
|
import { Icon } from '../../Icon';
|
|
15
15
|
import { Text } from '../../Text';
|
|
16
|
+
import Animated from 'react-native-reanimated';
|
|
16
17
|
import { useSideBarItemStyle } from '../../Layout/utils';
|
|
18
|
+
import { useSideBarCounterStyle } from './DuoQuickReturnHeader';
|
|
17
19
|
import { SideBarButton, SideBarGroup, SideBarGroupItem } from './SideBarButton';
|
|
20
|
+
import { useDuoBarCompression } from '../BottomTab/DuoBarCompression';
|
|
21
|
+
import { DuoMetrics } from '../../Layout/utils';
|
|
18
22
|
|
|
19
23
|
const DID_SYNC_NEW_HOME = 'did_sync_new_home';
|
|
20
24
|
|
|
@@ -26,6 +30,7 @@ const HeaderRight: React.FC<any> = props => {
|
|
|
26
30
|
const { headerRight = {}, floating = false } = props;
|
|
27
31
|
const { useOnBoarding = false, buttonOnBoarding, onPress } = headerRight;
|
|
28
32
|
const sideBarStyle = useSideBarItemStyle('right', 1);
|
|
33
|
+
const counterStyle = useSideBarCounterStyle();
|
|
29
34
|
|
|
30
35
|
if (typeof props.headerRight === 'function') {
|
|
31
36
|
return props.headerRight(props);
|
|
@@ -43,13 +48,16 @@ const HeaderRight: React.FC<any> = props => {
|
|
|
43
48
|
|
|
44
49
|
if (floating && sideBarStyle) {
|
|
45
50
|
return (
|
|
46
|
-
<View
|
|
51
|
+
<Animated.View
|
|
52
|
+
style={[sideBarStyle, counterStyle]}
|
|
53
|
+
pointerEvents={'box-none'}
|
|
54
|
+
>
|
|
47
55
|
<HeaderToolkitAction
|
|
48
56
|
{...props.headerRight}
|
|
49
57
|
tintColor={props.tintColor}
|
|
50
58
|
vertical
|
|
51
59
|
/>
|
|
52
|
-
</View>
|
|
60
|
+
</Animated.View>
|
|
53
61
|
);
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -75,6 +83,8 @@ const HeaderToolkitAction: React.FC<any> = ({
|
|
|
75
83
|
}) => {
|
|
76
84
|
const { navigator } = useContext(ApplicationContext);
|
|
77
85
|
const context = useContext<any>(MiniAppContext);
|
|
86
|
+
const { toolbarCompressed, toolbarHidden, expandToolbar, setToolbarHeight } =
|
|
87
|
+
useDuoBarCompression();
|
|
78
88
|
|
|
79
89
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
|
80
90
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -281,6 +291,41 @@ const HeaderToolkitAction: React.FC<any> = ({
|
|
|
281
291
|
? '16_navigation_close_circle'
|
|
282
292
|
: '16_basic_home';
|
|
283
293
|
|
|
294
|
+
const groupCount = useHelpCenter ? 2 : 1;
|
|
295
|
+
// Back slot, the optional shortcut, then the help/home capsule
|
|
296
|
+
const fullToolbarHeight =
|
|
297
|
+
DuoMetrics.controlSize +
|
|
298
|
+
Spacing.M +
|
|
299
|
+
(useShortcut ? DuoMetrics.controlSize + Spacing.M : 0) +
|
|
300
|
+
(groupCount > 1
|
|
301
|
+
? groupCount * DuoMetrics.itemPitch
|
|
302
|
+
: DuoMetrics.controlSize);
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (vertical) {
|
|
306
|
+
setToolbarHeight?.(fullToolbarHeight);
|
|
307
|
+
}
|
|
308
|
+
}, [vertical, fullToolbarHeight, setToolbarHeight]);
|
|
309
|
+
|
|
310
|
+
// an open tab rail takes this slot for a moment
|
|
311
|
+
if (vertical && toolbarHidden) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// HIG "Toolbar compressed": tools fold into one button; tapping it expands the
|
|
316
|
+
// toolbar and compresses the tab bar instead. Not the useMore icon, to tell them apart
|
|
317
|
+
if (vertical && toolbarCompressed) {
|
|
318
|
+
return (
|
|
319
|
+
<SideBarButton
|
|
320
|
+
accessibilityLabel={'btn_navigation_toolbar'}
|
|
321
|
+
icon={'navigation_menu'}
|
|
322
|
+
tintColor={tintColor}
|
|
323
|
+
showBadge={showBadge}
|
|
324
|
+
onPress={expandToolbar}
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
284
329
|
if (vertical) {
|
|
285
330
|
return (
|
|
286
331
|
<View style={styles.sideBarColumn}>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
// native glass is optional so apps without @momo-kits/glass-view still bundle
|
|
4
|
+
let GlassView: React.ComponentType<any> | undefined;
|
|
5
|
+
try {
|
|
6
|
+
GlassView = require('@momo-kits/glass-view').GlassView;
|
|
7
|
+
} catch {
|
|
8
|
+
GlassView = undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { GlassView };
|