Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Sep 29, 2025

Summary by CodeRabbit

  • New Features

    • Added localization for Notification Inbox (English, Spanish, Arabic).
    • Manual refresh for dispatch recipients with improved empty/no-results states and retry.
  • Refactor

    • Replaced multiple list views for smoother scrolling.
    • Null-safe search checks and simplified shifts list configuration.
  • Documentation

    • Guides for Notification Inbox localization and translation keys.
  • Tests

    • Expanded tests to validate localized UI and messages.
  • Chores

    • Dependency updates, CI secrets for analytics integration, and editor theme settings.

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Replaces many FlatList usages with FlashList, adds a Jest FlashList mock, enhances dispatch store fetch/categorization with a refresh API, localizes NotificationInbox with new translation keys and docs, bumps dependencies, tweaks CI to include Countly secrets, and applies miscellaneous UI and editor setting changes.

Changes

Cohort / File(s) Summary of Changes
CI workflow secrets
.github/workflows/react-native-cicd.yml
Added secret-based env vars RESPOND_COUNTLY_APP_KEY and RESPOND_COUNTLY_URL; no step logic changes.
Editor settings
.vscode/settings.json
Added workbench color customizations and peacock.color; UI-only VSCode settings.
Dependency bumps
package.json
Updated versions for @expo/metro-runtime, @livekit/react-native, @livekit/react-native-webrtc, axios, expo, expo-router, livekit-client, and react-native.
API tweaks
src/api/calls/calls.ts, src/api/messaging/messages.ts
calls.ts: stopped encoding callId with encodeURIComponent; messages.ts: added a commented-out alternative recipients API line (no runtime effect).
Global FlatList → FlashList mapping
src/components/ui/flat-list/index.tsx, src/components/ui/actionsheet/index.tsx, src/components/ui/select/select-actionsheet.tsx
Re-exported/updated mappings so FlatList and actionsheet/select internals use FlashList from @shopify/flash-list instead of RN FlatList.
Screen list migrations (FlashList)
src/app/(app)/contacts.tsx, src/app/(app)/home/personnel.tsx, src/app/(app)/home/units.tsx, src/app/(app)/notes.tsx, src/app/(app)/protocols.tsx, src/app/onboarding.tsx, src/components/notifications/NotificationInbox.tsx
Replaced RN FlatList with FlashList imports/usages; preserved props/behavior; added minor optional chaining in some filters; NotificationInbox now uses i18n.
Shifts list props cleanup
src/app/(app)/shifts.tsx
Removed several FlatList perf props (removeClippedSubviews, maxToRenderPerBatch, windowSize, initialNumToRender).
Calls image modal → FlashList
src/components/calls/call-images-modal.tsx
Switched to FlashList, updated ref types, keyExtractor typings, and adjusted list-related props/scroll handling.
Dispatch selection modal overhaul
src/components/calls/dispatch-selection-modal.tsx
Reworked filtering to use useMemo for derived lists, ensure fetch on open, added refresh/retry flows and empty/no-results UI, updated layout/styling and some state handling.
Dispatch store enhancements
src/stores/dispatch/store.ts
Added refreshDispatchData() public method; strengthened fetchDispatchData with validation, categorization (users/groups/roles/units), dev logging, and improved loading/error state handling.
BottomSheet formatting
src/components/ui/bottomsheet/index.tsx
Whitespace/formatting-only changes; no behavioral changes.
Jest setup (FlashList mock)
jest-setup.ts
Added a mock for @shopify/flash-list that proxies to RN FlatList via forwardRef for tests.
Notification localization & docs
src/components/notifications/NotificationInbox.tsx, src/translations/en.json, src/translations/es.json, src/translations/ar.json, docs/notification-inbox-localization-implementation.md, docs/notification-inbox-translation-keys-verification.md
Introduced react-i18next usage in NotificationInbox, added notifications.* and common.* translation keys across languages, and added documentation describing the localization implementation and verification.
Notification tests (i18n)
src/components/notifications/__tests__/NotificationInbox.test.tsx
Mocked useTranslation in tests, added localization assertions and mocked translation strings.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Modal as DispatchSelectionModal
  participant Store as Dispatch Store
  participant API as Recipients API

  User->>Modal: open()
  activate Modal
  Modal->>Store: fetchDispatchData()
  activate Store
  Store->>API: GET /recipients
  API-->>Store: recipients data / error
  alt success
    Store->>Store: validate & categorize (users, groups, roles, units)
    Store-->>Modal: data (loading:false, error:null)
    Modal->>Modal: derive filtered lists (useMemo)
    Modal-->>User: render recipients / empty / no-results
  else error/invalid
    Store-->>Modal: set error state
    Modal-->>User: show error + Retry
    User->>Modal: Retry
    Modal->>Store: refreshDispatchData()
    Store->>API: GET /recipients (retry)
  end
  deactivate Store
  deactivate Modal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

Hop hop, I swapped the list with speed,
Flashing rows to do the deed.
I fetch and sort, then sing aloud,
Translated lines make users proud.
Secrets tucked beneath the sod—happy rabbit nods. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The pull request title "Develop" is the name of the source branch and does not describe any aspect of the actual changes in the changeset. The PR contains substantial updates including dependency upgrades (React Native, Expo, LiveKit, Axios), migration from FlatList to FlashList across multiple components, addition of Countly integration secrets, localization support for NotificationInbox, improvements to dispatch data handling with refresh capability, and various testing and configuration updates. The title fails to convey any meaningful information about these changes and is unrelated to the content of the changeset. The pull request title should be updated to clearly summarize the primary change from a developer's perspective. Consider titles such as "Migrate from FlatList to FlashList and upgrade core dependencies" or "Add FlashList implementation and dependency updates" or "Upgrade React Native ecosystem and implement FlashList migration". Choose a title that highlights the most significant change—likely the FlashList migration or the dependency upgrades—so teammates can quickly understand the main purpose of this PR when scanning the project history.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd24a4f and a24999e.

📒 Files selected for processing (1)
  • src/components/calls/call-images-modal.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/calls/call-images-modal.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/notifications/NotificationInbox.tsx (1)

232-233: Localize all user-facing strings with t()

Several literals bypass i18n. Wrap with useTranslation to meet guidelines and l10n goals.

Example minimal changes:

