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