Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/android-tablet-safe-area-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Android Tablet Safe Area Handling Fix

## Problem
On Android tablets, the new call view did not hide or properly handle the bottom Android system navigation bar, causing it to overlap with the bottom buttons of the form.

## Root Cause
The new call screen was not using proper safe area handling for Android devices, specifically:
1. Missing `FocusAwareStatusBar` component for edge-to-edge experience
2. No safe area insets applied to prevent overlap with system UI
3. Bottom buttons were positioned without considering system navigation bar

## Solution

### 1. Added FocusAwareStatusBar Component
Added `FocusAwareStatusBar` import and usage to ensure proper edge-to-edge handling on Android:

```tsx
import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';

// In component render:
<FocusAwareStatusBar />
```

The `FocusAwareStatusBar` component automatically:
- Makes the navigation bar transparent with overlay behavior
- Sets system UI flags to hide navigation bar when needed
- Provides a seamless edge-to-edge experience

### 2. Added Safe Area Insets
Imported and used `useSafeAreaInsets` from `react-native-safe-area-context`:

```tsx
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const insets = useSafeAreaInsets();
```

### 3. Applied Safe Area Padding
Applied safe area insets to both top and bottom of the screen:

**Top Padding (ScrollView):**
```tsx
<ScrollView
className="flex-1 px-4 py-6"
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 16) }}
style={{ paddingTop: Math.max(insets.top, 16) }}
>
```

**Bottom Padding (Button Container):**
```tsx
<Box
className="mb-6 flex-row space-x-4"
style={{ paddingBottom: Math.max(insets.bottom, 16) }}
>
```

### 4. Safe Area Implementation Details

- **Minimum Padding**: Uses `Math.max(insets.bottom, 16)` to ensure at least 16px of padding even when insets are smaller
- **Dynamic Padding**: Adapts to different device configurations and orientations
- **Android Tablets**: Typical navigation bar height is ~48px, which gets properly handled
- **Cross-Platform**: Works on both iOS and Android devices

## Benefits

1. **No UI Overlap**: Bottom buttons are no longer hidden behind the system navigation bar
2. **Professional Appearance**: Provides a seamless edge-to-edge experience
3. **Device Compatibility**: Works across different Android tablet sizes and configurations
4. **Accessibility**: Ensures all interactive elements are accessible to users
5. **Consistent UX**: Matches the behavior of other screens in the app

## Files Modified

- `/src/app/call/new/index.tsx`: Added safe area handling and FocusAwareStatusBar

## Testing

The fix should be tested on:
1. Android tablets with different screen sizes
2. Devices with different navigation bar heights
3. Both portrait and landscape orientations
4. Light and dark themes

## Future Considerations

This pattern should be applied to other screens that might have similar issues with system UI overlap on Android devices.
2 changes: 1 addition & 1 deletion expo-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/// <reference types="expo/types" />

// NOTE: This file should not be edited and should be in your git ignore
// NOTE: This file should not be edited and should be in your git ignore
2 changes: 1 addition & 1 deletion jest-platform-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ Object.defineProperty(global, 'Platform', {
jest.doMock('react-native/Libraries/Utilities/Platform', () => mockPlatform);

// Ensure Platform is available in the global scope for React Navigation and other libs
(global as any).Platform = mockPlatform;
(global as any).Platform = mockPlatform;
24 changes: 16 additions & 8 deletions src/api/calls/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,20 @@ export const createCall = async (callData: CreateCallRequest) => {
const dispatchEntries: string[] = [];

if (callData.dispatchUsers) {
dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
//dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
//dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
//dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
//dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
dispatchEntries.push(...callData.dispatchUnits);
}

dispatchList = dispatchEntries.join('|');
Expand Down Expand Up @@ -130,16 +134,20 @@ export const updateCall = async (callData: UpdateCallRequest) => {
const dispatchEntries: string[] = [];

if (callData.dispatchUsers) {
dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
//dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
//dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
//dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
//dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
dispatchEntries.push(...callData.dispatchUnits);
}

dispatchList = dispatchEntries.join('|');
Expand Down
2 changes: 2 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { APIProvider } from '@/api';
import { CountlyProvider } from '@/components/common/countly-provider';
import { LiveKitBottomSheet } from '@/components/livekit';
import { PushNotificationModal } from '@/components/push-notification/push-notification-modal';
import { ToastContainer } from '@/components/toast/toast-container';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import { loadKeepAliveState } from '@/lib/hooks/use-keep-alive';
import { loadSelectedTheme } from '@/lib/hooks/use-selected-theme';
Expand Down Expand Up @@ -181,6 +182,7 @@ function Providers({ children }: { children: React.ReactNode }) {
<LiveKitBottomSheet />
<PushNotificationModal />
<FlashMessage position="top" />
<ToastContainer />
</BottomSheetModalProvider>
</ThemeProvider>
</GluestackUIProvider>
Expand Down
Loading
Loading