-  const renderEmpty = () => (
+  const { t } = useTranslation();
+  const renderEmpty = () => (
     <View style={styles.emptyContainer}>
-      <Text>No updates available</Text>
+      <Text>{t('notifications.empty', 'No updates available')}</Text>
     </View>
   );

Header/actions and modal:

-  <Text style={styles.headerTitle}>Notifications</Text>
+  <Text style={styles.headerTitle}>{t('notifications.title', 'Notifications')}</Text>

-  <Text>Unable to load notifications</Text>
+  <Text>{t('notifications.unableToLoad', 'Unable to load notifications')}</Text>

-  <Text className="text-lg font-semibold">Confirm Delete</Text>
+  <Text className="text-lg font-semibold">{t('notifications.confirmDeleteTitle', 'Confirm Delete')}</Text>

-  Are you sure you want to delete {selectedNotificationIds.size} notification{s}? This action cannot be undone.
+  {t('notifications.confirmDeleteBody', {
+    defaultValue: 'Are you sure you want to delete {{count}} notification{{suffix}}? This action cannot be undone.',
+    count: selectedNotificationIds.size,
+    suffix: selectedNotificationIds.size > 1 ? 's' : '',
+  })}

-  <Text>Cancel</Text>
+  <Text>{t('common.cancel', 'Cancel')}</Text>

-  <Text className="text-white">Delete</Text>
+  <Text className="text-white">{t('common.delete', 'Delete')}</Text>

I can open a follow-up PR adding these keys to the locale files.

Also applies to: 279-281, 298-299, 323-329, 332-336

src/app/(app)/home/personnel.tsx (1)

49-58: Null-safety: guard toLowerCase() on optional fields

Direct calls can throw if any field is null/undefined (e.g., FirstName, LastName, Roles entries).

-    return personnel.filter(
-      (person) =>
-        person.FirstName.toLowerCase().includes(query) ||
-        person.LastName.toLowerCase().includes(query) ||
-        person.EmailAddress?.toLowerCase().includes(query) ||
-        person.GroupName?.toLowerCase().includes(query) ||
-        person.Status?.toLowerCase().includes(query) ||
-        person.Staffing?.toLowerCase().includes(query) ||
-        person.IdentificationNumber?.toLowerCase().includes(query) ||
-        person.Roles?.some((role) => role.toLowerCase().includes(query))
-    );
+    return personnel.filter((person) =>
+      (person.FirstName?.toLowerCase().includes(query) ?? false) ||
+      (person.LastName?.toLowerCase().includes(query) ?? false) ||
+      (person.EmailAddress?.toLowerCase().includes(query) ?? false) ||
+      (person.GroupName?.toLowerCase().includes(query) ?? false) ||
+      (person.Status?.toLowerCase().includes(query) ?? false) ||
+      (person.Staffing?.toLowerCase().includes(query) ?? false) ||
+      (person.IdentificationNumber?.toLowerCase().includes(query) ?? false) ||
+      (person.Roles?.some((role) => role?.toLowerCase().includes(query)) ?? false)
+    );
src/app/(app)/home/units.tsx (1)

55-60: Null-safety: guard toLowerCase() on optional fields

Protect against undefined values to avoid crashes on malformed data.

-    return units.filter(
-      (unit) =>
-        unit.Name.toLowerCase().includes(query) ||
-        unit.Type.toLowerCase().includes(query) ||
-        unit.PlateNumber?.toLowerCase().includes(query) ||
-        unit.Vin?.toLowerCase().includes(query) ||
-        unit.GroupName?.toLowerCase().includes(query)
-    );
+    return units.filter((unit) =>
+      (unit.Name?.toLowerCase().includes(query) ?? false) ||
+      (unit.Type?.toLowerCase().includes(query) ?? false) ||
+      (unit.PlateNumber?.toLowerCase().includes(query) ?? false) ||
+      (unit.Vin?.toLowerCase().includes(query) ?? false) ||
+      (unit.GroupName?.toLowerCase().includes(query) ?? false)
+    );
🧹 Nitpick comments (34)
src/api/messaging/messages.ts (3)

16-17: Remove commented-out duplicate and use a config toggle instead.

Leaving an alternate recipientsApi declaration commented out adds noise and invites accidental shadowing if uncommented. Prefer a single declaration with a config-driven switch.

Example diff to remove the commented code and make caching toggleable:

-//const recipientsApi = createApiEndpoint('/Messages/GetRecipients');
+// To disable caching for troubleshooting, flip this flag via env/config instead of swapping implementations.
+const CACHE_RECIPIENTS_ENABLED = true; // e.g., Boolean(process.env.EXPO_PUBLIC_CACHE_RECIPIENTS)

11-14: Confirm 1‑day TTL is acceptable for recipients freshness.

A 24h cache can stale membership/permissions after roster/unit changes. If freshness is critical, consider a shorter TTL (e.g., 5–15m) or explicit invalidation after mutating actions (user joins/leaves unit, role changes).


8-14: Align caching with react-query where possible.

Guidelines call for react-query for data fetching/caching. If feasible, expose this endpoint as a typed query (with staleTime, cache keys, and invalidation) and keep this API layer thin. This centralizes cache policy and avoids bespoke TTL logic.

As per coding guidelines

src/translations/en.json (1)

355-356: Approve additions; correct verification script and confirm usage.

  • Remove the erroneous search key from the locale‐check and adjust the ripgrep call to include .tsx (e.g. drop --type flags or add --type-add 'tsx:*.tsx').
  • Run a broader search such as
    rg -n "no_data_available|no_results_found" -C1
    
    to locate UI references and ensure the new keys are actually consumed.
.vscode/settings.json (3)

40-45: Unify formatter configuration to avoid churn on save (ESLint vs Prettier vs TS extension).

You’re setting ESLint as the formatter for JS/TS/TSX (lines 11-18) but overriding TS/TSX to different formatters here. This often causes inconsistent edits or flicker on save. Suggest picking one strategy.

Option A (common): Prettier formats; ESLint fixes via code actions.

  • Set defaultFormatter to Prettier for JS/TS/TSX.
  • Keep "source.fixAll.eslint" on save.
  • Disable ESLint-as-formatter.

Apply these diffs:

-  "[javascript][typescript][typescriptreact]": {
-    "editor.formatOnSave": true,
-    "editor.defaultFormatter": "dbaeumer.vscode-eslint",
-    "editor.codeActionsOnSave": [
-      "source.addMissingImports",
-      "source.fixAll.eslint"
-    ]
-  },
+  "[javascript][typescript][typescriptreact]": {
+    "editor.formatOnSave": true,
+    "editor.defaultFormatter": "esbenp.prettier-vscode",
+    "editor.codeActionsOnSave": [
+      "source.addMissingImports",
+      "source.fixAll.eslint"
+    ]
+  },
-  "eslint.format.enable": true,
+  "eslint.format.enable": false,
-  "[typescriptreact]": {
-    "editor.defaultFormatter": "vscode.typescript-language-features"
-  },
-  "[typescript]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[typescript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },

Option B: ESLint formats everything (if using eslint-plugin-prettier).

  • Keep ESLint as default formatter for JS/TS/TSX and remove the TS/TSX overrides above.
  • Ensure prettier rules flow through ESLint.
    Pick one path to prevent formatter tug-of-war.

Also applies to: 11-18


46-65: Workspace theme overrides may be intrusive; confirm team intent.

Adding workbench.colorCustomizations forces UI colors for all contributors. If that’s intentional (branding/Peacock workflow), fine. Otherwise consider relying on Peacock’s single color or moving these to user settings to avoid overriding personal themes.

If you prefer to keep just Peacock:

-  "workbench.colorCustomizations": {
-    "activityBar.activeBackground": "#8804a5",
-    "activityBar.background": "#8804a5",
-    "activityBar.foreground": "#e7e7e7",
-    "activityBar.inactiveForeground": "#e7e7e799",
-    "activityBarBadge.background": "#9c8004",
-    "activityBarBadge.foreground": "#e7e7e7",
-    "commandCenter.border": "#e7e7e799",
-    "sash.hoverBorder": "#8804a5",
-    "statusBar.background": "#5f0373",
-    "statusBar.foreground": "#e7e7e7",
-    "statusBarItem.hoverBackground": "#8804a5",
-    "statusBarItem.remoteBackground": "#5f0373",
-    "statusBarItem.remoteForeground": "#e7e7e7",
-    "titleBar.activeBackground": "#5f0373",
-    "titleBar.activeForeground": "#e7e7e7",
-    "titleBar.inactiveBackground": "#5f037399",
-    "titleBar.inactiveForeground": "#e7e7e799"
-  },
+  // Keep Peacock for easy workspace identification; avoid overriding contributors' themes.

27-33: cSpell: add “FlashList” proper case to avoid flags.

You have “Flashlist”; adding “FlashList” ensures both variants are whitelisted and avoids noise during the migration.

   "cSpell.words": [
-    "Flashlist",
+    "Flashlist",
+    "FlashList",
     "Gluestack",
     "Lato",
     "nativewind",
     "Resgrid"
   ],
src/api/calls/calls.ts (2)

81-104: DRY up duplicate dispatch list construction

createCall and updateCall duplicate the dispatch list logic. Extract a small helper to reduce drift and ease testing.

Example helper (placed in this module or a small util):

function computeDispatchList(input: {
  dispatchEveryone?: boolean;
  dispatchUsers?: string[];
  dispatchGroups?: string[];
  dispatchRoles?: string[];
  dispatchUnits?: string[];
}): string {
  if (input.dispatchEveryone) return '0';
  const entries: string[] = [];
  if (input.dispatchUsers?.length) entries.push(...input.dispatchUsers.map(u => `U:${u}`));
  if (input.dispatchGroups?.length) entries.push(...input.dispatchGroups.map(g => `G:${g}`));
  if (input.dispatchRoles?.length) entries.push(...input.dispatchRoles.map(r => `R:${r}`));
  if (input.dispatchUnits?.length) entries.push(...input.dispatchUnits.map(unit => `U:${unit}`)); // verify prefix per API
  return entries.join('|');
}

Then in both functions:

const dispatchList = computeDispatchList(callData);

Please verify the 'U:' prefix for both users and units is intentional per API.

Also applies to: 124-147


21-23: Use explicit return types and property shorthand; axios.get params handles encoding

Axios api.get wraps your { callId } in its params option, which is URL-encoded by default—no manual encodeURIComponent needed. Refactor exported functions:

-export const getCallExtraData = async (callId: string) => {
+export const getCallExtraData = async (callId: string): Promise<CallExtraDataResult> => {
   const response = await getCallExtraDataApi.get<CallExtraDataResult>({
-    callId: callId,
+    callId,
   });
   return response.data;
 };

-export const getCall = async (callId: string) => {
+export const getCall = async (callId: string): Promise<CallResult> => {
   const response = await getCallApi.get<CallResult>({
-    callId: callId,
+    callId,
   });
   return response.data;
 };
src/components/ui/bottomsheet/index.tsx (1)

190-195: Prefer ternary over && for conditional rendering

Align with repo guideline to use ?: instead of && in TSX. Apply:

-        {visible && (
-          <FocusScope contain={visible} autoFocus={true} restoreFocus={true}>
-            {props.children}
-          </FocusScope>
-        )}
+        {visible ? (
+          <FocusScope contain={visible} autoFocus={true} restoreFocus={true}>
+            {props.children}
+          </FocusScope>
+        ) : null}

As per coding guidelines

src/app/(app)/contacts.tsx (1)

84-92: Stabilize renderItem to reduce re-renders with FlashList

Avoid inline functions in renderItem; hoist into a memoized callback.

Apply within the list:

-            renderItem={({ item }) => <ContactCard contact={item} onPress={selectContact} />}
+            renderItem={renderContactItem}

Add near other hooks:

const renderContactItem = React.useCallback(
  ({ item }: { item: typeof filteredContacts[number] }) => (
    <ContactCard contact={item} onPress={selectContact} />
  ),
  [selectContact]
);
jest-setup.ts (1)

683-694: Harden FlashList Jest mock exports

Expose __esModule and default to avoid ESM/CJS interop hiccups and future imports.

-jest.mock('@shopify/flash-list', () => {
+jest.mock('@shopify/flash-list', () => {
   const React = require('react');
   const { FlatList } = require('react-native');

-  return {
-    FlashList: React.forwardRef((props: any, ref: any) => {
-      return React.createElement(FlatList, { ...props, ref });
-    }),
-  };
-});
+  const FlashList = React.forwardRef((props, ref) => React.createElement(FlatList, { ...props, ref }));
+  return {
+    __esModule: true,
+    FlashList,
+    default: FlashList,
+  };
+});
src/components/notifications/NotificationInbox.tsx (2)

301-311: Type FlashList and memoize renderItem

Provide list generics and stable renderItem to minimize re-renders and improve DX.

-                <FlashList
-                  data={notifications}
-                  renderItem={renderItem}
-                  keyExtractor={(item: any) => item.id}
+                <FlashList<NotificationPayload>
+                  data={notifications as unknown as NotificationPayload[]}
+                  renderItem={renderItem}
+                  keyExtractor={(item) => item.id}

Also wrap renderItem in useCallback:

-  const renderItem = ({ item }: { item: any }) => {
+  const renderItem = React.useCallback(({ item }: { item: any }) => {
     // ...
-  };
+  }, [isSelectionMode, selectedNotificationIds]);

359-366: Avoid baking color scheme into static StyleSheet

colorScheme.get() runs once; styles won’t update on theme change. Prefer dynamic values from useColorScheme() or tailwind classes.

Light-touch option:

const isDark = colorScheme.get() === 'dark'; // or nativewind/useColorScheme()

Then move affected values to inline style arrays or conditional className (e.g., className="bg-white dark:bg-black"). This keeps the sidebar responsive to runtime theme toggles.

Also applies to: 417-425, 433-433, 445-445, 449-449, 452-452

src/app/(app)/home/personnel.tsx (1)

93-101: Hoist renderItem into a memoized callback

Avoid anonymous functions in renderItem; improves stability and perf with FlashList.

-            <FlashList
+            <FlashList
               data={filteredPersonnel}
               keyExtractor={(item, index) => item.UserId || `personnel-${index}`}
-              renderItem={({ item }) => <PersonnelCard personnel={item} onPress={selectPersonnel} />}
+              renderItem={renderPersonnelItem}

Add near hooks:

const renderPersonnelItem = React.useCallback(
  ({ item }: { item: typeof filteredPersonnel[number] }) => (
    <PersonnelCard personnel={item} onPress={selectPersonnel} />
  ),
  [selectPersonnel]
);
src/app/(app)/home/units.tsx (1)

95-102: Hoist renderItem into a memoized callback

Avoid anonymous functions in renderItem; stabilize identity for FlashList.

-            <FlashList
+            <FlashList
               data={filteredUnits}
               keyExtractor={(item, index) => item.UnitId || `unit-${index}`}
-              renderItem={({ item }) => <UnitCard unit={item as any} onPress={selectUnit} />}
+              renderItem={renderUnitItem}

Add near hooks:

const renderUnitItem = React.useCallback(
  ({ item }: { item: typeof filteredUnits[number] }) => <UnitCard unit={item as any} onPress={selectUnit} />,
  [selectUnit]
);
src/components/ui/actionsheet/index.tsx (1)

59-59: Map FlatList to FlashList with a web-safe fallback.

Since this Actionsheet is used across platforms and this file has explicit web handling, guard FlashList on web.

-  FlatList: FlashList,
+  FlatList: Platform.OS === 'web' ? VirtualizedList : FlashList,

Also sanity‑check that cssInterop keys used for FlatList (e.g., columnWrapperClassName, indicatorClassName) are honored by FlashList in your target versions.

src/app/(app)/protocols.tsx (1)

77-85: Avoid inline renderItem/keyExtractor to reduce unnecessary re-renders.

Extract and memoize the callbacks.

@@
-        {filteredProtocols.length > 0 ? (
-          <FlashList
+        {filteredProtocols.length > 0 ? (
+          <FlashList
             testID="protocols-list"
-            data={filteredProtocols}
-            keyExtractor={(item, index) => item.Id || `protocol-${index}`}
-            renderItem={({ item }) => <ProtocolCard protocol={item} onPress={selectProtocol} />}
+            data={filteredProtocols}
+            keyExtractor={keyExtractor}
+            renderItem={renderItem}
             showsVerticalScrollIndicator={false}
             contentContainerStyle={{ paddingBottom: 100 }}
             refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
           />
         ) : (

Add near the filtered list computation:

+  const keyExtractor = React.useCallback((item: { Id?: string }, index: number) => item.Id ?? `protocol-${index}`, []);
+  const renderItem = React.useCallback(
+    ({ item }: { item: any }) => <ProtocolCard protocol={item} onPress={selectProtocol} />,
+    [selectProtocol]
+  );
src/app/onboarding.tsx (2)

143-156: Add overrideItemLayout and memoize item callbacks for smoother paging and stable scrollToIndex.

With uniform, full‑width pages, provide the size and avoid inline callbacks.

@@
-      <FlashList
+      <FlashList
         ref={flatListRef}
         data={onboardingData}
-        renderItem={({ item }: { item: OnboardingItemProps }) => <OnboardingItem {...item} />}
+        renderItem={renderItem}
         horizontal
         showsHorizontalScrollIndicator={false}
         pagingEnabled
         bounces={false}
-        keyExtractor={(item: OnboardingItemProps) => item.title}
+        keyExtractor={keyExtractor}
         onScroll={handleScroll}
         scrollEventThrottle={16}
         testID="onboarding-flatlist"
+        overrideItemLayout={(layout) => {
+          layout.size = width; // width for horizontal lists
+          layout.span = 1;
+        }}
       />

Add above the return:

+  const renderItem = React.useCallback(
+    ({ item }: { item: OnboardingItemProps }) => <OnboardingItem {...item} />,
+    []
+  );
+  const keyExtractor = React.useCallback((item: OnboardingItemProps) => item.title, []);

175-181: Localize visible strings.

Wrap “Skip”, “Next”, and “Let’s Get Started” with t(...) per guidelines.

Also applies to: 203-204

src/components/ui/flat-list/index.tsx (1)

2-2: Also re-export the types to preserve the public typing surface.

Downstream code expecting FlatListProps/Ref will benefit from aliased type exports.

-'use client';
-export { FlashList as FlatList } from '@shopify/flash-list';
+'use client';
+export { FlashList as FlatList } from '@shopify/flash-list';
+export type {
+  FlashListProps as FlatListProps,
+  FlashListRef as FlatListRef,
+  ListRenderItem,
+  ListRenderItemInfo,
+} from '@shopify/flash-list';
src/app/(app)/notes.tsx (1)

111-118: Extract renderItem/keyExtractor to stable callbacks.

Reduces re-renders and keeps item identities stable.

-          <FlashList
-            data={filteredNotes}
-            keyExtractor={(item) => item.NoteId}
-            renderItem={({ item }) => <NoteCard note={item} onPress={handleNoteSelect} />}
+          <FlashList
+            data={filteredNotes}
+            keyExtractor={keyExtractor}
+            renderItem={renderItem}
             showsVerticalScrollIndicator={false}
             contentContainerStyle={{ paddingBottom: 100 }}
             refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
           />

Add above the return:

+  const keyExtractor = React.useCallback((item: { NoteId: string }) => item.NoteId, []);
+  const renderItem = React.useCallback(
+    ({ item }: { item: any }) => <NoteCard note={item} onPress={handleNoteSelect} />,
+    [handleNoteSelect]
+  );
src/components/calls/call-images-modal.tsx (3)

270-275: Prefer FlashList’s onVisibleIndicesChanged over onViewableItemsChanged

FlashList exposes onVisibleIndicesChanged for lighter, reliable index tracking. It also avoids viewabilityConfig complexity. Update to keep activeIndex in sync.

-  const handleViewableItemsChanged = useRef(({ viewableItems }: any) => {
-    if (viewableItems.length > 0) {
-      setActiveIndex(viewableItems[0].index || 0);
-    }
-  }).current;
+  const onVisibleIndicesChanged = useCallback((indices: number[]) => {
+    if (indices.length > 0) {
+      setActiveIndex(indices[0]);
+    }
+  }, []);
@@
-            onViewableItemsChanged={handleViewableItemsChanged}
-            viewabilityConfig={{
-              itemVisiblePercentThreshold: 50,
-              minimumViewTime: 100,
-            }}
+            onVisibleIndicesChanged={onVisibleIndicesChanged}

Also applies to: 398-402


36-36: Make snap width responsive to orientation

Dimensions.get('window') is static here. Use useWindowDimensions so snapToInterval tracks rotation/resizes.

-import { Alert, Dimensions, type ImageSourcePropType, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
+import { Alert, useWindowDimensions, type ImageSourcePropType, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
@@
-const { width } = Dimensions.get('window');
+// remove this line; use the hook inside the component
@@
-const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, callId }) => {
+const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, callId }) => {
+  const { width } = useWindowDimensions();
@@
-            snapToInterval={width}
+            snapToInterval={width}

Also applies to: 51-53, 403-405


196-269: Memoize renderItem to reduce re-renders

Avoid recreating renderImageItem every render. Also, the explicit key on the Image isn’t needed since keyExtractor is provided.

-  const renderImageItem = ({ item, index }: { item: CallFileResultData; index: number }) => {
+  const renderImageItem = useCallback(({ item, index }: { item: CallFileResultData; index: number }) => {
     if (!item) return null;
@@
-          <Image
-            key={`${item.Id}-${index}`}
+          <Image
             source={imageSource}
             style={styles.galleryImage}
             contentFit="contain"
             transition={200}
             pointerEvents="none"
@@
-  };
+  }, [imageErrors, t]);

As per coding guidelines (avoid anonymous functions in renderItem).

src/components/calls/dispatch-selection-modal.tsx (3)

35-49: De-duplicate filtering logic; use the store’s selector

The component reimplements getFilteredData. Use the store source of truth to prevent drift and reduce code.

-  // Calculate filtered data directly in component to ensure reactivity
-  const filteredData = useMemo(() => {
-    if (!searchQuery.trim()) {
-      return data;
-    }
-    const query = searchQuery.toLowerCase();
-    return {
-      users: data.users.filter((user) => user.Name.toLowerCase().includes(query)),
-      groups: data.groups.filter((group) => group.Name.toLowerCase().includes(query)),
-      roles: data.roles.filter((role) => role.Name.toLowerCase().includes(query)),
-      units: data.units.filter((unit) => unit.Name.toLowerCase().includes(query)),
-    };
-  }, [data, searchQuery]);
+  // Use store-provided computed data to avoid duplication
+  const filteredData = useDispatchStore((s) => s.getFilteredData());

285-291: Set icon colors via props (className may not affect lucide icon stroke)

lucide-react-native icons reliably take color/size; className color isn’t guaranteed. Pass color explicitly; keep spacing via style.

-          <UsersIcon size={24} className={colorScheme === 'dark' ? 'text-white' : 'text-neutral-900'} />
+          <UsersIcon size={24} color={colorScheme === 'dark' ? '#ffffff' : '#111827'} />
@@
-          <X size={24} className={colorScheme === 'dark' ? 'text-white' : 'text-neutral-900'} />
+          <X size={24} color={colorScheme === 'dark' ? '#ffffff' : '#111827'} />
@@
-          <SearchIcon size={20} className="ml-3 mr-2 text-neutral-500" />
+          <SearchIcon size={20} color="#737373" style={{ marginLeft: 12, marginRight: 8 }} />

As per coding guidelines: use lucide-react-native directly.

Also applies to: 297-299


329-435: Potential virtualization need if lists grow

Mapping over many users/groups/roles/units in a ScrollView may stutter. If recipient counts can be high, consider FlashList per section.

src/stores/dispatch/store.ts (1)

145-147: Consider a friendlier user-facing error

error.message may be technical. Optionally map to a generic message and log details only in dev.

FLATLIST_TO_FLASHLIST_MIGRATION.md (5)

56-69: Jest mock falls back to RN FlatList; may mask FlashList-specific regressions.

Document this caveat and consider at least one test path without the mock to exercise FlashList behavior.

Add under “Jest Configuration Updates”:

+ Note: This mock intentionally proxies to RN FlatList to simplify tests. It does not exercise FlashList’s measuring/recycling paths.
+ We run a small set of integration/component tests with the mock disabled to catch regressions (e.g., sticky headers, onEndReached).

Optional opt‑out pattern:

// jest-setup.ts
- jest.mock('@shopify/flash-list', () => { ... })
+ if (!process.env.DISABLE_FLASHLIST_MOCK) {
+   jest.mock('@shopify/flash-list', () => { ... })
+ }

And run one suite with DISABLE_FLASHLIST_MOCK=1.


14-17: Document exported types alongside the wrapper re-export.

Re-exporting only the component can degrade typing/auto‑complete for consumers. Recommend re‑exporting types too.

Add to the snippet:

- export { FlashList as FlatList } from '@shopify/flash-list';
+ export { FlashList as FlatList } from '@shopify/flash-list';
+ export type {
+   FlashListProps as FlatListProps,
+   FlashListRef as FlatListRef,
+ } from '@shopify/flash-list';

100-106: Add a note about changed defaults (maintainVisibleContentPosition) and inverted deprecation.

Helps teams migrating chat/feeds that relied on inverted or scroll position behavior.

Append:

 4. **Drop-in Replacement**: Maintains API compatibility with FlatList for most use cases
+5. **Changed defaults**: `maintainVisibleContentPosition` is enabled by default; prefer it over `inverted` patterns for chats.

124-129: Temper “No runtime errors expected” and list hotspots to sanity‑check.

A few behaviors commonly differ between FlatList and FlashList; documenting them sets expectations.

Replace:

- 4. ✅ No runtime errors expected (FlashList is API-compatible)
+ 4. ✅ Known hotspots validated: sticky headers, onEndReached thresholds, ListEmptyComponent sizing, keyboardShouldPersistTaps, maintainVisibleContentPosition behavior on prepend/appends.

46-53: Tighten prop support wording and clarify estimatedItemSize
Update FLATLIST_TO_FLASHLIST_MIGRATION.md as follows:

- Removed FlatList-specific props that are not supported by FlashList:
-- `estimatedItemSize` (was attempted but doesn't exist in FlashList)
+ Removed/ignored props under FlashList v2 (auto-measures items; v1 required `estimatedItemSize`):
+ - `estimatedItemSize` (removed/ignored in v2)
  - `getItemLayout` (not supported)
  - `initialNumToRender` (not supported)
  - `maxToRenderPerBatch` (not supported)
  - `windowSize` (not supported)
  - `removeClippedSubviews` (not supported)

Scan confirms no remaining references to these props in TS/TSX files.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45ff70c and ac84768.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (25)
  • .github/workflows/react-native-cicd.yml (1 hunks)
  • .vscode/settings.json (1 hunks)
  • FLATLIST_TO_FLASHLIST_MIGRATION.md (1 hunks)
  • jest-setup.ts (1 hunks)
  • package.json (2 hunks)
  • src/api/calls/calls.ts (1 hunks)
  • src/api/messaging/messages.ts (1 hunks)
  • src/app/(app)/contacts.tsx (2 hunks)
  • src/app/(app)/home/personnel.tsx (2 hunks)
  • src/app/(app)/home/units.tsx (2 hunks)
  • src/app/(app)/notes.tsx (2 hunks)
  • src/app/(app)/protocols.tsx (2 hunks)
  • src/app/(app)/shifts.tsx (0 hunks)
  • src/app/onboarding.tsx (3 hunks)
  • src/components/calls/call-images-modal.tsx (3 hunks)
  • src/components/calls/dispatch-selection-modal.tsx (7 hunks)
  • src/components/notifications/NotificationInbox.tsx (2 hunks)
  • src/components/ui/actionsheet/index.tsx (2 hunks)
  • src/components/ui/bottomsheet/index.tsx (2 hunks)
  • src/components/ui/flat-list/index.tsx (1 hunks)
  • src/components/ui/select/select-actionsheet.tsx (2 hunks)
  • src/stores/dispatch/store.ts (4 hunks)
  • src/translations/ar.json (2 hunks)
  • src/translations/en.json (2 hunks)
  • src/translations/es.json (2 hunks)
💤 Files with no reviewable changes (1)
  • src/app/(app)/shifts.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components and favor interfaces for props and state
Avoid using any; use precise types
Use React Navigation for navigation and deep linking following best practices
Handle errors gracefully and provide user feedback
Implement proper offline support (caching, queueing, retries)
Use Expo SecureStore for sensitive data storage
Use zustand for state management
Use react-hook-form for form handling
Use react-query for data fetching and caching
Use react-native-mmkv for local storage
Use axios for API requests

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Ensure support for dark mode and light mode
Handle errors gracefully and provide user feedback
Use react-query for data fetching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

Files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/app/(app)/home/units.tsx
  • src/stores/dispatch/store.ts
  • src/components/ui/bottomsheet/index.tsx
  • src/api/messaging/messages.ts
  • src/components/ui/actionsheet/index.tsx
  • src/api/calls/calls.ts
  • src/components/ui/flat-list/index.tsx
  • jest-setup.ts
  • src/app/(app)/notes.tsx
  • src/components/calls/call-images-modal.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
  • src/app/(app)/protocols.tsx
  • src/app/(app)/home/personnel.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/app/(app)/contacts.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursorrules)

**/*.tsx: Use functional components and React hooks instead of class components
Use PascalCase for React component names
Use React.FC for defining functional components with props
Minimize useEffect/useState usage and avoid heavy computations during render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Provide getItemLayout to FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers; define callbacks with useCallback or outside render
Use gluestack-ui for styling where available from components/ui; otherwise, style via StyleSheet.create or styled-components
Ensure responsive design across screen sizes and orientations
Use react-native-fast-image for image handling instead of the default Image where appropriate
Wrap all user-facing text in t() from react-i18next for translations
Support dark mode and light mode in UI components
Use @rnmapbox/maps for maps or navigation features
Use lucide-react-native for icons directly; do not use the gluestack-ui icon component
Use conditional rendering with the ternary operator (?:) instead of &&

**/*.tsx: Use functional components and hooks over class components
Ensure components are modular, reusable, and maintainable
Ensure all components are mobile-friendly, responsive, and support both iOS and Android
Use PascalCase for component names
Utilize React.FC for defining functional components with props
Minimize useEffect, useState, and heavy computations inside render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers to prevent re-renders
Ensure responsive design for different screen sizes and orientations
Optimize image handling using rea...

Files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/app/(app)/home/units.tsx
  • src/components/ui/bottomsheet/index.tsx
  • src/components/ui/actionsheet/index.tsx
  • src/components/ui/flat-list/index.tsx
  • src/app/(app)/notes.tsx
  • src/components/calls/call-images-modal.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
  • src/app/(app)/protocols.tsx
  • src/app/(app)/home/personnel.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/app/(app)/contacts.tsx
src/**

📄 CodeRabbit inference engine (.cursorrules)

src/**: Organize files by feature, grouping related components, hooks, and styles
Directory and file names should be lowercase and hyphenated (e.g., user-profile)

Files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/app/(app)/home/units.tsx
  • src/translations/en.json
  • src/translations/es.json
  • src/stores/dispatch/store.ts
  • src/components/ui/bottomsheet/index.tsx
  • src/translations/ar.json
  • src/api/messaging/messages.ts
  • src/components/ui/actionsheet/index.tsx
  • src/api/calls/calls.ts
  • src/components/ui/flat-list/index.tsx
  • src/app/(app)/notes.tsx
  • src/components/calls/call-images-modal.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
  • src/app/(app)/protocols.tsx
  • src/app/(app)/home/personnel.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/app/(app)/contacts.tsx
src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use gluestack-ui components from components/ui; if unavailable, style via StyleSheet.create or styled-components

Files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/components/ui/bottomsheet/index.tsx
  • src/components/ui/actionsheet/index.tsx
  • src/components/ui/flat-list/index.tsx
src/translations/**/*.json

📄 CodeRabbit inference engine (.cursorrules)

Store translation dictionary files under src/translations as JSON resources

Files:

  • src/translations/en.json
  • src/translations/es.json
  • src/translations/ar.json
src/translations/**/*.{ts,tsx,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Store translation dictionary files in src/translations

Files:

  • src/translations/en.json
  • src/translations/es.json
  • src/translations/ar.json
🧠 Learnings (10)
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize

Applied to files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/app/(app)/home/units.tsx
  • src/components/ui/actionsheet/index.tsx
  • src/components/ui/flat-list/index.tsx
  • src/app/(app)/notes.tsx
  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
  • src/app/(app)/protocols.tsx
  • src/app/(app)/home/personnel.tsx
  • FLATLIST_TO_FLASHLIST_MIGRATION.md
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to src/components/ui/**/*.tsx : Use gluestack-ui components from components/ui; if unavailable, style via StyleSheet.create or styled-components

Applied to files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/components/ui/actionsheet/index.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Use gluestack-ui for styling where available from components/ui; otherwise, style via StyleSheet.create or styled-components

Applied to files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/components/ui/actionsheet/index.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Provide getItemLayout to FlatList when items have consistent size

Applied to files:

  • src/components/ui/select/select-actionsheet.tsx
  • src/components/ui/actionsheet/index.tsx
  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Use lucide-react-native for icons directly in markup; do not use gluestack-ui icon component

Applied to files:

  • src/components/ui/actionsheet/index.tsx
  • src/components/calls/dispatch-selection-modal.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Use lucide-react-native for icons directly; do not use the gluestack-ui icon component

Applied to files:

  • src/components/ui/actionsheet/index.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/app/(app)/contacts.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Use react-native-fast-image for image handling instead of the default Image where appropriate

Applied to files:

  • src/components/calls/call-images-modal.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Optimize image handling using react-native-fast-image

Applied to files:

  • src/components/calls/call-images-modal.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Use getItemLayout for FlatList when items have consistent size

Applied to files:

  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.{ts,tsx} : Use React Navigation for navigation and deep linking following best practices

Applied to files:

  • src/app/onboarding.tsx
🧬 Code graph analysis (12)
src/components/ui/select/select-actionsheet.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/app/(app)/home/units.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/ui/actionsheet/index.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
jest-setup.ts (2)
__mocks__/react-native-webview.js (2)
  • React (1-1)
  • require (2-2)
__mocks__/@gorhom/bottom-sheet.js (2)
  • React (2-2)
  • require (3-3)
src/app/(app)/notes.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/calls/call-images-modal.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/notifications/NotificationInbox.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/app/onboarding.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/app/(app)/protocols.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/app/(app)/home/personnel.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/calls/dispatch-selection-modal.tsx (4)
src/stores/dispatch/store.ts (1)
  • useDispatchStore (56-260)
__mocks__/@/components/ui/button.tsx (2)
  • Button (4-6)
  • ButtonText (8-10)
__mocks__/@/components/ui/box.tsx (1)
  • Box (4-6)
__mocks__/@/components/ui/hstack.tsx (1)
  • HStack (4-6)
src/app/(app)/contacts.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (18)
src/translations/es.json (1)

355-356: ES translations read well; approve.

Wording is natural and consistent. Use this updated verification script to check parity and usage:

#!/bin/bash
set -euo pipefail

# Parity check (same as EN review)
for f in src/translations/en.json src/translations/es.json src/translations/ar.json; do
  echo "Checking $f"
  jq -e '.common | has("no_data_available") and has("no_results_found") and has("search")' "$f" >/dev/null || {
    echo "Missing one or more keys in $f"; exit 1; }
done

# Usage check across .ts, .tsx, and .js files
rg -n -C1 -g '*.ts' -g '*.tsx' -g '*.js' '\bcommon\.(no_data_available|no_results_found|search)\b' || true
src/translations/ar.json (1)

355-356: Approve AR translations

Terminology fits generic UI usage. Use this updated verification script to check key parity and usage:

#!/usr/bin/env bash
set -euo pipefail

for f in src/translations/en.json src/translations/es.json src/translations/ar.json; do
  echo "Checking $f"
  jq -e '.common | has("no_data_available") and has("no_results_found") and has("search")' "$f"
done

rg -n -C1 -g '*.ts' -g '*.tsx' -g '*.js' '\bcommon\.(no_data_available|no_results_found|search)\b' || true
.github/workflows/react-native-cicd.yml (1)

73-74: Verification needed: confirm RESPOND_COUNTLY_ usage*
Ripgrep didn’t locate any references in the codebase. Please manually verify which jobs or steps actually consume RESPOND_COUNTLY_APP_KEY and RESPOND_COUNTLY_URL before scoping them.

src/components/ui/bottomsheet/index.tsx (2)

45-47: No‑op context handlers: LGTM

Formatting-only; safe defaults preserved.


169-176: Escape‑to‑close on web: LGTM

Handler is memoized and clean; no behavior risk.

package.json (1)

80-91: FlashList v2 upgrade validated
Search confirmed no deprecated FlashList props (getItemLayout, windowSize, etc.) or New Architecture opt-outs; bump to v2.1.0 is safe.

src/components/ui/select/select-actionsheet.tsx (1)

10-11: Swap to FlashList: v2 audit passed

  • No direct react-native FlatList imports remain (only wrapper imports).
  • No unsupported v2 props found (inverted, getItemLayout, windowSize, maxToRenderPerBatch, onScrollToIndexFailed, initialNumToRender).
  • Expo SDK 53 / RN 0.79 new-architecture prerequisites are satisfied.
src/app/(app)/contacts.tsx (1)

2-2: Confirm RN New Architecture enabled for FlashList v2.1.0
FlashList v2.1.0 is installed and requires React Native’s New Architecture; I didn’t find any newArchEnabled=true, RN_NEW_ARCH_ENABLED=1, or RCT_NEW_ARCH_ENABLED=1 settings in your Android or iOS configs. Please verify and enable New Architecture in android/gradle.properties and your iOS Podfile/Xcode project.

src/components/ui/actionsheet/index.tsx (2)

8-8: Switch to FlashList acknowledged. Verify RN New Architecture is enabled.

FlashList v2 targets RN New Architecture. Please confirm Android/iOS new-arch flags are on in this repo and CI. Based on learnings.


11-11: Import changes look fine.
No concerns with the added RN imports in this context.

src/app/(app)/protocols.tsx (2)

2-2: Good migration to FlashList. Confirm New Architecture is enabled in builds.


6-6: Import change is fine.

src/app/onboarding.tsx (1)

2-2: Type updates look good.

Using FlashListRef and updating the ref type aligns with the new component.

Also applies to: 70-70

src/app/(app)/notes.tsx (2)

2-2: FlashList import looks good.


6-6: Import change is fine.

src/components/calls/dispatch-selection-modal.tsx (1)

249-261: Confirm expected Cancel behavior (clears selection)

Cancel clears selection before closing. If users expect to keep their current selection when dismissing, this will surprise them.

src/components/calls/call-images-modal.tsx (1)

1-1: Remove suggested change; FlashListRef is exported in v2.1.0. FlashListRef is available in "dist/index.d.ts", so you can keep

import { FlashList, type FlashListRef } from '@shopify/flash-list';
const flatListRef = useRef<FlashListRef<CallFileResultData>>(null);

Likely an incorrect or invalid review comment.

FLATLIST_TO_FLASHLIST_MIGRATION.md (1)

116-121: Documentation version matches actual package version
The version "~2.1.0" is declared in package.json, locked in lockfiles, and published on npm as 2.1.0. No changes required.

Comment on lines 107 to 113
## Compatibility Notes

- FlashList doesn't support some FlatList optimization props (`getItemLayout`, `initialNumToRender`, etc.)
- These props were removed as FlashList handles optimization internally
- All existing functionality is preserved
- Performance should be improved, especially for large lists

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Call out RN New Architecture prerequisite (Fabric) and guardrails.

FlashList v2 requires RN New Architecture. Please add an explicit note and verification steps; otherwise runtime will break on old-arch builds.

Add to “Compatibility Notes”:

 - All existing functionality is preserved
 - Performance should be improved, especially for large lists
+ - Prerequisite: React Native New Architecture (Fabric + TurboModules) must be enabled for iOS and Android.
+   - Android: `gradle.properties` → `newArchEnabled=true`
+   - iOS: `Podfile` `use_react_native!` opts include `:fabric_enabled => true` (or `RCT_NEW_ARCH_ENABLED=1`)
+ - If you ship old-arch builds, keep FlatList or gate FlashList by build flag.

To verify in-repo:


🏁 Script executed:

#!/bin/bash
rg -nP 'newArchEnabled\s*=\s*true' -g '!**/node_modules/**'
rg -nP 'use_react_native!\s*\(.*:fabric_enabled\s*=>\s*true' ios -g '!**/node_modules/**' -S -C2
rg -nP 'RCT_NEW_ARCH_ENABLED\s*=\s*1|RCT_NEW_ARCH_ENABLED=YES' -g '!**/node_modules/**'

Length of output: 341


🏁 Script executed:

#!/bin/bash
# List all gradle.properties files
echo "Gradle properties files:"
fd --type f gradle.properties

# Search for newArchEnabled in gradle.properties
echo -e "\nSearching for newArchEnabled in gradle.properties:"
rg -n 'newArchEnabled' $(fd --type f gradle.properties)

# List all Podfile files
echo -e "\nPodfile files:"
fd --type f Podfile

# Search for fabric_enabled in Podfile files
echo -e "\nSearching for fabric_enabled in Podfile:"
rg -n 'fabric_enabled' $(fd --type f Podfile)

# Search for RCT_NEW_ARCH_ENABLED in entire repository
echo -e "\nSearching for RCT_NEW_ARCH_ENABLED in repo:"
rg -n 'RCT_NEW_ARCH_ENABLED' .

Length of output: 220


Add React Native New Architecture prerequisite to Compatibility Notes
FlashList v2 relies on Fabric + TurboModules. Ensure this is clearly documented:

  • For Expo-managed apps: verify newArchEnabled: true in app.config.ts.
  • For bare React Native apps:
    • Android: set newArchEnabled=true in android/gradle.properties.
    • iOS: enable Fabric in your Podfile (:fabric_enabled => true or RCT_NEW_ARCH_ENABLED=1).
  • If you need to ship non–New Architecture (old-arch) builds, continue using FlatList or gate FlashList behind a feature flag.
🤖 Prompt for AI Agents
In FLATLIST_TO_FLASHLIST_MIGRATION.md around lines 107 to 113, add a
compatibility prerequisite noting FlashList v2 requires React Native's New
Architecture (Fabric + TurboModules). Update the Compatibility Notes to instruct
Expo-managed apps to set newArchEnabled: true in app.config.ts, for bare Android
to set newArchEnabled=true in android/gradle.properties, and for bare iOS to
enable Fabric in the Podfile (e.g., :fabric_enabled => true or
RCT_NEW_ARCH_ENABLED=1); also mention that apps needing non-New-Architecture
builds should keep using FlatList or gate FlashList behind a feature flag.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/onboarding.tsx (1)

26-42: Localize user-facing strings (titles, descriptions, buttons)

Wrap onboarding copy and button labels with t(). Consider memoizing onboardingData from t to update on locale change.

+import { useTranslation } from 'react-i18next';
...
 export default function Onboarding() {
+  const { t } = useTranslation();
...
-const onboardingData: OnboardingItemProps[] = [
+const onboardingData: OnboardingItemProps[] = [
   { 
-    title: 'Resgrid Responder',
-    description: 'Manage your status, staffing, and interact with your organization in real-time',
+    title: t('onboarding.title_app'),
+    description: t('onboarding.desc_app'),
     icon: <MapPin size={80} color="#FF7B1A" />,
   },
   {
-    title: 'Instant Notifications',
-    description: 'Receive immediate alerts for emergencies and important updates from your department',
+    title: t('onboarding.title_notifications'),
+    description: t('onboarding.desc_notifications'),
     icon: <Bell size={80} color="#FF7B1A" />,
   },
   {
-    title: 'Interact with Calls',
-    description: 'Seamlessly view call information and interact with your team members for efficient emergency response',
+    title: t('onboarding.title_calls'),
+    description: t('onboarding.desc_calls'),
     icon: <Users size={80} color="#FF7B1A" />,
   },
 ];
...
-<Text className="text-gray-500">Skip</Text>
+<Text className="text-gray-500">{t('common.skip')}</Text>
...
-<ButtonText>Next </ButtonText>
+<ButtonText>{t('common.next')}</ButtonText>
...
-<ButtonText>Let's Get Started</ButtonText>
+<ButtonText>{t('onboarding.get_started')}</ButtonText>

If keys don’t exist yet, add them to your locales. As per coding guidelines.

Also applies to: 175-181, 203-204

src/components/calls/dispatch-selection-modal.tsx (1)

173-187: Trim analytics PII: don’t send raw search queries

Sending the full searchQuery may be PII. You already send length; drop the raw string.

-        trackEvent('dispatch_selection_search', {
+        trackEvent('dispatch_selection_search', {
           timestamp: new Date().toISOString(),
-          searchQuery: query,
           searchLength: query.length,
         });
🧹 Nitpick comments (20)
src/app/(app)/home/units.tsx (2)

95-102: FlashList: add estimatedItemSize and memoize renderItem; remove any cast

  • Provide estimatedItemSize to help layout/recycling.
  • Avoid inline renderItem; memoize with useCallback.
  • Replace any with a concrete Unit type.

Apply within this block:

 <FlashList
   data={filteredUnits}
   keyExtractor={(item, index) => item.UnitId || `unit-${index}`}
-  renderItem={({ item }) => <UnitCard unit={item as any} onPress={selectUnit} />}
+  renderItem={renderUnit}
   showsVerticalScrollIndicator={false}
   contentContainerStyle={{ paddingBottom: 100 }}
   refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
+  estimatedItemSize={120}
 />

Add outside the block (near other hooks):

// Prefer importing a real Unit type from your models
interface Unit {
  UnitId?: string;
  Name?: string;
  Type?: string;
  PlateNumber?: string;
  Vin?: string;
  GroupName?: string;
}

const renderUnit = React.useCallback(
  ({ item }: { item: Unit }) => <UnitCard unit={item} onPress={selectUnit} />,
  [selectUnit]
);

Based on learnings.


2-2: Prefer centralized UI alias for list component

If src/components/ui/flat-list re-exports FlashList, import from there for consistency and easier future swaps/config defaults.

-import { FlashList } from '@shopify/flash-list';
+import { FlatList as FlashList } from '@/components/ui/flat-list';
src/app/onboarding.tsx (1)

143-155: FlashList: set estimatedItemSize and memoize renderItem

For a horizontal pager where each item is screen-width, hint the size and avoid inline lambdas.

 <FlashList
   ref={flatListRef}
   data={onboardingData}
-  renderItem={({ item }: { item: OnboardingItemProps }) => <OnboardingItem {...item} />}
+  renderItem={renderOnboardingItem}
   horizontal
   showsHorizontalScrollIndicator={false}
   pagingEnabled
   bounces={false}
   keyExtractor={(item: OnboardingItemProps) => item.title}
   onScroll={handleScroll}
   scrollEventThrottle={16}
   testID="onboarding-flatlist"
+  estimatedItemSize={width}
 />

Add outside the block:

const renderOnboardingItem = React.useCallback(
  ({ item }: { item: OnboardingItemProps }) => <OnboardingItem {...item} />,
  []
);

Based on learnings.

src/components/calls/call-images-modal.tsx (1)

390-413: Horizontal FlashList: add estimatedItemSize and handle scrollToIndex failures

  • For width-paged galleries, set estimatedItemSize={width} to help layout.
  • Add onScrollToIndexFailed to recover when the target cell isn’t measured yet.
 <FlashList
   ref={flatListRef}
   data={validImages}
   renderItem={renderImageItem}
   keyExtractor={(item: CallFileResultData, index: number) => item?.Id || `image-${index}-${item?.Name || 'unknown'}`}
   horizontal
   pagingEnabled
   showsHorizontalScrollIndicator={false}
   onViewableItemsChanged={handleViewableItemsChanged}
   viewabilityConfig={{
     itemVisiblePercentThreshold: 50,
     minimumViewTime: 100,
   }}
+  onScrollToIndexFailed={(info) => {
+    // Fallback: scroll close to the desired offset then retry
+    flatListRef.current?.scrollToOffset({
+      offset: Math.max(0, info.averageItemLength * info.index),
+      animated: false,
+    });
+    requestAnimationFrame(() =>
+      flatListRef.current?.scrollToIndex({ index: info.index, animated: true })
+    );
+  }}
   snapToInterval={width}
   snapToAlignment="start"
   decelerationRate="fast"
   className="w-full"
   contentContainerStyle={{ paddingHorizontal: 0 }}
   ListEmptyComponent={() => (

Based on learnings.

src/components/notifications/__tests__/NotificationInbox.test.tsx (2)

174-193: Avoid reimplementing pluralization in mock; derive from resources or return keys

Current mock hardcodes English strings and plural rules, which can drift from real translations and doesn’t reflect Arabic/Spanish forms.

Minimal improvement example:

+import en from '@/translations/en.json';
+
+const getByPath = (obj: any, path: string) =>
+  path.split('.').reduce((o, k) => (o ? o[k] : undefined), obj);
+
-const mockT = jest.fn((key: string, options?: any) => {
-  const translations: Record<string, string> = { /* ...hardcoded... */ };
-  return translations[key] || key;
-});
+const mockT = jest.fn((key: string, options?: any) => {
+  // Try exact key, then _plural fallback when count !== 1
+  const base = getByPath(en, key);
+  const plural = getByPath(en, `${key}_plural`);
+  const template = options?.count !== undefined && options.count !== 1 && plural ? plural : base;
+  if (typeof template === 'string') {
+    return template.replace(/\{\{(\w+)\}\}/g, (_, p) => options?.[p] ?? '');
+  }
+  return key;
+});

This keeps tests aligned with actual resources. You can extract this into a shared test util.


503-573: Assert rendered UI for i18n where possible, not only t() invocations

Several tests only check that t() was called. Prefer asserting visible text after triggering the relevant UI (e.g., open the delete modal, enter selection mode) to reduce brittleness.

src/components/notifications/NotificationInbox.tsx (3)

303-312: Add estimatedItemSize to FlashList for better perf

FlashList benefits from an estimatedItemSize to stabilize virtualization.

 <FlashList
   data={notifications}
   renderItem={renderItem}
-  keyExtractor={(item: any) => item.id}
+  keyExtractor={(item: NotificationPayload) => item.id}
   onEndReached={fetchMore}
   onEndReachedThreshold={0.5}
   ListFooterComponent={renderFooter}
   ListEmptyComponent={renderEmpty}
   refreshControl={<RefreshControl refreshing={isLoading} onRefresh={refetch} colors={['#2196F3']} />}
+  estimatedItemSize={72}
 />

Consider also getItemType if items are heterogeneous.


165-165: Tighten types for renderItem

Avoid any; this also improves keyExtractor typing.

-const renderItem = ({ item }: { item: any }) => {
+const renderItem = ({ item }: { item: NotificationPayload }) => {

Additionally (outside this hunk), change:

-const allIds = notifications?.map((item: any) => item.id) || [];
+const allIds = notifications?.map((item: NotificationPayload) => item.id) || [];

181-191: Optional a11y: add accessibilityRole and labels to interactives

Improve screen reader support on list items and icon-only buttons.

Example:

 <Pressable
+  accessibilityRole="button"
+  accessibilityLabel={notification.title ?? t('notifications.title')}
   onPress={() => handleNotificationPress(notification)}
   onLongPress={() => { /* ... */ }}
   style={[/* ... */]}
>

Also consider accessibilityLabel for the More/Close buttons.

docs/notification-inbox-localization-implementation.md (1)

54-60: Docs: replace ICU plural examples with i18next plural keys (or document ICU plugin)

Current examples use ICU; align with i18next defaults for consistency.

-  bulkDeleteSuccess: "{{count}} notification{{count, plural, one {} other {s}}} removed",
+  bulkDeleteSuccess: "{{count}} notification removed",
+  bulkDeleteSuccess_plural: "{{count}} notifications removed",
   confirmDelete: {
     title: "Confirm Delete",
-    message: "Are you sure you want to delete {{count}} notification{{count, plural, one {} other {s}}}? This action cannot be undone."
+    message: "Are you sure you want to delete {{count}} notification? This action cannot be undone.",
+    message_plural: "Are you sure you want to delete {{count}} notifications? This action cannot be undone."
   }

If you intend to use ICU, add a note and link to enabling i18next-icu in app i18n setup.

Also applies to: 116-118

src/stores/dispatch/store.ts (3)

95-103: Centralize type normalization to reduce branching and drift

The flexible matching works; consider a small helper to normalize once and switch on a union. This reduces copy/paste and future drift.

-      // Categorize recipients based on Type field with both exact and flexible matching
-      recipients.Data.forEach((recipient) => {
+      // Categorize recipients based on normalized Type
+      const normalizeType = (t?: string) => {
+        const type = (t || '').toLowerCase().trim();
+        if (type === 'personnel' || type === 'user' || type === 'users') return 'users';
+        if (type === 'groups' || type === 'group') return 'groups';
+        if (type === 'roles'  || type === 'role')  return 'roles';
+        if (type === 'unit'   || type === 'units') return 'units';
+        return '';
+      };
+      recipients.Data.forEach((recipient) => {
         if (!recipient || !recipient.Type || !recipient.Name || !recipient.Id) {
           if (__DEV__) {
             console.warn('Skipping invalid recipient - missing required fields');
           }
           return;
         }
-
-        // First try exact matching (as per the test data)
-        if (recipient.Type === 'Personnel') {
-          categorizedUsers.push(recipient);
-        } else if (recipient.Type === 'Groups') {
-          categorizedGroups.push(recipient);
-        } else if (recipient.Type === 'Roles') {
-          categorizedRoles.push(recipient);
-        } else if (recipient.Type === 'Unit') {
-          categorizedUnits.push(recipient);
-        } else {
-          // Fallback to case-insensitive matching
-          const type = recipient.Type.toLowerCase().trim();
-          if (type === 'personnel' || type === 'user' || type === 'users') {
-            categorizedUsers.push(recipient);
-          } else if (type === 'groups' || type === 'group') {
-            categorizedGroups.push(recipient);
-          } else if (type === 'roles' || type === 'role') {
-            categorizedRoles.push(recipient);
-          } else if (type === 'unit' || type === 'units') {
-            categorizedUnits.push(recipient);
-          } else {
-            // Log unknown types for debugging
-            if (__DEV__) {
-              console.warn(`Unknown recipient type: '${recipient.Type}'`);
-            }
-          }
-        }
+        const t = recipient.Type;
+        switch (t) {
+          case 'Personnel': categorizedUsers.push(recipient); break;
+          case 'Groups':    categorizedGroups.push(recipient); break;
+          case 'Roles':     categorizedRoles.push(recipient); break;
+          case 'Unit':      categorizedUnits.push(recipient); break;
+          default:
+            switch (normalizeType(t)) {
+              case 'users':  categorizedUsers.push(recipient); break;
+              case 'groups': categorizedGroups.push(recipient); break;
+              case 'roles':  categorizedRoles.push(recipient); break;
+              case 'units':  categorizedUnits.push(recipient); break;
+              default: if (__DEV__) console.warn(`Unknown recipient type: '${t}'`);
+            }
+        }
       });

Also applies to: 113-129


133-145: Wrap the “no recipients categorized” check in DEV to avoid prod compute

totalCategorized and available types are only for logging; skip the work in prod.

-      // Only log if we have issues with categorization
-      const totalCategorized = categorizedUsers.length + categorizedGroups.length + categorizedRoles.length + categorizedUnits.length;
-      if (totalCategorized === 0 && recipients.Data.length > 0) {
-        if (__DEV__) {
-          console.warn('No recipients were successfully categorized!');
-          console.warn('Available recipient types:', [...new Set(recipients.Data.map((r) => r.Type))]);
-        }
-      }
+      // Dev-only: log categorization issues
+      if (__DEV__) {
+        const totalCategorized = categorizedUsers.length + categorizedGroups.length + categorizedRoles.length + categorizedUnits.length;
+        if (totalCategorized === 0 && recipients.Data.length > 0) {
+          console.warn('No recipients were successfully categorized!');
+          console.warn('Available recipient types:', [...new Set(recipients.Data.map((r) => r.Type))]);
+        }
+      }

156-161: Prefer generic user-facing errors; keep details in DEV logs

Exposing raw error.message to UI may leak internal details. Keep the console message in DEV but show a generic string to users.

-      if (__DEV__) {
-        console.error('Error fetching dispatch data:', error instanceof Error ? error.message : 'Unknown error');
-      }
+      if (__DEV__) console.error('Error fetching dispatch data:', error);
       set({
-        error: error instanceof Error ? error.message : 'Failed to fetch dispatch data',
+        error: 'Failed to fetch dispatch data',
         isLoading: false,
       });
src/components/calls/dispatch-selection-modal.tsx (7)

3-3: Remove unused import

useState isn’t used.

-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';

71-73: Gate fetch on open to avoid redundant calls

Only fetch when lists are empty (or add a TTL later).

-      // Always fetch data when modal opens
-      fetchDispatchData();
+      // Fetch on first open or when empty
+      if (
+        data.users.length === 0 &&
+        data.groups.length === 0 &&
+        data.roles.length === 0 &&
+        data.units.length === 0
+      ) {
+        fetchDispatchData();
+      }

247-249: Add accessibility labels to the close button

Improve screen-reader support.
[As per coding guidelines]

-        <TouchableOpacity onPress={handleCancel}>
+        <TouchableOpacity
+          onPress={handleCancel}
+          accessibilityRole="button"
+          accessibilityLabel={t('common.close')}
+        >

288-311: Use FlashList for virtualization; replace && rendering with ternaries

Long recipient lists will jank in a ScrollView. Prefer a single FlashList with interleaved section headers (or one per section if you keep them separate). Also, per guidelines, use ?: instead of &&.
[As per coding guidelines]

Minimal example for Users section (illustrative):

-          {/* Users Section */}
-          {filteredData.users.length > 0 && (
+          {/* Users Section */}
+          {filteredData.users.length > 0 ? (
             <VStack className="mb-6">
               <Text className="mb-3 text-lg font-semibold">
                 {t('calls.users')} ({filteredData.users.length})
               </Text>
-              {filteredData.users.map((user) => (
-                <Card key={`user-${user.Id}`} className={`mb-2 rounded-lg border p-3 ${colorScheme === 'dark' ? 'border-neutral-800 bg-neutral-900' : 'border-neutral-200 bg-white'}`}>
-                  <TouchableOpacity onPress={() => handleToggleUser(user.Id)}>
-                    ...
-                  </TouchableOpacity>
-                </Card>
-              ))}
+              {/* Consider FlashList here */}
+              {/* <FlashList
+                    data={filteredData.users}
+                    keyExtractor={(u) => `user-${u.Id}`}
+                    estimatedItemSize={56}
+                    renderItem={({ item: user }) => (
+                      <Card className={`mb-2 rounded-lg border p-3 ${colorScheme === 'dark' ? 'border-neutral-800 bg-neutral-900' : 'border-neutral-200 bg-white'}`}>
+                        <TouchableOpacity onPress={() => handleToggleUser(user.Id)}>
+                          ...row...
+                        </TouchableOpacity>
+                      </Card>
+                    )}
+                /> */}
             </VStack>
-          )}
+          ) : null}

If you consolidate to one FlashList, build a flat data model of headers and items with getItemType and render by type; set estimatedItemSize and, if applicable, getItemType for better recycling. Based on learnings.

Also applies to: 314-337, 340-363, 366-389


391-397: Replace && with ternaries for conditional blocks

Matches the project’s TSX guidelines.
[As per coding guidelines]

-          {!isLoading && !error && searchQuery && filteredData.users.length === 0 && filteredData.groups.length === 0 && filteredData.roles.length === 0 && filteredData.units.length === 0 && (
+          {!isLoading && !error && searchQuery && filteredData.users.length === 0 && filteredData.groups.length === 0 && filteredData.roles.length === 0 && filteredData.units.length === 0 ? (
             <Box className="items-center justify-center py-8">
               <Text className="text-center text-neutral-500">{t('common.no_results_found')}</Text>
             </Box>
-          )}
+          ) : null}
-          {!isLoading && !error && !searchQuery && data.users.length === 0 && data.groups.length === 0 && data.roles.length === 0 && data.units.length === 0 && (
+          {!isLoading && !error && !searchQuery && data.users.length === 0 && data.groups.length === 0 && data.roles.length === 0 && data.units.length === 0 ? (
             <Box className="items-center justify-center py-8">
               <Text className="text-center text-neutral-500">{t('common.no_data_available', { defaultValue: 'No recipients available' })}</Text>
               <Button variant="outline" className="mt-4" onPress={() => refreshDispatchData()}>
                 <ButtonText>{t('common.refresh', { defaultValue: 'Refresh' })}</ButtonText>
               </Button>
             </Box>
-          )}
+          ) : null}

Also applies to: 398-406


97-114: Inline handlers per row can churn; extract memoized row components

For large lists, per-item closures cause extra allocations. If keeping ScrollView maps, consider a small memoized Row component that receives onToggle and selected to minimize re-renders.

Also applies to: 116-133, 135-152, 154-171


34-47: Reuse dispatch store’s getFilteredData selector

This component duplicates the exact filtering logic already implemented in the dispatch store. Replace the local useMemo block with the store selector to keep a single source of truth:

-  // Calculate filtered data directly in component to ensure reactivity
-  const filteredData = useMemo(() => {
-    if (!searchQuery.trim()) {
-      return data;
-    }
-
-    const query = searchQuery.toLowerCase();
-    return {
-      users: data.users.filter((user) => user.Name.toLowerCase().includes(query)),
-      groups: data.groups.filter((group) => group.Name.toLowerCase().includes(query)),
-      roles: data.roles.filter((role) => role.Name.toLowerCase().includes(query)),
-      units: data.units.filter((unit) => unit.Name.toLowerCase().includes(query)),
-    };
-  }, [data, searchQuery]);
+  // Reuse the store selector to avoid logic drift
+  const filteredData = useDispatchStore(state => state.getFilteredData());

Ensure you import the hook from your dispatch store (e.g. import { useDispatchStore } from 'src/stores/dispatch/store').

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac84768 and fd24a4f.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • docs/notification-inbox-localization-implementation.md (1 hunks)
  • docs/notification-inbox-translation-keys-verification.md (1 hunks)
  • package.json (6 hunks)
  • src/app/(app)/home/personnel.tsx (3 hunks)
  • src/app/(app)/home/units.tsx (3 hunks)
  • src/app/onboarding.tsx (3 hunks)
  • src/components/calls/call-images-modal.tsx (3 hunks)
  • src/components/calls/dispatch-selection-modal.tsx (9 hunks)
  • src/components/notifications/NotificationInbox.tsx (7 hunks)
  • src/components/notifications/__tests__/NotificationInbox.test.tsx (6 hunks)
  • src/stores/dispatch/store.ts (4 hunks)
  • src/translations/ar.json (3 hunks)
  • src/translations/en.json (3 hunks)
  • src/translations/es.json (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/notification-inbox-translation-keys-verification.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/(app)/home/personnel.tsx
  • src/translations/es.json
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components and favor interfaces for props and state
Avoid using any; use precise types
Use React Navigation for navigation and deep linking following best practices
Handle errors gracefully and provide user feedback
Implement proper offline support (caching, queueing, retries)
Use Expo SecureStore for sensitive data storage
Use zustand for state management
Use react-hook-form for form handling
Use react-query for data fetching and caching
Use react-native-mmkv for local storage
Use axios for API requests

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Ensure support for dark mode and light mode
Handle errors gracefully and provide user feedback
Use react-query for data fetching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

Files:

  • src/app/(app)/home/units.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/components/calls/call-images-modal.tsx
  • src/stores/dispatch/store.ts
  • src/components/notifications/__tests__/NotificationInbox.test.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursorrules)

**/*.tsx: Use functional components and React hooks instead of class components
Use PascalCase for React component names
Use React.FC for defining functional components with props
Minimize useEffect/useState usage and avoid heavy computations during render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Provide getItemLayout to FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers; define callbacks with useCallback or outside render
Use gluestack-ui for styling where available from components/ui; otherwise, style via StyleSheet.create or styled-components
Ensure responsive design across screen sizes and orientations
Use react-native-fast-image for image handling instead of the default Image where appropriate
Wrap all user-facing text in t() from react-i18next for translations
Support dark mode and light mode in UI components
Use @rnmapbox/maps for maps or navigation features
Use lucide-react-native for icons directly; do not use the gluestack-ui icon component
Use conditional rendering with the ternary operator (?:) instead of &&

**/*.tsx: Use functional components and hooks over class components
Ensure components are modular, reusable, and maintainable
Ensure all components are mobile-friendly, responsive, and support both iOS and Android
Use PascalCase for component names
Utilize React.FC for defining functional components with props
Minimize useEffect, useState, and heavy computations inside render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers to prevent re-renders
Ensure responsive design for different screen sizes and orientations
Optimize image handling using rea...

Files:

  • src/app/(app)/home/units.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/components/calls/call-images-modal.tsx
  • src/components/notifications/__tests__/NotificationInbox.test.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
src/**

📄 CodeRabbit inference engine (.cursorrules)

src/**: Organize files by feature, grouping related components, hooks, and styles
Directory and file names should be lowercase and hyphenated (e.g., user-profile)

Files:

  • src/app/(app)/home/units.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/components/calls/call-images-modal.tsx
  • src/stores/dispatch/store.ts
  • src/translations/ar.json
  • src/components/notifications/__tests__/NotificationInbox.test.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/app/onboarding.tsx
  • src/translations/en.json
src/translations/**/*.json

📄 CodeRabbit inference engine (.cursorrules)

Store translation dictionary files under src/translations as JSON resources

Files:

  • src/translations/ar.json
  • src/translations/en.json
src/translations/**/*.{ts,tsx,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Store translation dictionary files in src/translations

Files:

  • src/translations/ar.json
  • src/translations/en.json
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Create Jest tests for all generated components, services, and logic
Ensure tests run without errors and fix failing tests

Files:

  • src/components/notifications/__tests__/NotificationInbox.test.tsx
src/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.test.{ts,tsx}: Create and use Jest tests to validate all generated components
Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/components/notifications/__tests__/NotificationInbox.test.tsx
🧠 Learnings (9)
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize

Applied to files:

  • src/app/(app)/home/units.tsx
  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Use lucide-react-native for icons directly; do not use the gluestack-ui icon component

Applied to files:

  • src/components/calls/dispatch-selection-modal.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Use lucide-react-native for icons directly in markup; do not use gluestack-ui icon component

Applied to files:

  • src/components/calls/dispatch-selection-modal.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Optimize image handling using react-native-fast-image

Applied to files:

  • src/components/calls/call-images-modal.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Provide getItemLayout to FlatList when items have consistent size

Applied to files:

  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Use getItemLayout for FlatList when items have consistent size

Applied to files:

  • src/components/calls/call-images-modal.tsx
  • src/app/onboarding.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Wrap all user-facing text in t() from react-i18next for translations

Applied to files:

  • src/components/notifications/__tests__/NotificationInbox.test.tsx
  • docs/notification-inbox-localization-implementation.md
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.{ts,tsx} : Use react-i18next for internationalization

Applied to files:

  • src/components/notifications/__tests__/NotificationInbox.test.tsx
  • docs/notification-inbox-localization-implementation.md
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.{ts,tsx} : Use React Navigation for navigation and deep linking following best practices

Applied to files:

  • src/app/onboarding.tsx
🧬 Code graph analysis (6)
src/app/(app)/home/units.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/calls/dispatch-selection-modal.tsx (4)
src/stores/dispatch/store.ts (1)
  • useDispatchStore (56-274)
__mocks__/@/components/ui/button.tsx (2)
  • Button (4-6)
  • ButtonText (8-10)
__mocks__/@/components/ui/box.tsx (1)
  • Box (4-6)
__mocks__/@/components/ui/hstack.tsx (1)
  • HStack (4-6)
src/components/calls/call-images-modal.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/components/notifications/__tests__/NotificationInbox.test.tsx (1)
src/components/notifications/NotificationInbox.tsx (1)
  • NotificationInbox (29-341)
src/components/notifications/NotificationInbox.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
src/app/onboarding.tsx (1)
src/components/ui/flat-list/index.tsx (1)
  • FlashList (2-2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (10)
src/app/(app)/home/units.tsx (1)

55-56: Nice null-safety on search fields

Using optional chaining on Name and Type guards undefined entries cleanly.

src/app/onboarding.tsx (1)

70-71: Ref typing looks good

useRef<FlashList<OnboardingItemProps>>(null) aligns with FlashList’s generic API.

src/components/calls/call-images-modal.tsx (1)

64-66: Ref and types are correct

useRef<FlashList<CallFileResultData>>(null) is the right typing for imperative scrollToIndex usage.

src/translations/en.json (1)

355-366: Common keys addition looks good

Keys are consistent and reusable across features.

src/translations/ar.json (2)

355-366: Common keys addition looks good (Arabic)

Consistent with English; helpful for shared UI.


641-656: Verify i18next-icu configuration

  • No reference to i18next-icu was found in your dependencies or i18n initialization, so ICU plural syntax in your JSON won’t be applied.
  • Either install and configure i18next-icu (e.g. import Icu from 'i18next-icu'; i18n.use(Icu)…) or replace ICU plurals with standard i18next plural keys (key/key_plural).
src/components/notifications/NotificationInbox.tsx (1)

136-145: Good i18n integration in toasts, headers, empty/error states, and modal

t is correctly used and included in callback deps. Nice coverage.

If ICU isn’t configured (see translation comments), ensure pluralization strings are updated to i18next plural keys.

Also applies to: 150-157, 232-236, 265-275, 281-281, 300-300, 324-335

src/stores/dispatch/store.ts (2)

36-36: New refresh API looks good

Adding refreshDispatchData and delegating to fetchDispatchData is clean and idempotent. No concerns.

Also applies to: 166-170


73-88: Good: strict data-shape guard + DEV‑only logs (resolves prior PII concerns)

Validates recipients.Data and gates logs under DEV, avoiding payload/name leakage in prod. Nice.

src/components/calls/dispatch-selection-modal.tsx (1)

260-269: Retry UX looks good

Error message + retry button via refreshDispatchData is clear.

Comment on lines +99 to 100
"expo": "~53.0.23",
"expo-application": "~6.1.5",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Expo 53 upgrade breaks builds with LiveKit Expo plugin 1.0.1

Expo SDK 53 ships with React Native 0.79 and turns the New Architecture on by default. (expo.dev) The LiveKit Expo config plugin you still have at 1.0.1 is known to crash iOS builds under this combo with Cannot find type 'RCTBridge' in scope. (github.com) LiveKit merged a fix in May 2025, but they have not cut a new npm release yet—1.0.1 remains the latest published version—so the patched code isn’t available through a version bump. (github.com) In practice this upgrade will block delivery until you either (a) hold the Expo bump, (b) point the plugin at a commit containing the fix, or (c) vendor/patch the plugin locally. Please resolve before landing.

🤖 Prompt for AI Agents
package.json around lines 99-100: upgrading Expo to SDK 53 breaks iOS builds
because the LiveKit Expo plugin v1.0.1 is incompatible with RN 0.79 / New
Architecture; to fix, either (A) pin Expo back to the previous working SDK
version by reverting the "expo" value to the prior release in package.json and
run yarn/npm install, (B) point the livekit expo plugin dependency to the
upstream commit that includes the May 2025 fix (replace the npm version with the
github repo@<commit-hash> in package.json and reinstall), or (C) vendor/patch
the plugin locally by copying the patched plugin source into your repo, update
package.json to reference the local path, run install, and verify the iOS build;
after making the change, reinstall node modules, clear caches (expo start -c /
pod install in ios), and run an iOS build to confirm the crash is resolved.

Comment on lines +641 to +656
"notifications": {
"bulkDeleteError": "Failed to remove notifications",
"bulkDeleteSuccess": "{{count}} notification{{count, plural, one {} other {s}}} removed",
"confirmDelete": {
"message": "Are you sure you want to delete {{count}} notification{{count, plural, one {} other {s}}}? This action cannot be undone.",
"title": "Confirm Delete"
},
"deleteError": "Failed to remove notification",
"deleteSuccess": "Notification removed",
"deselectAll": "Deselect All",
"empty": "No updates available",
"loadError": "Unable to load notifications",
"selectAll": "Select All",
"selectedCount": "{{count}} selected",
"title": "Notifications"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Pluralization format assumes ICU; switch to i18next plural keys or confirm i18next-icu

The strings use ICU syntax ({{count, plural,...}}). If i18next-icu isn’t configured, these won’t pluralize and will render literally. Prefer native i18next pluralization keys.

Apply this diff for safe i18next pluralization:

   "notifications": {
-    "bulkDeleteSuccess": "{{count}} notification{{count, plural, one {} other {s}}} removed",
+    "bulkDeleteSuccess": "{{count}} notification removed",
+    "bulkDeleteSuccess_plural": "{{count}} notifications removed",
     "confirmDelete": {
-      "message": "Are you sure you want to delete {{count}} notification{{count, plural, one {} other {s}}}? This action cannot be undone.",
+      "message": "Are you sure you want to delete {{count}} notification? This action cannot be undone.",
+      "message_plural": "Are you sure you want to delete {{count}} notifications? This action cannot be undone.",
       "title": "Confirm Delete"
     },
     "deleteError": "Failed to remove notification",
     "deleteSuccess": "Notification removed",
     "deselectAll": "Deselect All",
     "empty": "No updates available",
     "loadError": "Unable to load notifications",
     "selectAll": "Select All",
     "selectedCount": "{{count}} selected",
     "title": "Notifications"
   },

Run this to verify ICU usage and plugin presence:


🏁 Script executed:

#!/bin/bash
# Find ICU-style pluralization patterns in translations
rg -nP "plural\s*,\s*one" src/translations

# Check if i18next-icu (or any ICU plugin) is configured
rg -nP "i18next-icu|ICU|@formatjs|i18next\.use\(.+ICU" -g "!**/node_modules/**"

Length of output: 1062


🏁 Script executed:

#!/bin/bash
# Search for i18next-icu plugin imports and usage
rg -nP "i18next-icu" -g "!**/node_modules/**"
rg -nP "import\s+.*i18next-icu" -g "!**/node_modules/**"
rg -nP "require\(['\"]i18next-icu['\"]\)" -g "!**/node_modules/**"
rg -nP "i18next\.use\s*\(" -g "!**/node_modules/**"

Length of output: 595


🏁 Script executed:

#!/bin/bash
# Check dependencies in package.json
grep -R "\"i18next-icu\"" -n .

# Search for plugin registration in code
grep -R "i18next.use" -n .
grep -R "import.*i18next-icu" -n .
grep -R "require.*i18next-icu" -n .

Length of output: 100769


ICU plural syntax won’t work without a plugin—switch keys or install i18next-icu
No i18next-icu (or equivalent) plugin is present—ICU patterns like {{count, plural, one {} other {s}}} in src/translations/en.json will render literally. You must either:

  • Migrate to native i18next plurals (key/key_plural), or
  • Add and initialize i18next-icu (install the package and call i18next.use(require('i18next-icu')) in your i18n setup)
🤖 Prompt for AI Agents
In src/translations/en.json around lines 641-656, the ICU plural patterns (e.g.
"{{count, plural, one {} other {s}}}") will render literally because i18next-icu
is not configured; fix by either (A) replacing ICU patterns with i18next native
plural keys (e.g. split keys into "bulkDeleteSuccess" and
"bulkDeleteSuccess_plural" and use singular/plural forms with the count
variable), or (B) install and initialize the ICU plugin (add the i18next-icu
package to dependencies and call i18next.use(require('i18next-icu')) during your
i18n setup before init) so the ICU syntax is parsed; choose one approach and
update translations or i18n bootstrap accordingly.

@ucswift
Copy link
Member Author

ucswift commented Sep 29, 2025

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift ucswift merged commit ea54cc2 into master Sep 29, 2025
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants