stream-chat-react-native-core 9.9.1 → 9.9.2-beta.1
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/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.js +1 -1
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.js.map +1 -1
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +7 -10
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
- package/lib/commonjs/components/ChannelDetails/components/modal/Modal.js +3 -1
- package/lib/commonjs/components/ChannelDetails/components/modal/Modal.js.map +1 -1
- package/lib/commonjs/components/ChannelDetails/components/navigation-section/MediaList.js +2 -2
- package/lib/commonjs/components/ChannelDetails/components/navigation-section/MediaList.js.map +1 -1
- package/lib/commonjs/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.js +2 -2
- package/lib/commonjs/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryFooter.js +1 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryFooter.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryHeader.js +5 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryHeader.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGrid.js +6 -6
- package/lib/commonjs/components/ImageGallery/components/ImageGrid.js.map +1 -1
- package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js +2 -2
- package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
- package/lib/commonjs/components/Message/MessageItemView/MessageItemView.js +1 -1
- package/lib/commonjs/components/UIComponents/BottomSheetModal.js +5 -2
- package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
- package/lib/commonjs/components/UIComponents/SafeAreaViewWrapper.js +1 -1
- package/lib/commonjs/components/UIComponents/SafeAreaViewWrapper.js.map +1 -1
- package/lib/commonjs/hooks/useAppStateListener.js +10 -6
- package/lib/commonjs/hooks/useAppStateListener.js.map +1 -1
- package/lib/commonjs/hooks/useHorizontalInsets.js +21 -0
- package/lib/commonjs/hooks/useHorizontalInsets.js.map +1 -0
- package/lib/commonjs/hooks/useScreenDimensions.js.map +1 -1
- package/lib/commonjs/hooks/useWindowContentWidth.js +15 -0
- package/lib/commonjs/hooks/useWindowContentWidth.js.map +1 -0
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.js +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.js.map +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +7 -10
- package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
- package/lib/module/components/ChannelDetails/components/modal/Modal.js +3 -1
- package/lib/module/components/ChannelDetails/components/modal/Modal.js.map +1 -1
- package/lib/module/components/ChannelDetails/components/navigation-section/MediaList.js +2 -2
- package/lib/module/components/ChannelDetails/components/navigation-section/MediaList.js.map +1 -1
- package/lib/module/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.js +2 -2
- package/lib/module/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.js.map +1 -1
- package/lib/module/components/ImageGallery/components/ImageGalleryFooter.js +1 -1
- package/lib/module/components/ImageGallery/components/ImageGalleryFooter.js.map +1 -1
- package/lib/module/components/ImageGallery/components/ImageGalleryHeader.js +5 -1
- package/lib/module/components/ImageGallery/components/ImageGalleryHeader.js.map +1 -1
- package/lib/module/components/ImageGallery/components/ImageGrid.js +6 -6
- package/lib/module/components/ImageGallery/components/ImageGrid.js.map +1 -1
- package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js +2 -2
- package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
- package/lib/module/components/Message/MessageItemView/MessageItemView.js +1 -1
- package/lib/module/components/UIComponents/BottomSheetModal.js +5 -2
- package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
- package/lib/module/components/UIComponents/SafeAreaViewWrapper.js +1 -1
- package/lib/module/components/UIComponents/SafeAreaViewWrapper.js.map +1 -1
- package/lib/module/hooks/useAppStateListener.js +10 -6
- package/lib/module/hooks/useAppStateListener.js.map +1 -1
- package/lib/module/hooks/useHorizontalInsets.js +21 -0
- package/lib/module/hooks/useHorizontalInsets.js.map +1 -0
- package/lib/module/hooks/useScreenDimensions.js.map +1 -1
- package/lib/module/hooks/useWindowContentWidth.js +15 -0
- package/lib/module/hooks/useWindowContentWidth.js.map +1 -0
- package/lib/module/version.json +1 -1
- package/lib/typescript/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.d.ts.map +1 -1
- package/lib/typescript/components/ChannelDetails/components/modal/Modal.d.ts.map +1 -1
- package/lib/typescript/components/ChannelDetails/components/navigation-section/MediaList.d.ts.map +1 -1
- package/lib/typescript/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/ImageGalleryFooter.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/ImageGalleryHeader.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/ImageGrid.d.ts.map +1 -1
- package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts.map +1 -1
- package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
- package/lib/typescript/hooks/useAppStateListener.d.ts.map +1 -1
- package/lib/typescript/hooks/useHorizontalInsets.d.ts +25 -0
- package/lib/typescript/hooks/useHorizontalInsets.d.ts.map +1 -0
- package/lib/typescript/hooks/useScreenDimensions.d.ts +7 -0
- package/lib/typescript/hooks/useScreenDimensions.d.ts.map +1 -1
- package/lib/typescript/hooks/useWindowContentWidth.d.ts +25 -0
- package/lib/typescript/hooks/useWindowContentWidth.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx +2 -0
- package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx +7 -7
- package/src/components/ChannelDetails/components/modal/Modal.tsx +3 -0
- package/src/components/ChannelDetails/components/navigation-section/MediaList.tsx +6 -13
- package/src/components/ChannelDetails/components/navigation-section/MediaListLoadingSkeleton.tsx +4 -2
- package/src/components/ImageGallery/components/ImageGalleryFooter.tsx +4 -1
- package/src/components/ImageGallery/components/ImageGalleryHeader.tsx +22 -2
- package/src/components/ImageGallery/components/ImageGrid.tsx +7 -4
- package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx +4 -2
- package/src/components/Message/MessageItemView/MessageItemView.tsx +1 -1
- package/src/components/UIComponents/BottomSheetModal.tsx +7 -1
- package/src/components/UIComponents/SafeAreaViewWrapper.tsx +1 -1
- package/src/hooks/__tests__/useAppStateListener.test.tsx +80 -47
- package/src/hooks/useAppStateListener.ts +15 -7
- package/src/hooks/useHorizontalInsets.ts +37 -0
- package/src/hooks/useScreenDimensions.ts +7 -0
- package/src/hooks/useWindowContentWidth.ts +31 -0
- package/src/version.json +1 -1
|
@@ -18,6 +18,23 @@ import { ImageGalleryState } from '../../../state-store/image-gallery-state-stor
|
|
|
18
18
|
import { primitives } from '../../../theme';
|
|
19
19
|
import { getDateString } from '../../../utils/i18n/getDateString';
|
|
20
20
|
import { Button } from '../../ui/Button/Button';
|
|
21
|
+
import { SafeAreaView } from '../../UIComponents/SafeAreaViewWrapper';
|
|
22
|
+
|
|
23
|
+
// Never called - it exists only so `ReturnType` below can name the animated component's type.
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in type position only
|
|
25
|
+
const createAnimatedSafeAreaViewType = () => Animated.createAnimatedComponent(SafeAreaView);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A frame-aware wrapper, matching `ImageGalleryFooter`. `SafeAreaView` compares the window inset
|
|
29
|
+
* against its own measured frame, so a nested instance contributes nothing once an ancestor has
|
|
30
|
+
* already narrowed the subtree - unlike `useSafeAreaInsets`, which is window-global and would
|
|
31
|
+
* double-pad this header inside `ChannelDetailsModal`, which insets its own root.
|
|
32
|
+
*
|
|
33
|
+
* `createAnimatedComponent` is guarded because a stripped-down Reanimated mock may not provide it.
|
|
34
|
+
*/
|
|
35
|
+
const ReanimatedSafeAreaView = (
|
|
36
|
+
Animated.createAnimatedComponent ? Animated.createAnimatedComponent(SafeAreaView) : SafeAreaView
|
|
37
|
+
) as ReturnType<typeof createAnimatedSafeAreaViewType>;
|
|
21
38
|
|
|
22
39
|
const imageGallerySelector = (state: ImageGalleryState) => ({
|
|
23
40
|
asset: state.assets[state.currentIndex],
|
|
@@ -69,7 +86,10 @@ export const ImageGalleryHeader = (props: ImageGalleryHeaderProps) => {
|
|
|
69
86
|
onLayout={(event) => setHeight(event.nativeEvent.layout.height)}
|
|
70
87
|
pointerEvents={'box-none'}
|
|
71
88
|
>
|
|
72
|
-
<
|
|
89
|
+
<ReanimatedSafeAreaView
|
|
90
|
+
edges={['left', 'right']}
|
|
91
|
+
style={[styles.container, { paddingTop: topInset }, headerStyle]}
|
|
92
|
+
>
|
|
73
93
|
<View style={styles.innerContainer}>
|
|
74
94
|
<View style={styles.leftContainer}>
|
|
75
95
|
<Button
|
|
@@ -90,7 +110,7 @@ export const ImageGalleryHeader = (props: ImageGalleryHeaderProps) => {
|
|
|
90
110
|
</View>
|
|
91
111
|
<View style={styles.rightContainer} accessibilityLabel='Right element' />
|
|
92
112
|
</View>
|
|
93
|
-
</
|
|
113
|
+
</ReanimatedSafeAreaView>
|
|
94
114
|
</View>
|
|
95
115
|
);
|
|
96
116
|
};
|
|
@@ -6,7 +6,7 @@ import type { ImageGalleryGridProps } from './types';
|
|
|
6
6
|
import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContextBase';
|
|
7
7
|
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
|
|
8
8
|
import { useStateStore } from '../../../hooks/useStateStore';
|
|
9
|
-
import {
|
|
9
|
+
import { useWindowContentWidth } from '../../../hooks/useWindowContentWidth';
|
|
10
10
|
import type {
|
|
11
11
|
ImageGalleryAsset,
|
|
12
12
|
ImageGalleryState,
|
|
@@ -33,12 +33,12 @@ export type GridImageItem = ImageGalleryAsset & {
|
|
|
33
33
|
|
|
34
34
|
const GridImage = ({ item }: { item: GridImageItem }) => {
|
|
35
35
|
const styles = useStyles();
|
|
36
|
-
const
|
|
36
|
+
const contentWidth = useWindowContentWidth();
|
|
37
37
|
const { ...restItem } = item;
|
|
38
38
|
|
|
39
39
|
const { numberOfImageGalleryGridColumns, selectAndClose, thumb_url, type, uri } = restItem;
|
|
40
40
|
|
|
41
|
-
const size =
|
|
41
|
+
const size = contentWidth / (numberOfImageGalleryGridColumns || 3) - 2;
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<Pressable accessibilityLabel='Grid Image' onPress={selectAndClose}>
|
|
@@ -76,6 +76,8 @@ export const ImageGalleryGrid = (props: ImageGalleryGridProps) => {
|
|
|
76
76
|
} = useTheme();
|
|
77
77
|
const styles = useStyles();
|
|
78
78
|
|
|
79
|
+
const columns = numberOfImageGalleryGridColumns || 3;
|
|
80
|
+
|
|
79
81
|
const imageGridItems = assets.map((photo, index) => ({
|
|
80
82
|
...photo,
|
|
81
83
|
numberOfImageGalleryGridColumns,
|
|
@@ -91,7 +93,8 @@ export const ImageGalleryGrid = (props: ImageGalleryGridProps) => {
|
|
|
91
93
|
contentContainerStyle={styles.contentContainer}
|
|
92
94
|
data={imageGridItems as GridImageItem[]}
|
|
93
95
|
keyExtractor={(item, index) => `${item.uri}-${index}`}
|
|
94
|
-
|
|
96
|
+
key={columns}
|
|
97
|
+
numColumns={columns}
|
|
95
98
|
renderItem={renderItem}
|
|
96
99
|
style={container}
|
|
97
100
|
/>
|
|
@@ -215,11 +215,13 @@ export class KeyboardCompatibleView extends React.Component<
|
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
_handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
218
|
-
|
|
218
|
+
// `inactive` is visible-but-unfocused (Split View), so it must not tear down the listeners.
|
|
219
|
+
// Mirrors `useAppStateListener`.
|
|
220
|
+
if (this.state.appState === 'background' && nextAppState === 'active') {
|
|
219
221
|
this.setKeyboardListeners();
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
if (nextAppState
|
|
224
|
+
if (nextAppState === 'background') {
|
|
223
225
|
this.unsetKeyboardListeners();
|
|
224
226
|
}
|
|
225
227
|
|
|
@@ -128,7 +128,7 @@ export type MessageItemViewPropsWithContext = Pick<
|
|
|
128
128
|
>;
|
|
129
129
|
|
|
130
130
|
const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => {
|
|
131
|
-
const { width } = Dimensions.get('
|
|
131
|
+
const { width } = Dimensions.get('window');
|
|
132
132
|
const {
|
|
133
133
|
alignment,
|
|
134
134
|
channel,
|
|
@@ -45,6 +45,7 @@ import { useResolvedModalAccessibilityProps } from '../../a11y/hooks/useResolved
|
|
|
45
45
|
import { BottomSheetProvider } from '../../contexts/bottomSheetContext/BottomSheetContext';
|
|
46
46
|
import { useTheme } from '../../contexts/themeContext/ThemeContext';
|
|
47
47
|
import { useScreenOrientation, useStableCallback } from '../../hooks';
|
|
48
|
+
import { useHorizontalInsets } from '../../hooks/useHorizontalInsets';
|
|
48
49
|
import { primitives } from '../../theme';
|
|
49
50
|
|
|
50
51
|
const supportedOrientations: ModalProps['supportedOrientations'] = [
|
|
@@ -661,6 +662,7 @@ const useStyles = () => {
|
|
|
661
662
|
const {
|
|
662
663
|
theme: { semantics },
|
|
663
664
|
} = useTheme();
|
|
665
|
+
const horizontalInsets = useHorizontalInsets();
|
|
664
666
|
return useMemo(
|
|
665
667
|
() =>
|
|
666
668
|
StyleSheet.create({
|
|
@@ -671,6 +673,10 @@ const useStyles = () => {
|
|
|
671
673
|
},
|
|
672
674
|
contentContainer: {
|
|
673
675
|
flex: 1,
|
|
676
|
+
// A RN `Modal` is its own native window, so a consumer's `SafeAreaView` never reaches
|
|
677
|
+
// this - it insets itself. Spread rather than assigned: a `paddingLeft: 0` longhand
|
|
678
|
+
// would override a themed `paddingHorizontal` for that side.
|
|
679
|
+
...horizontalInsets,
|
|
674
680
|
},
|
|
675
681
|
handle: {
|
|
676
682
|
alignSelf: 'center',
|
|
@@ -691,6 +697,6 @@ const useStyles = () => {
|
|
|
691
697
|
flex: 1,
|
|
692
698
|
},
|
|
693
699
|
}),
|
|
694
|
-
[semantics.backgroundCoreScrim, semantics.backgroundCoreElevation1],
|
|
700
|
+
[semantics.backgroundCoreScrim, semantics.backgroundCoreElevation1, horizontalInsets],
|
|
695
701
|
);
|
|
696
702
|
};
|
|
@@ -27,7 +27,7 @@ export const SafeAreaViewWrapper = ({
|
|
|
27
27
|
}: PropsWithChildren<{ style: ViewStyle }> & SafeAreaViewProps) => {
|
|
28
28
|
return (
|
|
29
29
|
<SafeAreaProvider>
|
|
30
|
-
<SafeAreaView edges={['bottom', 'top']} style={style} {...restProps}>
|
|
30
|
+
<SafeAreaView edges={['bottom', 'left', 'right', 'top']} style={style} {...restProps}>
|
|
31
31
|
{children}
|
|
32
32
|
</SafeAreaView>
|
|
33
33
|
</SafeAreaProvider>
|
|
@@ -5,54 +5,87 @@ import { renderHook, waitFor } from '@testing-library/react-native';
|
|
|
5
5
|
import { useAppStateListener } from '../useAppStateListener';
|
|
6
6
|
|
|
7
7
|
describe('useAppStateListener', () => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
['active', onForegroundFn, 2],
|
|
18
|
-
['background', onBackgroundFn, 3],
|
|
19
|
-
])(
|
|
20
|
-
'Appropriate callback called when appstate is changed to %s)',
|
|
21
|
-
async (newAppState, expectedCallback, times) => {
|
|
22
|
-
renderHook(
|
|
23
|
-
(props: {
|
|
24
|
-
onBackground?: (() => void) | undefined;
|
|
25
|
-
onForeground?: (() => void) | undefined;
|
|
26
|
-
}) => useAppStateListener(props.onForeground, props.onBackground),
|
|
27
|
-
{
|
|
28
|
-
initialProps: {
|
|
29
|
-
onBackground: onBackgroundFn,
|
|
30
|
-
onForeground: onForegroundFn,
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
);
|
|
34
|
-
const appStateOnChangeMockFunc = addEventListenerSpy.mock.calls[0][1];
|
|
35
|
-
|
|
36
|
-
appStateOnChangeMockFunc(newAppState);
|
|
37
|
-
await waitFor(() => {
|
|
38
|
-
expect(expectedCallback).toHaveBeenCalledTimes(times);
|
|
39
|
-
});
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
it('check unmount behavior', async () => {
|
|
8
|
+
const renderListener = (onForegroundFn: jest.Mock, onBackgroundFn: jest.Mock) => {
|
|
9
|
+
const addEventListenerSpy = jest.spyOn(AppState, 'addEventListener');
|
|
10
|
+
addEventListenerSpy.mockClear();
|
|
11
|
+
const utils = renderHook(() => useAppStateListener(onForegroundFn, onBackgroundFn));
|
|
12
|
+
const emit = addEventListenerSpy.mock.calls[0][1] as (state: AppStateStatus) => void;
|
|
13
|
+
return { ...utils, addEventListenerSpy, emit };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
43
17
|
jest.clearAllMocks();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
18
|
+
AppState.currentState = 'active';
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls onBackground when the app is backgrounded', async () => {
|
|
22
|
+
const onForeground = jest.fn();
|
|
23
|
+
const onBackground = jest.fn();
|
|
24
|
+
const { emit } = renderListener(onForeground, onBackground);
|
|
25
|
+
|
|
26
|
+
emit('inactive');
|
|
27
|
+
emit('background');
|
|
28
|
+
|
|
29
|
+
await waitFor(() => {
|
|
30
|
+
expect(onBackground).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
expect(onForeground).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('calls onForeground when the app returns to active from background', async () => {
|
|
36
|
+
const onForeground = jest.fn();
|
|
37
|
+
const onBackground = jest.fn();
|
|
38
|
+
const { emit } = renderListener(onForeground, onBackground);
|
|
39
|
+
|
|
40
|
+
emit('inactive');
|
|
41
|
+
emit('background');
|
|
42
|
+
emit('inactive');
|
|
43
|
+
emit('active');
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(onForeground).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
expect(onBackground).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Regression: an app in Split View sits `inactive` while fully visible. Treating that as
|
|
52
|
+
// backgrounded closed the WebSocket while the user was reading the chat.
|
|
53
|
+
it('does NOT treat `inactive` as backgrounded', async () => {
|
|
54
|
+
const onForeground = jest.fn();
|
|
55
|
+
const onBackground = jest.fn();
|
|
56
|
+
const { emit } = renderListener(onForeground, onBackground);
|
|
57
|
+
|
|
58
|
+
emit('inactive');
|
|
59
|
+
emit('active');
|
|
60
|
+
emit('inactive');
|
|
61
|
+
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
expect(onForeground).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('fires each callback once per transition even with repeated events', async () => {
|
|
69
|
+
const onForeground = jest.fn();
|
|
70
|
+
const onBackground = jest.fn();
|
|
71
|
+
const { emit } = renderListener(onForeground, onBackground);
|
|
72
|
+
|
|
73
|
+
emit('background');
|
|
74
|
+
emit('background');
|
|
75
|
+
emit('active');
|
|
76
|
+
emit('active');
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(onBackground).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
expect(onForeground).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('check unmount behavior', async () => {
|
|
85
|
+
const onForeground = jest.fn();
|
|
86
|
+
const onBackground = jest.fn();
|
|
87
|
+
const { addEventListenerSpy, unmount } = renderListener(onForeground, onBackground);
|
|
88
|
+
|
|
56
89
|
const { remove: appStateOnChangeSubscriptionRemoveMockFunc } =
|
|
57
90
|
addEventListenerSpy.mock.results[0].value;
|
|
58
91
|
|
|
@@ -5,8 +5,8 @@ export const useAppStateListener = (onForeground?: () => void, onBackground?: ()
|
|
|
5
5
|
// React Native 0.87 widened `AppState.currentState` to `string | null | undefined` (it was
|
|
6
6
|
// `AppStateStatus` up to 0.86). Normalise once here so the comparisons below stay total on
|
|
7
7
|
// every supported version.
|
|
8
|
-
const
|
|
9
|
-
(AppState.currentState as AppStateStatus | null | undefined) ?? 'unknown',
|
|
8
|
+
const isBackgroundedRef = useRef(
|
|
9
|
+
((AppState.currentState as AppStateStatus | null | undefined) ?? 'unknown') === 'background',
|
|
10
10
|
);
|
|
11
11
|
const onForegroundRef = useRef(onForeground);
|
|
12
12
|
const onBackgroundRef = useRef(onBackground);
|
|
@@ -17,13 +17,21 @@ export const useAppStateListener = (onForeground?: () => void, onBackground?: ()
|
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Only `background` is backgrounded. `inactive` is visible-but-unfocused, and from iOS 27 an
|
|
21
|
+
// app in Split View stays `inactive` the whole time it is side by side. Transitions pass
|
|
22
|
+
// through `inactive` in both directions, so ignoring it still observes both.
|
|
23
|
+
if (nextAppState === 'background') {
|
|
24
|
+
if (!isBackgroundedRef.current) {
|
|
25
|
+
isBackgroundedRef.current = true;
|
|
26
|
+
onBackgroundRef.current?.();
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (nextAppState === 'active' && isBackgroundedRef.current) {
|
|
32
|
+
isBackgroundedRef.current = false;
|
|
22
33
|
onForegroundRef.current?.();
|
|
23
|
-
} else if (prevAppState === 'active' && nextAppState.match(/inactive|background/)) {
|
|
24
|
-
onBackgroundRef.current?.();
|
|
25
34
|
}
|
|
26
|
-
appStateRef.current = nextAppState;
|
|
27
35
|
};
|
|
28
36
|
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
29
37
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Horizontal safe area padding for a surface that spans the full window width.
|
|
7
|
+
*
|
|
8
|
+
* **Only for surfaces an integrator cannot wrap.** Everything the SDK renders in-tree is left
|
|
9
|
+
* alone: the integrator wraps their screen in a `SafeAreaView` and the whole subtree - including
|
|
10
|
+
* absolutely positioned children, which track their containing block's border box - narrows with
|
|
11
|
+
* it. Applying this to an in-tree surface double-pads it for every integrator who already wraps.
|
|
12
|
+
*
|
|
13
|
+
* In practice that leaves React Native's own `Modal`, which renders in a separate native window.
|
|
14
|
+
* A bottom sheet or an overlay is *not* automatically out of reach - check where it actually
|
|
15
|
+
* mounts. Prefer a `SafeAreaView` (see `ImageGalleryHeader`/`Footer`) wherever one fits: it
|
|
16
|
+
* compares the window inset against its own measured frame, so a nested instance contributes
|
|
17
|
+
* nothing once an ancestor has narrowed the subtree. This hook reads `useSafeAreaInsets`, which is
|
|
18
|
+
* window-global and has no such protection.
|
|
19
|
+
*
|
|
20
|
+
* `left` and `right` are never collapsed into one value: from iOS 27 the system stacks its controls
|
|
21
|
+
* down a single edge, so the two differ (measured 0 and 84 on iPhone Duo).
|
|
22
|
+
*
|
|
23
|
+
* A side whose inset is 0 is omitted rather than emitted as `paddingLeft: 0`, because a longhand
|
|
24
|
+
* overrides a `padding` shorthand for that side - emitting a zero would silently drop padding the
|
|
25
|
+
* component or a consumer's theme had already set.
|
|
26
|
+
*/
|
|
27
|
+
export const useHorizontalInsets = (): ViewStyle => {
|
|
28
|
+
const { left, right } = useSafeAreaInsets();
|
|
29
|
+
|
|
30
|
+
return useMemo<ViewStyle>(
|
|
31
|
+
() => ({
|
|
32
|
+
...(left > 0 ? { paddingLeft: left } : null),
|
|
33
|
+
...(right > 0 ? { paddingRight: right } : null),
|
|
34
|
+
}),
|
|
35
|
+
[left, right],
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -6,6 +6,13 @@ import { Dimensions, ScaledSize } from 'react-native';
|
|
|
6
6
|
* a percentage of the screen height (vh) and viewport width (vw). It
|
|
7
7
|
* dynamically updates dimensions on changes in device orientation.
|
|
8
8
|
*
|
|
9
|
+
* @deprecated These are percentages of the *screen*, not of the window the app is laid out in. The
|
|
10
|
+
* two diverge once the window is resizable (Split View, fold/unfold, Android multi-window): we
|
|
11
|
+
* measured an 834pt screen behind a 417pt window, and an outer 466pt screen behind a 951pt window.
|
|
12
|
+
*
|
|
13
|
+
* Use {@link useViewport}, the same API measured against the window, or `onLayout` on your own view
|
|
14
|
+
* inside a component that does not fill the window.
|
|
15
|
+
*
|
|
9
16
|
* @returns {Object} An object containing functions vh and vw.
|
|
10
17
|
*/
|
|
11
18
|
export const useScreenDimensions = (rounded?: boolean) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Width available to a surface that spans the window, minus the horizontal safe area.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately *not* measured. `onLayout` reports a frame after the window has already resized, so
|
|
7
|
+
* anything sized from it - tile sizes in a grid, for instance - visibly resizes a frame late on a
|
|
8
|
+
* rotation or a fold.
|
|
9
|
+
*
|
|
10
|
+
* The frame comes from `useSafeAreaFrame`, not `useWindowDimensions`: the frame and the insets are
|
|
11
|
+
* delivered by the *same* native event and set in the same handler, so they update together in one
|
|
12
|
+
* render. Mixing `Dimensions` with the insets desyncs them - the window width lands first and the
|
|
13
|
+
* insets a render later, producing an extra intermediate size (and, for anything deriving an image
|
|
14
|
+
* URL from it, an extra fetch).
|
|
15
|
+
*
|
|
16
|
+
* Assumes the caller fills the window. A component rendered inside a narrower container must pass
|
|
17
|
+
* that container's width via `containerWidth`, or measure it itself and accept the frame of lag
|
|
18
|
+
* that comes with measuring.
|
|
19
|
+
*
|
|
20
|
+
* @param containerWidth Width of the surface, when it is known and narrower than the window.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const contentWidth = useWindowContentWidth();
|
|
24
|
+
* const tileSize = contentWidth / columns;
|
|
25
|
+
*/
|
|
26
|
+
export const useWindowContentWidth = (containerWidth?: number) => {
|
|
27
|
+
const { width: frameWidth } = useSafeAreaFrame();
|
|
28
|
+
const { left, right } = useSafeAreaInsets();
|
|
29
|
+
|
|
30
|
+
return containerWidth ?? frameWidth - left - right;
|
|
31
|
+
};
|
package/src/version.json
CHANGED