diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index 3947acdc50f..86c531d52c9 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -53,6 +53,7 @@ runs: echo -e "reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64" >> ./gradle.properties echo -e "newArchEnabled=true" >> ./gradle.properties echo -e "hermesEnabled=true" >> ./gradle.properties + echo -e "edgeToEdgeEnabled=true" >> ./gradle.properties echo -e "VERSIONCODE=${{ inputs.BUILD_VERSION }}" >> ./gradle.properties echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties echo -e "BugsnagAPIKey=${{ inputs.BUGSNAG_KEY_OFFICIAL }}" >> ./gradle.properties diff --git a/.github/workflows/e2e-build-android.yml b/.github/workflows/e2e-build-android.yml index 02cb630fdc2..5fd11a44f80 100644 --- a/.github/workflows/e2e-build-android.yml +++ b/.github/workflows/e2e-build-android.yml @@ -106,6 +106,7 @@ jobs: echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties echo -e "newArchEnabled=true" >> ./gradle.properties echo -e "hermesEnabled=true" >> ./gradle.properties + echo -e "edgeToEdgeEnabled=true" >> ./gradle.properties echo -e "VERSIONCODE=999999" >> ./gradle.properties echo -e "KEYSTORE=release.keystore" >> ./gradle.properties echo -e 'KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_OFFICIAL_PASSWORD }}' >> ./gradle.properties diff --git a/.maestro/tests/room/message-markdown-click.yaml b/.maestro/tests/room/message-markdown-click.yaml index caffeae2f2f..f0a746f2648 100644 --- a/.maestro/tests/room/message-markdown-click.yaml +++ b/.maestro/tests/room/message-markdown-click.yaml @@ -78,7 +78,7 @@ tags: platform: iOS commands: - tapOn: - point: 66%,66% + point: 66%,63% # Verify alert is shown with the link - extendedWaitUntil: @@ -106,7 +106,7 @@ tags: platform: iOS commands: - longPressOn: - point: 66%,66% + point: 66%,63% # Verify clipboard has the link alert - extendedWaitUntil: diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index a612e0f380b..74d1a1c5bd7 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -3,6 +3,7 @@ #aaaaaa false @drawable/rn_edit_text_material + false diff --git a/android/gradle.properties b/android/gradle.properties index ce8b83a17d2..48449f001cd 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -51,4 +51,4 @@ hermesEnabled=true # Use this property to enable edge-to-edge display support. # This allows your app to draw behind system bars for an immersive UI. # Note: Only works with ReactActivity and should not be used with custom Activity. -edgeToEdgeEnabled=false \ No newline at end of file +edgeToEdgeEnabled=true \ No newline at end of file diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index 52377a608aa..cf4719b5e7e 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -6,7 +6,6 @@ import { findNodeHandle, Keyboard, type LayoutChangeEvent, - Platform, useWindowDimensions, type View } from 'react-native'; @@ -35,10 +34,6 @@ const ActionSheet = memo( const [contentHeight, setContentHeight] = useState(0); const onCloseSnapshotRef = useRef(undefined); - // TrueSheet detects the bottom inset for Android 16 and iOS - // To avoid content hiding behind navigation bar on older Android versions - const isNewAndroid = isAndroid && Number(Platform.Version) >= 36; - const bottom = isIOS || isNewAndroid ? 0 : windowHeight * 0.03; const itemHeight = useActionSheetItemHeight(); const handleContentLayout = ({ nativeEvent: { layout } }: LayoutChangeEvent) => { @@ -108,7 +103,6 @@ const ActionSheet = memo( const { detents, maxHeight, scrollEnabled } = useActionSheetDetents({ windowHeight, - bottomInset: bottom, itemHeight, optionsLength: data?.options?.length || 0, snaps: effectiveSnaps, @@ -119,7 +113,7 @@ const ActionSheet = memo( const hasOptions = !!data?.options?.length; const hasSnaps = !!effectiveSnaps?.length; - const disableContentPanning = data?.enableContentPanningGesture === false || (!scrollEnabled && isAndroid); + const disableContentPanning = data?.enableContentPanningGesture === false; const isScrollable = hasOptions || (hasSnaps && !disableContentPanning); const contentMinHeight = diff --git a/app/containers/ActionSheet/BottomSheetContent.tsx b/app/containers/ActionSheet/BottomSheetContent.tsx index 8451db9cfda..ffd92507298 100644 --- a/app/containers/ActionSheet/BottomSheetContent.tsx +++ b/app/containers/ActionSheet/BottomSheetContent.tsx @@ -41,7 +41,6 @@ const BottomSheetContent = memo( const { colors } = useTheme(); const { bottom } = useSafeAreaInsets(); const height = useActionSheetItemHeight(); - const paddingBottom = isAndroid ? bottom + height : bottom; const minHeightStyle = isAndroid || !contentMinHeight ? undefined : { minHeight: contentMinHeight }; const renderFooter = () => @@ -68,7 +67,7 @@ const BottomSheetContent = memo( style={{ backgroundColor: colors.strokeExtraDark }} keyboardDismissMode='interactive' indicatorStyle='black' - contentContainerStyle={{ paddingBottom, backgroundColor: colors.surfaceLight }} + contentContainerStyle={{ paddingBottom: bottom, backgroundColor: colors.surfaceLight }} ItemSeparatorComponent={List.Separator} ListHeaderComponent={List.Separator} ListFooterComponent={renderFooter} diff --git a/app/containers/ActionSheet/useActionSheetDetents.test.tsx b/app/containers/ActionSheet/useActionSheetDetents.test.tsx index 63ddc6dddbc..2564b0cbd74 100644 --- a/app/containers/ActionSheet/useActionSheetDetents.test.tsx +++ b/app/containers/ActionSheet/useActionSheetDetents.test.tsx @@ -9,7 +9,6 @@ describe('useActionSheetDetents', () => { const { result } = renderHook(() => useActionSheetDetents({ windowHeight, - bottomInset: 0, itemHeight: 0, optionsLength: 0, snaps: [0.3, '80%', 2], @@ -26,7 +25,6 @@ describe('useActionSheetDetents', () => { const { result } = renderHook(() => useActionSheetDetents({ windowHeight, - bottomInset: 16, itemHeight: 50, optionsLength: 20, snaps: undefined, @@ -44,7 +42,6 @@ describe('useActionSheetDetents', () => { const { result } = renderHook(() => useActionSheetDetents({ windowHeight, - bottomInset: 10, itemHeight: 20, optionsLength: 3, snaps: undefined, @@ -54,14 +51,13 @@ describe('useActionSheetDetents', () => { }) ); - expect(result.current.detents).toEqual([0.108]); + expect(result.current.detents).toEqual([0.098]); }); it('computes detent from content height when there are no options', () => { const { result } = renderHook(() => useActionSheetDetents({ windowHeight, - bottomInset: 50, itemHeight: 0, optionsLength: 0, snaps: undefined, @@ -71,14 +67,13 @@ describe('useActionSheetDetents', () => { }) ); - expect(result.current.detents).toEqual([(300 + 50 + HANDLE_HEIGHT) / windowHeight]); + expect(result.current.detents).toEqual([(300 + HANDLE_HEIGHT) / windowHeight]); }); it('falls back to minimum height when no content or options', () => { const { result } = renderHook(() => useActionSheetDetents({ windowHeight, - bottomInset: 0, itemHeight: 0, optionsLength: 0, snaps: undefined, diff --git a/app/containers/ActionSheet/useActionSheetDetents.ts b/app/containers/ActionSheet/useActionSheetDetents.ts index 481919f57a7..5bc2b90abfa 100644 --- a/app/containers/ActionSheet/useActionSheetDetents.ts +++ b/app/containers/ActionSheet/useActionSheetDetents.ts @@ -23,7 +23,6 @@ function normalizeSnapsToDetents(snaps: (string | number)[]): number[] { type UseActionSheetDetentsParams = { windowHeight: number; - bottomInset: number; itemHeight: number; optionsLength?: number; snaps?: (string | number)[]; @@ -38,7 +37,6 @@ function heightToDetent(height: number, screenHeight: number): number { export function useActionSheetDetents({ windowHeight, - bottomInset, itemHeight, optionsLength = 0, snaps, @@ -51,10 +49,7 @@ export function useActionSheetDetents({ const hasOptions = optionsLength > 0; const maxSnap = hasOptions - ? Math.min( - (itemHeight + 0.5) * optionsLength + HANDLE_HEIGHT + headerHeight + bottomInset + (hasCancel ? itemHeight : 0), - maxHeight - ) + ? Math.min((itemHeight + 0.5) * optionsLength + HANDLE_HEIGHT + headerHeight + (hasCancel ? itemHeight : 0), maxHeight) : 0; let detents: SheetDetent[]; @@ -67,14 +62,13 @@ export function useActionSheetDetents({ detents = [0.5, ACTION_SHEET_MAX_HEIGHT_FRACTION]; scrollEnabled = true; } else { - const measuredHeight = - optionsLength * itemHeight + HANDLE_HEIGHT + headerHeight + bottomInset + (hasCancel ? itemHeight : 0); + const measuredHeight = optionsLength * itemHeight + HANDLE_HEIGHT + headerHeight + (hasCancel ? itemHeight : 0); scrollEnabled = false; detents = [heightToDetent(Math.round(measuredHeight), windowHeight)]; } } else if (contentHeight > 0) { - const rawContentDetent = (contentHeight + bottomInset + HANDLE_HEIGHT) / windowHeight; + const rawContentDetent = (contentHeight + HANDLE_HEIGHT) / windowHeight; const contentDetent = Math.min( ACTION_SHEET_MAX_HEIGHT_FRACTION, Math.max(ACTION_SHEET_MIN_HEIGHT_FRACTION, rawContentDetent) @@ -86,5 +80,5 @@ export function useActionSheetDetents({ } return { detents, maxHeight, scrollEnabled }; - }, [bottomInset, contentHeight, hasCancel, headerHeight, itemHeight, optionsLength, snaps, windowHeight]); + }, [contentHeight, hasCancel, headerHeight, itemHeight, optionsLength, snaps, windowHeight]); } diff --git a/app/containers/EmojiPicker/EmojiCategory.tsx b/app/containers/EmojiPicker/EmojiCategory.tsx index 54cb29ea1d2..d1e9250e8b0 100644 --- a/app/containers/EmojiPicker/EmojiCategory.tsx +++ b/app/containers/EmojiPicker/EmojiCategory.tsx @@ -1,6 +1,5 @@ import { memo, type ReactElement } from 'react'; import { FlatList } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { type ICustomEmojis, type IEmoji } from '../../definitions/IEmoji'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; @@ -55,7 +54,6 @@ const EmojiCategory = ({ bottomSheet = false }: IEmojiCategoryProps): ReactElement | null => { const items = useEmojis(category); - const { bottom } = useSafeAreaInsets(); if (!parentWidth) { return null; @@ -63,7 +61,7 @@ const EmojiCategory = ({ const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE); const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2; - const contentPaddingBottom = bottomSheet ? Math.max(0, MIN_BOTTOM_SHEET_BREATHING_ROOM - bottom) : undefined; + const contentPaddingBottom = bottomSheet ? MIN_BOTTOM_SHEET_BREATHING_ROOM : undefined; const renderItem = ({ item }: { item: IEmoji }) => ; diff --git a/app/containers/FormContainer.tsx b/app/containers/FormContainer.tsx index 3449efafee2..df719c1b7a8 100644 --- a/app/containers/FormContainer.tsx +++ b/app/containers/FormContainer.tsx @@ -1,6 +1,7 @@ import { type ReactElement } from 'react'; import { type ScrollViewProps, StyleSheet, View } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import sharedStyles from '../views/Styles'; import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps'; @@ -17,7 +18,7 @@ interface IFormContainer extends ScrollViewProps { const styles = StyleSheet.create({ scrollView: { - minHeight: '100%' + flexGrow: 1 } }); @@ -27,19 +28,20 @@ export const FormContainerInner = ({ }: { children: (ReactElement | null)[]; accessibilityLabel?: string; -}) => ( +}): ReactElement => ( {children} ); -const FormContainer = ({ children, testID, showAppVersion = true, ...props }: IFormContainer) => { +const FormContainer = ({ children, testID, showAppVersion = true, ...props }: IFormContainer): ReactElement => { const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); return ( diff --git a/app/containers/List/ListContainer.tsx b/app/containers/List/ListContainer.tsx index fd007030536..4542f8be3d0 100644 --- a/app/containers/List/ListContainer.tsx +++ b/app/containers/List/ListContainer.tsx @@ -1,5 +1,6 @@ import { type ReactElement } from 'react'; import { ScrollView, type ScrollViewProps, StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; @@ -16,9 +17,11 @@ interface IListContainer extends ScrollViewProps { const ListContainer = ({ children, ...props }: IListContainer) => { 'use memo'; + const { bottom } = useSafeAreaInsets(); + return ( diff --git a/app/containers/List/__snapshots__/List.test.tsx.snap b/app/containers/List/__snapshots__/List.test.tsx.snap index ba7f78d3559..02ca3132ced 100644 --- a/app/containers/List/__snapshots__/List.test.tsx.snap +++ b/app/containers/List/__snapshots__/List.test.tsx.snap @@ -3,9 +3,14 @@ exports[`Story Snapshots: Alert should match snapshot 1`] = ` { const { bottom } = useSafeAreaInsets(); const { height } = useKeyboardAnimation(); - const initialHeight = Platform.OS === 'ios' ? bottom : 0; - const keyboardHeight = useSharedValue(initialHeight); - const previousHeight = useSharedValue(initialHeight); + const keyboardHeight = useSharedValue(bottom); + const previousHeight = useSharedValue(bottom); const updateHeight = (force: boolean = false) => { 'worklet'; @@ -93,9 +91,9 @@ export const useEmojiKeyboard = () => { ) { return; } - // On iOS, keyboard controller doesn't include bottom inset, so we add it when keyboard is closed - // On Android, keyboard controller already includes it, so we don't add it - const notch = Platform.OS === 'ios' && height.value === 0 ? bottom : 0; + // When keyboard is closed, add bottom safe area inset to ensure content is visible above + // navigation bars/home indicator. When keyboard is open, keyboard height already covers it. + const notch = height.value === 0 ? bottom : 0; keyboardHeight.value = height.value + notch; previousHeight.value = keyboardHeight.value; }; diff --git a/app/containers/markdown/Markdown.test.tsx b/app/containers/markdown/Markdown.test.tsx index 3beca437ef3..0372d0b5107 100644 --- a/app/containers/markdown/Markdown.test.tsx +++ b/app/containers/markdown/Markdown.test.tsx @@ -1,4 +1,8 @@ import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots'; import * as stories from './Markdown.stories'; +beforeAll(() => { + jest.useFakeTimers({ now: new Date('2026-02-01T00:00:00.000Z').getTime() }); +}); + generateSnapshots(stories); diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 2517ef4eb26..009e5615a36 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -5882,7 +5882,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 2 years ago + a year ago diff --git a/app/lib/hooks/withSafeAreaInsets.tsx b/app/lib/hooks/withSafeAreaInsets.tsx new file mode 100644 index 00000000000..3bc79af273c --- /dev/null +++ b/app/lib/hooks/withSafeAreaInsets.tsx @@ -0,0 +1,15 @@ +import { type ComponentType } from 'react'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import { withSafeAreaInsets as withSafeAreaInsetsLib, type WithSafeAreaInsetsProps } from 'react-native-safe-area-context'; + +import { type TNavigationOptions } from '../../definitions/navigationTypes'; + +export type { WithSafeAreaInsetsProps }; + +export function withSafeAreaInsets( + Component: ComponentType & TNavigationOptions +): ComponentType> & TNavigationOptions { + const WithInsets = withSafeAreaInsetsLib>(Component as any); + hoistNonReactStatics(WithInsets, Component); + return WithInsets as unknown as ComponentType> & TNavigationOptions; +} diff --git a/app/lib/methods/helpers/theme.ts b/app/lib/methods/helpers/theme.ts index 0e0f0317598..b54de5c4ba0 100644 --- a/app/lib/methods/helpers/theme.ts +++ b/app/lib/methods/helpers/theme.ts @@ -1,6 +1,6 @@ import { Appearance } from 'react-native'; -import * as NavigationBar from 'expo-navigation-bar'; import * as SystemUI from 'expo-system-ui'; +import { NavigationBar } from '@zoontek/react-native-navigation-bar'; import { type IThemePreference, type TThemeMode } from '../../../definitions/ITheme'; import { themes } from '../../constants/colors'; @@ -53,7 +53,7 @@ export const setNativeTheme = (themePreferences: IThemePreference) => { const isLightTheme = theme === 'light'; if (isAndroid) { try { - NavigationBar.setStyle(isLightTheme ? 'dark' : 'light'); + NavigationBar.setBarStyle(isLightTheme ? 'dark-content' : 'light-content'); } catch (error) { // Do nothing } diff --git a/app/views/AddExistingChannelView/index.tsx b/app/views/AddExistingChannelView/index.tsx index 4405a503b05..11546f34b28 100644 --- a/app/views/AddExistingChannelView/index.tsx +++ b/app/views/AddExistingChannelView/index.tsx @@ -3,6 +3,7 @@ import { type NativeStackNavigationOptions, type NativeStackNavigationProp } fro import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { FlatList } from 'react-native'; import { Q } from '@nozbe/watermelondb'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { textInputDebounceTime } from '../../lib/constants/debounceConfig'; import * as List from '../../containers/List'; @@ -38,6 +39,7 @@ const AddExistingChannelView = () => { const { params: { teamId } } = useRoute(); + const { bottom } = useSafeAreaInsets(); const { serverVersion, addTeamChannelPermission, moveRoomToTeamPermission } = useAppSelector(state => ({ serverVersion: state.server.version, @@ -175,7 +177,7 @@ const AddExistingChannelView = () => { ); }} ItemSeparatorComponent={List.Separator} - contentContainerStyle={{ backgroundColor: colors.surfaceRoom }} + contentContainerStyle={{ backgroundColor: colors.surfaceRoom, paddingBottom: bottom }} keyboardShouldPersistTaps='always' /> diff --git a/app/views/AutoTranslateView/index.tsx b/app/views/AutoTranslateView/index.tsx index f8ed8f56611..4b2f410d0cf 100644 --- a/app/views/AutoTranslateView/index.tsx +++ b/app/views/AutoTranslateView/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState, memo, type ReactElement } import { FlatList, StyleSheet, Switch } from 'react-native'; import { type Subscription } from 'rxjs'; import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as List from '../../containers/List'; import SafeAreaView from '../../containers/SafeAreaView'; @@ -24,6 +25,7 @@ const AutoTranslateView = (): ReactElement => { params: { rid, room } } = useRoute>(); const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const [languages, setLanguages] = useState<{ language: string; name: string }[]>([]); const [selectedLanguage, setSelectedLanguage] = useState(room?.autoTranslateLanguage); @@ -131,7 +133,7 @@ const AutoTranslateView = (): ReactElement => { } ItemSeparatorComponent={List.Separator} ListFooterComponent={List.Separator} - contentContainerStyle={[List.styles.contentContainerStyleFlatList, styles.list]} + contentContainerStyle={[List.styles.contentContainerStyleFlatList, styles.list, { paddingBottom: bottom }]} /> ); diff --git a/app/views/CannedResponsesListView/index.tsx b/app/views/CannedResponsesListView/index.tsx index 51616395267..3dad6f8d3cf 100644 --- a/app/views/CannedResponsesListView/index.tsx +++ b/app/views/CannedResponsesListView/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { FlatList } from 'react-native'; import { type RouteProp } from '@react-navigation/native'; import { type NativeStackNavigationOptions, type NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import database from '../../lib/database'; import I18n from '../../i18n'; @@ -67,6 +68,7 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView const { theme } = useTheme(); const isMasterDetail = useMasterDetail(); + const { bottom } = useSafeAreaInsets(); const getRoomFromDb = async () => { const { rid } = route.params; @@ -271,6 +273,7 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView data={cannedResponsesScopeName} extraData={cannedResponsesScopeName} style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} + contentContainerStyle={{ paddingBottom: bottom }} renderItem={({ item }) => ( { const [supported, setSupported] = useState([]); const navigation = useNavigation(); + const { bottom } = useSafeAreaInsets(); + + const paddingBottom = useMemo(() => Math.max(16, bottom), [bottom]); useLayoutEffect(() => { navigation.setOptions({ @@ -86,7 +90,7 @@ const DefaultBrowserView = () => { item.value} - contentContainerStyle={List.styles.contentContainerStyleFlatList} + contentContainerStyle={[List.styles.contentContainerStyleFlatList, { paddingBottom }]} renderItem={({ item }) => ( { const { colors } = useTheme(); - const insets = useSafeAreaInsets(); const renderItem = (itemType: string) => { let text = 'Users'; @@ -52,7 +50,7 @@ const DirectoryOptions = ({ }; return ( - + {renderItem('channels')} diff --git a/app/views/DirectoryView/index.tsx b/app/views/DirectoryView/index.tsx index 634b7453fad..c0e924bc325 100644 --- a/app/views/DirectoryView/index.tsx +++ b/app/views/DirectoryView/index.tsx @@ -1,6 +1,7 @@ import { useLayoutEffect, type ReactElement } from 'react'; import { FlatList, type ListRenderItem } from 'react-native'; import { shallowEqual } from 'react-redux'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { type NativeStackNavigationOptions, type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { type CompositeNavigationProp } from '@react-navigation/native'; @@ -36,6 +37,7 @@ interface IDirectoryViewProps { const DirectoryView = ({ navigation }: IDirectoryViewProps): ReactElement => { const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const { showActionSheet, hideActionSheet } = useActionSheet(); const { isFederationEnabled, directoryDefaultView } = useAppSelector( @@ -198,7 +200,7 @@ const DirectoryView = ({ navigation }: IDirectoryViewProps): ReactElement => { item._id} renderItem={renderItem} diff --git a/app/views/DirectoryView/styles.ts b/app/views/DirectoryView/styles.ts index 588d8dda6a1..32a11d852b5 100644 --- a/app/views/DirectoryView/styles.ts +++ b/app/views/DirectoryView/styles.ts @@ -3,6 +3,9 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../Styles'; export default StyleSheet.create({ + container: { + paddingVertical: 0 + }, list: { flex: 1 }, diff --git a/app/views/DiscussionsView/index.tsx b/app/views/DiscussionsView/index.tsx index 24e72334263..fb4ad5401d2 100644 --- a/app/views/DiscussionsView/index.tsx +++ b/app/views/DiscussionsView/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { FlatList, StyleSheet } from 'react-native'; import { type NativeStackNavigationOptions, type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { textInputDebounceTime } from '../../lib/constants/debounceConfig'; import { type IMessageFromServer, type TThreadModel } from '../../definitions'; @@ -33,6 +34,7 @@ const styles = StyleSheet.create({ const DiscussionsView = () => { const navigation = useNavigation>(); const route = useRoute>(); + const { bottom } = useSafeAreaInsets(); const rid = route.params?.rid; const t = route.params?.t; @@ -176,7 +178,7 @@ const DiscussionsView = () => { renderItem={renderItem} keyExtractor={(item: any) => item._id} style={{ backgroundColor: colors.surfaceRoom }} - contentContainerStyle={styles.contentContainer} + contentContainerStyle={[styles.contentContainer, { paddingBottom: bottom }]} onEndReachedThreshold={0.5} removeClippedSubviews={isIOS} onEndReached={() => isSearching && offset.current < total.current && load()} diff --git a/app/views/E2ESaveYourPasswordView.tsx b/app/views/E2ESaveYourPasswordView.tsx index 1e46f979446..d5ea90ce809 100644 --- a/app/views/E2ESaveYourPasswordView.tsx +++ b/app/views/E2ESaveYourPasswordView.tsx @@ -4,6 +4,7 @@ import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { useDispatch } from 'react-redux'; import { StackActions, useNavigation } from '@react-navigation/native'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { encryptionSetBanner } from '../actions/encryption'; import Button from '../containers/Button'; @@ -64,6 +65,7 @@ const E2ESaveYourPasswordView = () => { const dispatch = useDispatch(); const [password, setPassword] = useState(''); const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); useLayoutEffect(() => { navigation.setOptions({ @@ -105,7 +107,7 @@ const E2ESaveYourPasswordView = () => { + contentContainerStyle={[sharedStyles.containerScrollView, { flexGrow: 1, paddingBottom: bottom }]}> {I18n.t('Save_Your_Encryption_Password_warning')} diff --git a/app/views/JitsiMeetView/index.tsx b/app/views/JitsiMeetView/index.tsx index 3f82a909040..036b39e711e 100644 --- a/app/views/JitsiMeetView/index.tsx +++ b/app/views/JitsiMeetView/index.tsx @@ -2,8 +2,9 @@ import CookieManager from '@react-native-cookies/cookies'; import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import { useCallback, useEffect, useState, type ReactElement } from 'react'; -import { ActivityIndicator, Linking, SafeAreaView, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, Linking, StyleSheet, View } from 'react-native'; import WebView, { type WebViewNavigation } from 'react-native-webview'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { userAgent } from '../../lib/constants/userAgent'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; @@ -14,6 +15,7 @@ import { endVideoConfTimer, initVideoConfTimer } from '../../lib/methods/videoCo import { getUserSelector } from '../../selectors/login'; import { type ChatsStackParamList } from '../../stacks/types'; import JitsiAuthModal from './JitsiAuthModal'; +import SafeAreaView from '../../containers/SafeAreaView'; const JitsiMeetView = (): ReactElement => { const { @@ -22,6 +24,7 @@ const JitsiMeetView = (): ReactElement => { const { goBack } = useNavigation(); const user = useAppSelector(state => getUserSelector(state)); const serverUrl = useAppSelector(state => state.server.server); + const { bottom } = useSafeAreaInsets(); const [authModal, setAuthModal] = useState(false); const [cookiesSet, setCookiesSet] = useState(false); @@ -105,7 +108,7 @@ const JitsiMeetView = (): ReactElement => { return ( - {authModal && } + {authModal ? : null} {cookiesSet ? ( { }} onNavigationStateChange={onNavigationStateChange} onShouldStartLoadWithRequest={onNavigationStateChange} - style={styles.webviewContainer} + style={[styles.webviewContainer, { marginBottom: bottom }]} userAgent={userAgent} javaScriptEnabled domStorageEnabled diff --git a/app/views/LanguageView/index.tsx b/app/views/LanguageView/index.tsx index a196dedda43..96c46d04431 100644 --- a/app/views/LanguageView/index.tsx +++ b/app/views/LanguageView/index.tsx @@ -4,6 +4,7 @@ import RNRestart from 'react-native-restart'; import { useDispatch } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ListRadio from '../../containers/List/ListRadio'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; @@ -21,6 +22,8 @@ import log, { events, logEvent } from '../../lib/methods/helpers/log'; import { saveUserPreferences } from '../../lib/services/restApi'; const LanguageView = () => { + 'use memo'; + const { languageDefault, id } = useAppSelector(state => ({ languageDefault: getUserSelector(state).language, id: getUserSelector(state).id @@ -29,6 +32,9 @@ const LanguageView = () => { const dispatch = useDispatch(); const navigation = useNavigation>(); + const { bottom } = useSafeAreaInsets(); + + const paddingBottom = Math.max(16, bottom); useLayoutEffect(() => { navigation.setOptions({ @@ -95,7 +101,7 @@ const LanguageView = () => { keyExtractor={item => item.value} ListHeaderComponent={List.Separator} ListFooterComponent={List.Separator} - contentContainerStyle={List.styles.contentContainerStyleFlatList} + contentContainerStyle={[List.styles.contentContainerStyleFlatList, { paddingBottom }]} renderItem={({ item }) => ( { const { showActionSheet, hideActionSheet } = useActionSheet(); const { colors } = useTheme(); - const insets = useSafeAreaInsets(); const option = OPTIONS.find(option => option.value === value) || OPTIONS[2]; const getOptions = (): ReactElement => ( - + {OPTIONS.map(i => ( diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 5ac8fc574fd..442bf5b41de 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { type CompositeNavigationProp, type RouteProp } from '@react-navigation/core'; +import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { type MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; @@ -55,6 +56,7 @@ interface IMessagesViewProps { showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void; useRealName: boolean; isMasterDetail: boolean; + insets: EdgeInsets; } interface IMessagesViewState { @@ -358,7 +360,7 @@ class MessagesView extends Component { render() { const { messages, loading } = this.state; - const { theme } = this.props; + const { theme, insets } = this.props; if (!loading && messages.length === 0) { return this.renderEmpty(); @@ -372,6 +374,7 @@ class MessagesView extends Component { style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} keyExtractor={item => item._id} onEndReached={this.load} + contentContainerStyle={{ paddingBottom: insets.bottom }} ListFooterComponent={loading ? : null} /> @@ -386,4 +389,4 @@ const mapStateToProps = (state: IApplicationState) => ({ useRealName: state.settings.UI_Use_Real_Name }); -export default connect(mapStateToProps)(withTheme(withActionSheet(withMasterDetail(MessagesView)))); +export default connect(mapStateToProps)(withTheme(withActionSheet(withMasterDetail(withSafeAreaInsets(MessagesView))))); diff --git a/app/views/NewMessageView/index.tsx b/app/views/NewMessageView/index.tsx index f0929a2ba66..95a15220a33 100644 --- a/app/views/NewMessageView/index.tsx +++ b/app/views/NewMessageView/index.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react import { FlatList } from 'react-native'; import { shallowEqual } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ActivityIndicator from '../../containers/ActivityIndicator'; import * as HeaderButton from '../../containers/Header/components/HeaderButton'; @@ -38,6 +39,7 @@ const NewMessageView = () => { const { colors } = useTheme(); const navigation = useNavigation>(); + const { bottom } = useSafeAreaInsets(); const { maxUsers, useRealName } = useAppSelector( state => ({ @@ -132,6 +134,7 @@ const NewMessageView = () => { ItemSeparatorComponent={List.Separator} ListFooterComponent={searching ? () => : List.Separator} style={{ backgroundColor: colors.surfaceTint }} + contentContainerStyle={{ paddingBottom: bottom }} keyboardShouldPersistTaps='always' /> diff --git a/app/views/NewServerView/index.tsx b/app/views/NewServerView/index.tsx index 4e3565fe53b..a86ebc86675 100644 --- a/app/views/NewServerView/index.tsx +++ b/app/views/NewServerView/index.tsx @@ -53,7 +53,6 @@ const NewServerView = () => { const { submit } = useConnectServer({ workspaceUrl, certificate, previousServer }); const phoneMarginTop = previousServer ? 32 : 84; const marginTop = isTablet ? 0 : phoneMarginTop; - const formContainerStyle = previousServer ? { paddingBottom: 100 } : {}; const onChangeText = (text: string) => { setValue('workspaceUrl', text); @@ -148,11 +147,7 @@ const NewServerView = () => { }, [connecting, previousServer]); return ( - + { const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const [data, setData] = useState(paramData); const [total, setTotal] = useState(paramTotal ?? 0); const [searchText, setSearchText] = useState(''); @@ -106,6 +108,7 @@ const PickerView = (): ReactElement => { onEndReachedThreshold={0.5} ItemSeparatorComponent={List.Separator} ListHeaderComponent={} + contentContainerStyle={{ paddingBottom: bottom }} ListFooterComponent={List.Separator} ListEmptyComponent={() => ( {I18n.t('No_results_found')} diff --git a/app/views/ProfileView/index.tsx b/app/views/ProfileView/index.tsx index 6b4366d58da..451fe06adc9 100644 --- a/app/views/ProfileView/index.tsx +++ b/app/views/ProfileView/index.tsx @@ -2,6 +2,7 @@ import { type NativeStackNavigationOptions, type NativeStackNavigationProp } fro import { type ReactElement, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { Keyboard, ScrollView, View, type TextInput } from 'react-native'; import { useDispatch } from 'react-redux'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; @@ -58,6 +59,7 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { const { showActionSheet, hideActionSheet } = useActionSheet(); const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const dispatch = useDispatch(); const { Accounts_AllowDeleteOwnAccount, @@ -292,7 +294,10 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { diff --git a/app/views/ReadReceiptView/index.tsx b/app/views/ReadReceiptView/index.tsx index 19544f1d206..b6a4115d1f6 100644 --- a/app/views/ReadReceiptView/index.tsx +++ b/app/views/ReadReceiptView/index.tsx @@ -3,8 +3,10 @@ import { dequal } from 'dequal'; import { connect } from 'react-redux'; import { type NativeStackNavigationOptions, type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { type RouteProp } from '@react-navigation/core'; +import { type EdgeInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; +import { withSafeAreaInsets } from '../../lib/hooks/withSafeAreaInsets'; import dayjs from '../../lib/dayjs'; import * as List from '../../containers/List'; import Avatar from '../../containers/Avatar'; @@ -32,6 +34,7 @@ interface INavigationOption { interface IReadReceiptViewProps extends INavigationOption { Message_TimeAndDateFormat: string; theme: TSupportedThemes; + insets: EdgeInsets; } class ReadReceiptView extends Component { @@ -138,7 +141,7 @@ class ReadReceiptView extends Component @@ -147,7 +150,7 @@ class ReadReceiptView extends Component ({ Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat as string }); -export default connect(mapStateToProps)(withTheme(ReadReceiptView)); +export default connect(mapStateToProps)(withTheme(withSafeAreaInsets(ReadReceiptView))); diff --git a/app/views/RoomInfoEditView/index.tsx b/app/views/RoomInfoEditView/index.tsx index 7fbc27dfa7f..b09642bab89 100644 --- a/app/views/RoomInfoEditView/index.tsx +++ b/app/views/RoomInfoEditView/index.tsx @@ -2,6 +2,7 @@ import { useLayoutEffect, useState } from 'react'; import { BlockContext } from '@rocket.chat/ui-kit'; import { dequal } from 'dequal'; import { AccessibilityInfo, Alert, Keyboard, ScrollView, Text, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useForm } from 'react-hook-form'; import { type SetValueConfig } from 'react-hook-form'; import * as yup from 'yup'; @@ -51,6 +52,7 @@ const schema = yup.object().shape({ const RoomInfoEditView = ({ navigation, route }: IRoomInfoEditViewProps) => { const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const { encryptionEnabled, serverVersion } = useAppSelector(state => ({ serverVersion: state.server.version as string, encryptionEnabled: state.encryption.enabled @@ -336,7 +338,7 @@ const RoomInfoEditView = ({ navigation, route }: IRoomInfoEditViewProps) => { diff --git a/app/views/RoomMembersView/index.tsx b/app/views/RoomMembersView/index.tsx index 506495d3186..f448ed61cc7 100644 --- a/app/views/RoomMembersView/index.tsx +++ b/app/views/RoomMembersView/index.tsx @@ -2,6 +2,7 @@ import { type NavigationProp, type RouteProp, useNavigation, useRoute } from '@r import { type ReactElement, useCallback, useEffect, useReducer, useRef } from 'react'; import { FlatList, Text, View } from 'react-native'; import { shallowEqual } from 'react-redux'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { type TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; import { sendLoadingEvent } from '../../containers/Loading'; @@ -75,6 +76,7 @@ const RoomMembersView = (): ReactElement => { const { params } = useRoute>(); const navigation = useNavigation>(); + const { bottom } = useSafeAreaInsets(); const latestSearchRequest = useRef(0); @@ -434,6 +436,7 @@ const RoomMembersView = (): ReactElement => { )} style={styles.list} + contentContainerStyle={{ paddingBottom: bottom }} keyExtractor={item => item._id} ItemSeparatorComponent={List.Separator} ListHeaderComponent={ diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index f2b3c882a1d..f01b818c42c 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1526,14 +1526,15 @@ class RoomView extends Component { renderFooter = () => { const { joined, room, readOnly, loading } = this.state; - const { theme, airGappedRestrictionRemainingDays } = this.props; + const { theme, insets, airGappedRestrictionRemainingDays } = this.props; + const footerBottomInset = { paddingBottom: insets.bottom }; if (!this.rid) { return null; } if ('onHold' in room && room.onHold) { return ( - + {I18n.t('Chat_is_on_hold')} { } if (!joined) { return ( - + {I18n.t('You_are_in_preview_mode')} { } if (airGappedRestrictionRemainingDays !== undefined && airGappedRestrictionRemainingDays === 0) { return ( - + {I18n.t('AirGapped_workspace_read_only_title')} @@ -1575,14 +1576,14 @@ class RoomView extends Component { } if (readOnly) { return ( - + {I18n.t('This_room_is_read_only')} ); } if ('id' in room && isBlocked(room)) { return ( - + {I18n.t('This_room_is_blocked')} ); @@ -1593,7 +1594,7 @@ class RoomView extends Component { if (description) { return ( - + {description} ); diff --git a/app/views/RoomsListView/components/ServersList.tsx b/app/views/RoomsListView/components/ServersList.tsx index 1a15a4489dd..1eecc635086 100644 --- a/app/views/RoomsListView/components/ServersList.tsx +++ b/app/views/RoomsListView/components/ServersList.tsx @@ -1,6 +1,5 @@ import { memo, useLayoutEffect, useRef, useState } from 'react'; import { FlatList, Text, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { batch, useDispatch } from 'react-redux'; import { type Subscription } from 'rxjs'; @@ -38,7 +37,6 @@ const ServersList = () => { const server = useAppSelector(state => state.server.server); const isMasterDetail = useMasterDetail(); const { colors } = useTheme(); - const insets = useSafeAreaInsets(); useLayoutEffect(() => { const init = () => { @@ -125,8 +123,7 @@ const ServersList = () => { diff --git a/app/views/RoomsListView/index.tsx b/app/views/RoomsListView/index.tsx index 5c3e9c7f6e0..1873759c72b 100644 --- a/app/views/RoomsListView/index.tsx +++ b/app/views/RoomsListView/index.tsx @@ -1,7 +1,7 @@ import { useNavigation } from '@react-navigation/native'; import { memo, useContext, useEffect } from 'react'; import { BackHandler, FlatList, RefreshControl } from 'react-native'; -import { useSafeAreaFrame } from 'react-native-safe-area-context'; +import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context'; import { shallowEqual } from 'react-redux'; import ActivityIndicator from '../../containers/ActivityIndicator'; @@ -45,6 +45,7 @@ const RoomsListView = memo(function RoomsListView() { const isMasterDetail = useMasterDetail(); const navigation = useNavigation(); const { width } = useSafeAreaFrame(); + const { bottom } = useSafeAreaInsets(); const getItemLayout = useGetItemLayout(); const { subscriptions, loading } = useSubscriptions(); const subscribedRoom = useAppSelector(state => state.room.subscribedRoom); @@ -132,6 +133,7 @@ const RoomsListView = memo(function RoomsListView() { extraData={searchEnabled ? searchResults : subscriptions} keyExtractor={item => `${item.rid}-${searchEnabled}`} style={[styles.list, { backgroundColor: colors.surfaceRoom }]} + contentContainerStyle={{ paddingBottom: bottom }} renderItem={renderItem} ListHeaderComponent={ListHeader} ListFooterComponent={searching ? () => : undefined} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index e931573cb7f..4f58b84e1df 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -4,8 +4,10 @@ import { FlatList, Text, View } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; +import { type EdgeInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; +import { withSafeAreaInsets } from '../../lib/hooks/withSafeAreaInsets'; import { FormTextInput } from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; import Markdown from '../../containers/markdown'; @@ -78,6 +80,7 @@ interface ISearchMessagesViewProps extends INavigationOption { theme: TSupportedThemes; useRealName: boolean; isMasterDetail: boolean; + insets: EdgeInsets; } class SearchMessagesView extends Component { private offset: number; @@ -307,7 +310,7 @@ class SearchMessagesView extends Component { const { messages, loading, searchText } = this.state; - const { theme } = this.props; + const { theme, insets } = this.props; if (!loading && messages.length === 0 && searchText.length) { return this.renderEmpty(); @@ -318,6 +321,7 @@ class SearchMessagesView extends Component item._id} onEndReached={this.onEndReached} ListFooterComponent={loading ? : null} @@ -357,4 +361,4 @@ const mapStateToProps = (state: any) => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(withTheme(withMasterDetail(SearchMessagesView))); +export default connect(mapStateToProps)(withTheme(withMasterDetail(withSafeAreaInsets(SearchMessagesView)))); diff --git a/app/views/SelectListView.tsx b/app/views/SelectListView.tsx index 44c259e1f4c..39f193bbfca 100644 --- a/app/views/SelectListView.tsx +++ b/app/views/SelectListView.tsx @@ -1,6 +1,7 @@ import { type NativeStackNavigationOptions, type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { FlatList, StyleSheet, Text, View } from 'react-native'; import { type RouteProp } from '@react-navigation/native'; +import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; import { type ChatsStackParamList } from '../stacks/types'; @@ -38,6 +39,7 @@ interface ISelectListViewProps { route: RouteProp; theme: TSupportedThemes; isMasterDetail: boolean; + insets: EdgeInsets; } class SelectListView extends Component { @@ -190,7 +192,7 @@ class SelectListView extends Component item.rid} renderItem={this.renderItem} ListHeaderComponent={this.isSearch ? this.renderSearch : this.renderInfoText} - contentContainerStyle={{ backgroundColor: themes[theme].surfaceRoom }} + contentContainerStyle={{ backgroundColor: themes[theme].surfaceRoom, paddingBottom: insets.bottom }} keyboardShouldPersistTaps='always' /> @@ -207,4 +209,4 @@ class SelectListView extends Component item.id; const SelectServerView = () => { + 'use memo'; + const [servers, setServers] = useState([]); const dispatch = useDispatch(); + const { bottom } = useSafeAreaInsets(); const server = useAppSelector(state => state.server.server); const navigation = useNavigation>(); const { fontScale } = useResponsiveLayout(); + const paddingBottom = Math.max(16, bottom); + const getItemLayout = useCallback( (_data: any, index: number) => { const height = PixelRatio.roundToNearestPixel(ROW_HEIGHT * fontScale); @@ -67,7 +73,7 @@ const SelectServerView = () => { keyExtractor={keyExtractor} getItemLayout={getItemLayout} ItemSeparatorComponent={List.Separator} - contentContainerStyle={List.styles.contentContainerStyleFlatList} + contentContainerStyle={[List.styles.contentContainerStyleFlatList, { paddingBottom }]} ListHeaderComponent={List.Separator} ListFooterComponent={List.Separator} removeClippedSubviews diff --git a/app/views/SelectedUsersView/index.tsx b/app/views/SelectedUsersView/index.tsx index 4cdd3fd4a7d..9ffcedcf323 100644 --- a/app/views/SelectedUsersView/index.tsx +++ b/app/views/SelectedUsersView/index.tsx @@ -6,6 +6,7 @@ import { shallowEqual, useDispatch } from 'react-redux'; import { type Subscription } from 'rxjs'; import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { addUser, removeUser, reset } from '../../actions/selectedUsers'; import ActivityIndicator from '../../containers/ActivityIndicator'; @@ -47,6 +48,7 @@ const SelectedUsersView = () => { const { colors } = useTheme(); const dispatch = useDispatch(); + const { bottom } = useSafeAreaInsets(); const { users, loading, useRealName, user } = useAppSelector( state => ({ @@ -202,7 +204,7 @@ const SelectedUsersView = () => { ItemSeparatorComponent={List.Separator} ListFooterComponent={searching ? : } ListHeaderComponent={
} - contentContainerStyle={{ backgroundColor: colors.surfaceRoom }} + contentContainerStyle={{ backgroundColor: colors.surfaceRoom, paddingBottom: bottom }} keyboardShouldPersistTaps='always' /> diff --git a/app/views/ShareListView/index.tsx b/app/views/ShareListView/index.tsx index e159da28701..effcd3e6ebb 100644 --- a/app/views/ShareListView/index.tsx +++ b/app/views/ShareListView/index.tsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import * as mime from 'react-native-mime-types'; import { dequal } from 'dequal'; import { Q } from '@nozbe/watermelondb'; +import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; import database from '../../lib/database'; @@ -61,6 +62,7 @@ interface IShareListViewProps extends INavigationOption { airGappedRestrictionRemainingDays: number | undefined; shareExtensionParams: Record; dispatch: Dispatch; + insets: EdgeInsets; } const getItemLayout = (_data: any, index: number) => { @@ -448,7 +450,7 @@ class ShareListView extends Component { render = () => { const { chats, loading, searchResults, searching, serversCount } = this.state; - const { theme } = this.props; + const { theme, insets } = this.props; if (loading) { return ; @@ -489,6 +491,7 @@ class ShareListView extends Component { data={searching ? searchResults : chats} keyExtractor={keyExtractor} style={[styles.flatlist, { backgroundColor: themes[theme].surfaceHover }]} + contentContainerStyle={{ paddingBottom: insets.bottom }} renderItem={this.renderItem} getItemLayout={getItemLayout} ItemSeparatorComponent={List.Separator} @@ -516,4 +519,4 @@ const mapStateToProps = ({ login, server, share, settings }: IApplicationState) : undefined }); -export default connect(mapStateToProps)(withTheme(ShareListView)); +export default connect(mapStateToProps)(withTheme(withSafeAreaInsets(ShareListView))); diff --git a/app/views/StatusView/ClearAfterPicker/ClearAfterSheetContent.tsx b/app/views/StatusView/ClearAfterPicker/ClearAfterSheetContent.tsx index 5da52fb7cdc..d3a95c57c8b 100644 --- a/app/views/StatusView/ClearAfterPicker/ClearAfterSheetContent.tsx +++ b/app/views/StatusView/ClearAfterPicker/ClearAfterSheetContent.tsx @@ -1,7 +1,6 @@ import { DateTimePickerAndroid } from '@react-native-community/datetimepicker'; import { useState, Fragment, type ReactElement } from 'react'; import { View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useActionSheet } from '../../../containers/ActionSheet'; import * as List from '../../../containers/List'; @@ -23,7 +22,6 @@ const ClearAfterSheetContent = ({ initialValue, initialDate, onConfirm }: IClear const [pendingValue, setPendingValue] = useState(initialValue); const { colors } = useTheme(); const { showActionSheet, hideActionSheet } = useActionSheet(); - const insets = useSafeAreaInsets(); const pendingDate = initialDate ?? new Date(); const openAndroidDateTimePicker = (baseDate: Date) => { @@ -69,7 +67,7 @@ const ClearAfterSheetContent = ({ initialValue, initialDate, onConfirm }: IClear }; return ( - + {CLEAR_AFTER_OPTIONS.map(option => ( diff --git a/app/views/StatusView/ClearAfterPicker/DatePickerSheetContent.tsx b/app/views/StatusView/ClearAfterPicker/DatePickerSheetContent.tsx index 7c8890f5188..9651e8681ff 100644 --- a/app/views/StatusView/ClearAfterPicker/DatePickerSheetContent.tsx +++ b/app/views/StatusView/ClearAfterPicker/DatePickerSheetContent.tsx @@ -1,7 +1,6 @@ import DateTimePicker from '@react-native-community/datetimepicker'; import { useState, type ReactElement } from 'react'; import { View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useActionSheet } from '../../../containers/ActionSheet'; import Button from '../../../containers/Button'; @@ -20,10 +19,9 @@ const DatePickerSheetContent = ({ initialDate, onConfirm }: IDatePickerSheetCont const [pendingDate, setPendingDate] = useState(initialDate); const { colors } = useTheme(); const { hideActionSheet } = useActionSheet(); - const insets = useSafeAreaInsets(); return ( - + { const dispatch = useDispatch(); const { setOptions, goBack } = useNavigation(); const { colors } = useTheme(); + const { bottom } = useSafeAreaInsets(); const submit = async () => { const { status } = inputValues; @@ -244,6 +246,7 @@ const StatusView = (): ReactElement => { /> } style={{ backgroundColor: colors.surfaceTint }} + contentContainerStyle={{ paddingBottom: bottom }} /> ); diff --git a/app/views/TeamChannelsView.tsx b/app/views/TeamChannelsView.tsx index a74ea7b3c15..43fbdd2f148 100644 --- a/app/views/TeamChannelsView.tsx +++ b/app/views/TeamChannelsView.tsx @@ -2,6 +2,7 @@ import { Q } from '@nozbe/watermelondb'; import { type NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { Alert, FlatList, Keyboard, PixelRatio } from 'react-native'; import { connect } from 'react-redux'; +import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; import { deleteRoom } from '../actions/room'; @@ -87,6 +88,7 @@ interface ITeamChannelsViewProps extends IBaseScreen void; showAvatar: boolean; displayMode: DisplayMode; + insets: EdgeInsets; } class TeamChannelsView extends Component { private teamId: string; @@ -549,6 +551,7 @@ class TeamChannelsView extends Component { const { loading, data, search, isSearching, searchText } = this.state; + const { insets } = this.props; if (loading) { return ; } @@ -570,6 +573,7 @@ class TeamChannelsView extends Component this.load()} onEndReachedThreshold={0.5} + contentContainerStyle={{ paddingBottom: insets.bottom }} ListFooterComponent={this.renderFooter} /> ); @@ -601,4 +605,6 @@ const mapStateToProps = (state: IApplicationState) => ({ displayMode: state.sortPreferences.displayMode }); -export default connect(mapStateToProps)(withDimensions(withTheme(withActionSheet(withMasterDetail(TeamChannelsView))))); +export default connect(mapStateToProps)( + withDimensions(withTheme(withActionSheet(withMasterDetail(withSafeAreaInsets(TeamChannelsView))))) +); diff --git a/app/views/ThreadMessagesView/index.tsx b/app/views/ThreadMessagesView/index.tsx index 80f05939ea8..50f838344dd 100644 --- a/app/views/ThreadMessagesView/index.tsx +++ b/app/views/ThreadMessagesView/index.tsx @@ -4,6 +4,7 @@ import { Q } from '@nozbe/watermelondb'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { type NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { type Observable, type Subscription } from 'rxjs'; +import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { Component } from 'react'; import { showActionSheetRef } from '../../containers/ActionSheet'; @@ -66,6 +67,7 @@ interface IThreadMessagesViewProps extends IBaseScreen { @@ -491,7 +493,7 @@ class ThreadMessagesView extends Component { const { loading, messages, displayingThreads, currentFilter } = this.state; - const { theme } = this.props; + const { theme, insets } = this.props; if (!messages?.length || !displayingThreads?.length) { let text; if (currentFilter === Filter.Following) { @@ -510,7 +512,7 @@ class ThreadMessagesView extends Component ({ useRealName: state.settings.UI_Use_Real_Name as boolean }); -export default connect(mapStateToProps)(withTheme(withMasterDetail(ThreadMessagesView))); +export default connect(mapStateToProps)(withTheme(withMasterDetail(withSafeAreaInsets(ThreadMessagesView)))); diff --git a/app/views/UserNotificationPreferencesView/ListPicker.tsx b/app/views/UserNotificationPreferencesView/ListPicker.tsx index 815b22666bb..ec766ce7a99 100644 --- a/app/views/UserNotificationPreferencesView/ListPicker.tsx +++ b/app/views/UserNotificationPreferencesView/ListPicker.tsx @@ -1,5 +1,4 @@ import { StyleSheet, Text, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Fragment, type ReactElement } from 'react'; import * as List from '../../containers/List'; @@ -37,10 +36,9 @@ const ListPicker = ({ const { showActionSheet, hideActionSheet } = useActionSheet(); const { colors } = useTheme(); const option = value ? OPTIONS[preference].find(option => option.value === value) : OPTIONS[preference][0]; - const insets = useSafeAreaInsets(); const getOptions = (): ReactElement => ( - + {OPTIONS[preference].map(i => ( diff --git a/package.json b/package.json index 37b8c598ebd..cb488c56114 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@rocket.chat/mobile-crypto": "RocketChat/rocket.chat-mobile-crypto#main", "@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile", "@rocket.chat/ui-kit": "^0.39.0", + "@zoontek/react-native-navigation-bar": "^1.1.1", "axios": "0.30.3", "bytebuffer": "~5.0.1", "color2k": "1.2.4", @@ -70,7 +71,6 @@ "expo-image": "~3.0.11", "expo-keep-awake": "~15.0.8", "expo-local-authentication": "~17.0.8", - "expo-navigation-bar": "~5.0.10", "expo-notifications": "~0.32.15", "expo-status-bar": "~3.0.9", "expo-system-ui": "~6.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55b107cf318..eafb0f810ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@rocket.chat/ui-kit': specifier: ^0.39.0 version: 0.39.0(@rocket.chat/icons@0.47.0)(@types/node@25.0.3)(typescript@5.9.3) + '@zoontek/react-native-navigation-bar': + specifier: ^1.1.1 + version: 1.1.2(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) axios: specifier: 0.30.3 version: 0.30.3 @@ -150,9 +153,6 @@ importers: expo-local-authentication: specifier: ~17.0.8 version: 17.0.8(expo@54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-navigation-bar: - specifier: ~5.0.10 - version: 5.0.10(expo@54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-notifications: specifier: ~0.32.15 version: 0.32.15(expo@54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -3070,6 +3070,12 @@ packages: '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + '@zoontek/react-native-navigation-bar@1.1.2': + resolution: {integrity: sha512-ZEH516aS7Gvo6JDnpEBmH5geECnnzCCbh9jOGovywCjwMeOJxSlbcjkq3UFBWyqCeeQFsxTblhoszG/0nP0Wpw==} + peerDependencies: + react: '*' + react-native: '*' + abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead @@ -4481,13 +4487,6 @@ packages: react: '*' react-native: '*' - expo-navigation-bar@5.0.10: - resolution: {integrity: sha512-r9rdLw8mY6GPMQmVVOY/r1NBBw74DZefXHF60HxhRsdNI2kjc1wLdfWfR2rk4JVdOvdMDujnGrc9HQmqM3n8Jg==} - peerDependencies: - expo: '*' - react: '*' - react-native: '*' - expo-notifications@0.32.15: resolution: {integrity: sha512-gnJcauheC2S0Wl0RuJaFkaBRVzCG011j5hlG0TEbsuOCPBuB/F30YEk8yurK8Psv+zHkVfeiJ5AC+nL0LWk0WA==} peerDependencies: @@ -11165,6 +11164,11 @@ snapshots: '@yarnpkg/lockfile@1.1.0': {} + '@zoontek/react-native-navigation-bar@1.1.2(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0) + abab@2.0.6: {} abort-controller@3.0.0: @@ -12741,17 +12745,6 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0) - expo-navigation-bar@5.0.10(expo@54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): - dependencies: - '@react-native/normalize-colors': 0.81.5 - debug: 4.4.3 - expo: 54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react: 19.1.0 - react-native: 0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - transitivePeerDependencies: - - supports-color - expo-notifications@0.32.15(expo@54.0.30(@babel/core@7.25.9)(react-native-webview@13.16.1(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.25.9)(@react-native-community/cli@20.0.0(typescript@5.9.3))(@react-native/metro-config@0.81.5(@babel/core@7.25.9))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.8