Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Aug 23, 2025

Summary by CodeRabbit

  • New Features

    • Calendar & Contacts: Rich HTML descriptions rendered in secure, theme-aware WebViews; new compact calendar item and simplified selected-date list; contact notes support rich rendering.
    • Compose Message: real-time validation, unsaved-changes confirmation, bottom Send button, improved keyboard handling.
  • Bug Fixes / UX

    • Timezone-safe date handling for Today/selected dates; non-blocking toast notifications after actions; creator name loading indicator.
  • Documentation

    • Added WebView security docs and compose-sheet docs; removed outdated analytics doc.
  • Tests

    • Expanded test coverage, security tests, and WebView-focused mocks.
  • Chores

    • CI: iOS Xcode pre-check and cross-OS tooling fixes; Dependabot PRs disabled.

@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

Adds secure WebView HTML rendering (sanitization, dark-mode) across calendar and notes, introduces local-time date utilities and timezone-safe comparisons, enhances compose-message validation/unsaved-changes UX, adjusts CI/Dependabot and some API payloads/methods, broad test expansions and translation updates.

Changes

Cohort / File(s) Change Summary
CI & Dependabot
.github/dependabot.yml, .github/workflows/react-native-cicd.yml
Disable Dependabot PR openings for npm; add iOS Xcode toolchain verification step and OS-aware jq installation in CI workflow.
WebView HTML & Sanitizer
src/utils/webview-html.ts, src/utils/html-sanitizer.ts, src/utils/__tests__/webview-html.test.ts, src/utils/__tests__/html-sanitizer.test.ts, docs/webview-html-security-implementation.md, docs/webview-security-implementation.md, package.json
New HTML generator and sanitization utilities, secure default WebView props, tests and docs; adds sanitize-html dependency and types.
Calendar: UI, WebView, theming, personnel loading
src/components/calendar/calendar-card.tsx, src/components/calendar/calendar-item-details-sheet.tsx, src/components/calendar/__tests__/*, src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
Replace text descriptions with sanitized WebView HTML (dark/light), add useColorScheme, secure WebView settings, auto-fetch personnel when sheet opens with loading state, replace alerts with toasts, expand/adjust tests and mocks.
Compact Calendar / Views / Date handling
src/components/calendar/compact-calendar-item.tsx, src/components/calendar/enhanced-calendar-view.tsx, src/app/(app)/calendar.tsx, src/components/calendar/__tests__/*
Add CompactCalendarItem component; simplify selected-date rendering; switch to timezone-safe comparisons (isSameDate) and local-date formatting; many tests updated/added.
Date utilities & tests
src/lib/utils.ts, src/lib/__tests__/utils.test.ts, src/lib/__tests__/utils-date.test.ts
Add formatLocalDateString, getTodayLocalString, isSameDate, isToday for local-time date handling; comprehensive unit tests.
Calendar store & API changes
src/stores/calendar/store.ts, src/stores/calendar/__tests__/store.test.ts, src/api/calendar/calendar.ts
Today-items load uses ISO range + client-side isSameDate filtering and richer logging; store exposes explicit load* methods with legacy aliases; attendance payload mapping adjusted (CalendarEventId / numeric Type).
Messaging: API and Compose UX
src/api/messaging/messages.ts, src/components/messages/compose-message-sheet.tsx, src/components/messages/__tests__/compose-message-sheet.test.tsx
deleteMessage switches to HTTP DELETE; compose sheet gains validation, inline errors, unsaved-changes confirmation, bottom Send bar, keyboard handling, recipient mapping, analytics; tests updated.
Contacts notes: rich rendering
src/components/contacts/contact-notes-list.tsx
Notes now render via WebView for rich content or Text for plain notes; theme-aware HTML, external-link handling via Linking, and analytics event on render.
Jest / test environment & mocks
jest-setup.ts, multiple src/components/.../__tests__/*
Add react-native-webview mock to jest setup; expand many test mocks (nativewind, WebView, UI primitives, personnel store) to stabilize new tests.
Translations & docs
src/translations/en.json, src/translations/es.json, src/translations/ar.json, docs/*
Update confirmSignup wording, add common.discard, move message validation keys into messages.validation, add unsaved-changes keys; add docs for WebView refactors and compose improvements; remove calendar analytics README.
Minor / formatting / app init
src/components/sidebar/side-menu.tsx, src/app/_layout.tsx
Template-string formatting only; un-comment SplashScreen.preventAutoHideAsync() to prevent auto-hide during loading.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Sheet as CalendarItemDetailsSheet
  participant Store as PersonnelStore
  participant Web as WebView
  participant API as Calendar API
  participant Toast as useToast

  User->>Sheet: Open (item present)
  Sheet->>Store: check personnel (length / isLoading)
  alt store empty & not loading
    Sheet->>Store: fetchPersonnel()
    Store-->>Sheet: personnel data
  end
  Sheet->>Web: render sanitized HTML (generateWebViewHtml)
  User->>Sheet: Toggle attendance
  Sheet->>API: setCalendarAttending({ CalendarEventId, Type, Note })
  API-->>Sheet: response
  alt success
    Sheet->>API: fetchCalendarItem(item.Id)
    Sheet->>Toast: show success
  else failure
    Sheet->>Toast: show error
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Compose as ComposeMessageSheet
  participant API as Messaging API
  participant Analytics as useAnalytics

  User->>Compose: Tap Send
  Compose->>Compose: validateForm()
  alt invalid
    Compose-->>User: show inline errors
  else valid
    Compose->>API: sendNewMessage(payload)
    API-->>Compose: success/failure
    Compose->>Analytics: track send_success/send_failure
  end

  User->>Compose: Close
  alt hasFormChanges
    Compose-->>User: show discard confirmation
    alt discard confirmed
      Compose->>Analytics: track close {discardedChanges:true}
      Compose-->>User: close
    else cancel
      Compose-->>User: stay open
    end
  else no changes
    Compose-->>User: close
  end
Loading
sequenceDiagram
  autonumber
  participant View as Calendar View
  participant Utils as Date Utils
  participant Store as Calendar Store

  View->>Utils: getTodayLocalString / formatLocalDateString
  View->>Store: loadCalendarItemsForDateRange(startLocal, endLocal)
  Store-->>View: items
  View->>Utils: isSameDate(item.Start, selectedDate)
  Utils-->>View: true/false
  View-->>User: render CompactCalendarItem list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

A rabbit taps the code so neat,
Sanitizes HTML before we meet.
Dates align with local sun,
Compose asks if changes are done.
Tests hop through — green lights, a treat! 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 18

Caution

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

⚠️ Outside diff range comments (6)
src/api/messaging/messages.ts (1)

78-82: Ensure DELETE payload is sent in the request body

The createApiEndpoint.delete helper in src/api/common/client.tsx currently maps its single parameter to Axios’s params key, not data. As a result, calling

deleteMessageApi.delete<DeleteMessageResult>({ MessageId: messageId });

will issue a DELETE with ?MessageId=… rather than a JSON body, which will break any endpoint expecting a request body.

To fix this, update the helper and its call sites:

• In src/api/common/client.tsx (around line 126), change the delete implementation to accept a full Axios config:

- delete: <T,>(params?: Record<string, unknown>) =>
-   api.delete<T>(endpoint, { params }),
+ delete: <T,>(config?: AxiosRequestConfig) =>
+   api.delete<T>(endpoint, config),

• In src/api/messaging/messages.ts (lines 78–82), send the payload as data rather than inline params:

- const response = await deleteMessageApi.delete<DeleteMessageResult>({
-   MessageId: messageId,
- });
+ const response = await deleteMessageApi.delete<DeleteMessageResult>({
+   data: { MessageId: messageId },
+ });

This ensures the MessageId is included in the DELETE request body as intended.

src/components/messages/__tests__/compose-message-sheet.test.tsx (1)

57-65: Replace HTML

in lucide mocks with React Native View

Using DOM elements in RN tests can cause “View config not found for name div” errors. Mock icons with React Native View instead.

-jest.mock('lucide-react-native', () => ({
-  CalendarDays: ({ size, className }: any) => <div data-testid="calendar-days-icon" />,
-  Check: ({ size, className }: any) => <div data-testid="check-icon" />,
-  ChevronDown: ({ size, color }: any) => <div data-testid="chevron-down-icon" />,
-  Plus: ({ size, className }: any) => <div data-testid="plus-icon" />,
-  Send: ({ size, color }: any) => <div data-testid="send-icon" />,
-  Users: ({ size, className }: any) => <div data-testid="users-icon" />,
-  X: ({ size, className }: any) => <div data-testid="x-icon" />,
-}));
+jest.mock('lucide-react-native', () => ({
+  CalendarDays: () => {
+    const { View } = require('react-native');
+    return <View testID="calendar-days-icon" />;
+  },
+  Check: () => {
+    const { View } = require('react-native');
+    return <View testID="check-icon" />;
+  },
+  ChevronDown: () => {
+    const { View } = require('react-native');
+    return <View testID="chevron-down-icon" />;
+  },
+  Plus: () => {
+    const { View } = require('react-native');
+    return <View testID="plus-icon" />;
+  },
+  Send: () => {
+    const { View } = require('react-native');
+    return <View testID="send-icon" />;
+  },
+  Users: () => {
+    const { View } = require('react-native');
+    return <View testID="users-icon" />;
+  },
+  X: () => {
+    const { View } = require('react-native');
+    return <View testID="x-icon" />;
+  },
+}));
src/components/calendar/enhanced-calendar-view.tsx (1)

56-83: Marking type mismatch: multi-day ranges not rendered with 'multi-dot'

You’re computing startingDay/endingDay/color (period markings) but the Calendar uses markingType="multi-dot". As-is, only the start day gets a dot; intermediate days in a multi-day event are unmarked. Either switch to markingType="period" or add dots for every day in the range and remove period-only fields.

Minimal fix while keeping multi-dot:

-// If it's a multi-day event, mark the range
+// If it's a multi-day event, mark every day in the range for multi-dot
 if (startDate !== endDate) {
-  const start = new Date(startDate);
-  const end = new Date(endDate);
-  const current = new Date(start);
+  // Build local Date objects to avoid UTC parsing
+  const [sy, sm, sd] = startDate.split('-').map(Number);
+  const [ey, em, ed] = endDate.split('-').map(Number);
+  const startLocal = new Date(sy, sm - 1, sd);
+  const endLocal = new Date(ey, em - 1, ed);
+  const current = new Date(startLocal);
 
-  while (current <= end) {
+  while (current <= endLocal) {
     const dateStr = formatLocalDateString(current);
     if (!marked[dateStr]) {
       marked[dateStr] = {
         marked: true,
         dots: [],
       };
     }
-
-    // Add period marking for multi-day events
-    if (dateStr === startDate) {
-      marked[dateStr].startingDay = true;
-      marked[dateStr].color = item.TypeColor || '#3B82F6';
-    } else if (dateStr === endDate) {
-      marked[dateStr].endingDay = true;
-      marked[dateStr].color = item.TypeColor || '#3B82F6';
-    } else {
-      marked[dateStr].color = item.TypeColor || '#3B82F6';
-    }
+    // Add a dot for each day of the multi-day event
+    marked[dateStr].dots.push({
+      key: item.CalendarItemId,
+      color: item.TypeColor || '#3B82F6',
+    });
 
     current.setDate(current.getDate() + 1);
   }
 }

Alternatively, if you prefer range visualization, change markingType="period" and keep the startingDay/endingDay logic (but still fix the UTC parsing as above).

Also applies to: 183-184

src/components/calendar/__tests__/calendar-card.test.tsx (1)

296-304: Fix press handler test to target the Pressable, not inner text

Pressing the title Text may not invoke Pressable’s onPress with the current mocks. Trigger the event on the Pressable testID to avoid flakiness.

Apply this diff:

-    const { getByText } = render(
+    const { getByTestId } = render(
       <CalendarCard item={item} onPress={mockOnPress} />
     );
 
-    fireEvent.press(getByText('Test Event'));
+    fireEvent.press(getByTestId('pressable'));
     expect(mockOnPress).toHaveBeenCalledTimes(1);
src/components/messages/compose-message-sheet.tsx (2)

515-538: Units tab missing from the tab selector (functional gap)

You render Units content later, but the tab list only includes personnel, groups, roles. Users can’t switch to Units.

- {['personnel', 'groups', 'roles'].map((tab) => (
+ {['personnel', 'groups', 'roles', 'units'].map((tab) => (
    <Pressable
      key={tab}
      className={`flex-1 rounded-lg px-1 py-2 ${currentRecipientTab === tab ? 'bg-primary-500' : 'bg-gray-200 dark:bg-gray-600'}`}
      onPress={() => {
        const oldTab = currentRecipientTab;
        setCurrentRecipientTab(tab as any);
        // tracking ...
      }}
    >
-     <Text className={`text-center text-sm font-medium ${currentRecipientTab === tab ? 'text-white' : 'text-gray-700 dark:text-gray-300'}`}>{t(`calls.${tab}`)}</Text>
+     <Text className={`text-center text-sm font-medium ${currentRecipientTab === tab ? 'text-white' : 'text-gray-700 dark:text-gray-300'}`}>{t(`calls.${tab}`)}</Text>
    </Pressable>
  ))}

240-253: Handle singular/plural recipient types and provide a safe default

Some data sets use singular (Group, Unit) while others use plural. Expand mapping to avoid misclassification.

-const getRecipientType = (typeString: string): number => {
-  switch (typeString) {
-    case 'Personnel':
-      return 1;
-    case 'Groups':
-      return 2;
-    case 'Roles':
-      return 3;
-    case 'Unit':
-      return 4;
-    default:
-      return 1;
-  }
-};
+const getRecipientType = (typeString: string): number => {
+  const t = (typeString || '').toLowerCase();
+  if (t === 'personnel' || t === 'user' || t === 'users') return 1;
+  if (t === 'group' || t === 'groups') return 2;
+  if (t === 'role' || t === 'roles') return 3;
+  if (t === 'unit' || t === 'units') return 4;
+  return 1; // default to personnel
+};
🧹 Nitpick comments (70)
.github/dependabot.yml (1)

10-10: Pauses npm version-update PRs (limit=0). Confirm intent; consider batching or re-targeting instead.

Setting open-pull-requests-limit: 0 effectively prevents Dependabot from opening version-update PRs for the npm updater. If your goal is to reduce PR noise (not fully pause), consider batching minor/patch updates, lowering the cap, and/or targeting your integration branch.

Apply this minimal change if you just want to reduce churn while keeping updates flowing:

-    open-pull-requests-limit: 0
+    open-pull-requests-limit: 2

Optional improvements (illustrative YAML) to batch updates weekly and target your working branch:

updates:
  - package-ecosystem: "npm"
    directory: "/"
    target-branch: "develop"        # if PRs should go to develop, not master
    groups:
      all-minor-patch:
        applies-to: version-updates
        update-types:
          - "minor"
          - "patch"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
      timezone: "America/Los_Angeles"

Note: Version-update settings don’t typically govern security update PRs configured via repo settings. If your intent is a full pause, keep 0 and add a comment explaining when to revert. Otherwise, prefer batching + a small cap.

src/api/calendar/calendar.ts (1)

43-45: Replace magic numbers with named mapping; confirm server codes 1/4 are final

Using raw 1/4 reduces readability and is brittle if the API changes. Introduce a small mapper or enum and use it here. Also, confirm that the contract is indeed 1 for attending and 4 for not attending.

Proposed change within this block:

-    CalendarEventId: params.calendarItemId,
-    Type: params.attending === true ? 1 : 4,
+    CalendarEventId: params.calendarItemId,
+    Type: attendingToApiType(params.attending),
     Note: params.note || '',

Add this helper near the top of the file (outside this block):

// Prefer a tiny mapping function over magic numbers
const attendingToApiType = (attending: boolean): 1 | 4 => (attending ? 1 : 4);
// Or, if you prefer explicit names:
// enum CalendarAttendanceType { Attending = 1, NotAttending = 4 }
// const attendingToApiType = (a: boolean): CalendarAttendanceType => a ? CalendarAttendanceType.Attending : CalendarAttendanceType.NotAttending;
src/components/sidebar/side-menu.tsx (2)

194-203: Simplify nested ternary: connected branch duplicates dark/light classes

When isConnected is true, both dark/light paths return the same classes. Collapse to a single branch for readability.

Apply:

-              className={`flex-1 rounded-lg border px-3 py-2 ${
-                isConnected
-                  ? isDark
-                    ? 'border-green-600 bg-green-600 hover:bg-green-700 active:bg-green-800'
-                    : 'border-green-600 bg-green-600 hover:bg-green-700 active:bg-green-800'
-                  : isDark
-                    ? 'border-green-600 bg-transparent hover:bg-green-900/20 active:bg-green-900/30'
-                    : 'border-green-600 bg-transparent hover:bg-green-50 active:bg-green-100'
-              }`}
+              className={`flex-1 rounded-lg border px-3 py-2 ${
+                isConnected
+                  ? 'border-green-600 bg-green-600 hover:bg-green-700 active:bg-green-800'
+                  : isDark
+                    ? 'border-green-600 bg-transparent hover:bg-green-900/20 active:bg-green-900/30'
+                    : 'border-green-600 bg-transparent hover:bg-green-50 active:bg-green-100'
+              }`}

220-227: Ditto: remove redundant dark/light split for the “playing” branch

The connected/playing branch yields identical classes regardless of theme.

Apply:

-              className={`flex-1 rounded-lg border px-3 py-2 ${
-                currentStream && isPlaying
-                  ? isDark
-                    ? 'border-blue-600 bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
-                    : 'border-blue-600 bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
-                  : isDark
-                    ? 'border-blue-600 bg-transparent hover:bg-blue-900/20 active:bg-blue-900/30'
-                    : 'border-blue-600 bg-transparent hover:bg-blue-50 active:bg-blue-100'
-              }`}
+              className={`flex-1 rounded-lg border px-3 py-2 ${
+                currentStream && isPlaying
+                  ? 'border-blue-600 bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
+                  : isDark
+                    ? 'border-blue-600 bg-transparent hover:bg-blue-900/20 active:bg-blue-900/30'
+                    : 'border-blue-600 bg-transparent hover:bg-blue-50 active:bg-blue-100'
+              }`}
.github/workflows/react-native-cicd.yml (1)

161-172: Harden cross-OS jq install for CI robustness

Minor: set noninteractive apt flags and avoid long brew updates; also handle existing jq by no-op installs.

Apply:

-          # Ensure jq is available on both Linux and macOS
+          # Ensure jq is available on both Linux and macOS
           if ! command -v jq &> /dev/null; then
             echo "Installing jq..."
-            if [ "${RUNNER_OS}" = "Linux" ]; then
-              sudo apt-get update && sudo apt-get install -y jq
+            if [ "${RUNNER_OS}" = "Linux" ]; then
+              export DEBIAN_FRONTEND=noninteractive
+              sudo apt-get -yqq update
+              sudo apt-get -yqq install jq
             elif [ "${RUNNER_OS}" = "macOS" ]; then
-              brew update || true
-              brew install jq
+              export HOMEBREW_NO_AUTO_UPDATE=1
+              brew install jq || brew upgrade jq || true
             else
               echo "Unsupported runner OS: ${RUNNER_OS}" >&2
               exit 1
             fi
           fi
src/lib/utils.ts (1)

258-286: Harden isSameDate against invalid inputs and normalize via local date key

If either parsed date is invalid, current code silently compares NaN-derived values and returns false; also, the comparison can be simplified by reusing formatLocalDateString. Suggest:

Apply this diff:

-export function isSameDate(date1: string | Date, date2: string | Date): boolean {
-  // Helper function to create a date from string, handling date-only strings as local dates
-  const createDate = (date: string | Date): Date => {
-    if (date instanceof Date) {
-      return date;
-    }
-
-    // If it's a date-only string (YYYY-MM-DD), treat it as local date
-    if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
-      const [year, month, day] = date.split('-').map(Number);
-      return new Date(year, month - 1, day); // Month is 0-indexed
-    }
-
-    // Otherwise, parse as usual (handles ISO strings with time)
-    return new Date(date);
-  };
-
-  const d1 = createDate(date1);
-  const d2 = createDate(date2);
-
-  // Use local date methods for comparison to match user's timezone context
-  // This ensures calendar items appear on the correct day as intended by the backend
-  return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
-}
+export function isSameDate(date1: string | Date, date2: string | Date): boolean {
+  const toDate = (val: string | Date): Date => {
+    if (val instanceof Date) return val;
+    if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
+      const [y, m, d] = val.split('-').map(Number);
+      return new Date(y, m - 1, d);
+    }
+    return new Date(val);
+  };
+
+  const d1 = toDate(date1);
+  const d2 = toDate(date2);
+
+  if (Number.isNaN(d1.getTime()) || Number.isNaN(d2.getTime())) return false;
+
+  return formatLocalDateString(d1) === formatLocalDateString(d2);
+}

Also consider adding tests for date-only strings and invalid inputs (see test suggestion below).

src/components/calendar/calendar-view.tsx (2)

62-64: Using item.Start for day matching — consistent with .NET API, but precompute for O(1) lookups

The approach is correct. For large months, consider precomputing a Set of local day keys for selectedMonthItems to avoid O(n) scans per cell.

Example (outside render):

const eventDaySet = useMemo(() => {
  const s = new Set<string>();
  for (const it of selectedMonthItems) s.add(new Date(it.Start).toDateString());
  return s;
}, [selectedMonthItems]);

const hasEventsOnDate = (date: Date) => eventDaySet.has(date.toDateString());

121-126: Use ternary instead of && for conditional rendering (project guideline)

The codebase guidelines prefer ?: over && for conditionals. Replace the dot indicator render with a ternary.

Apply this diff:

-        {hasEvents && <View className={['absolute bottom-1 h-1 w-1 rounded-full', isSelected ? 'bg-white' : 'bg-amber-500'].join(' ')} />}
+        {hasEvents ? (
+          <View className={['absolute bottom-1 h-1 w-1 rounded-full', isSelected ? 'bg-white' : 'bg-amber-500'].join(' ')} />
+        ) : null}
src/lib/__tests__/utils-date.test.ts (1)

1-92: Add tests for date-only strings and invalid inputs

To lock in the intended behavior of isSameDate/isToday with 'YYYY-MM-DD' strings and guard against invalid dates, extend coverage as below.

Apply this diff at the end of the file:

@@
   });
 });
+
+describe('Date Utilities - additional cases', () => {
+  beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(new Date('2024-01-15T10:00:00Z'));
+  });
+  afterEach(() => jest.useRealTimers());
+
+  it('isSameDate should handle date-only strings as local dates', () => {
+    expect(isSameDate('2024-01-15', '2024-01-15T23:59:59Z')).toBe(true);
+    expect(isSameDate('2024-01-15', '2024-01-16T00:00:00Z')).toBe(false);
+  });
+
+  it('isSameDate should return false for invalid inputs', () => {
+    // @ts-expect-error testing runtime behavior with invalid strings
+    expect(isSameDate('not-a-date', '2024-01-15')).toBe(false);
+  });
+
+  it('isToday should handle date-only strings', () => {
+    expect(isToday('2024-01-15')).toBe(true);
+    expect(isToday('2024-01-16')).toBe(false);
+  });
+});
src/components/calendar/compact-calendar-item.tsx (2)

50-53: Accessibility: mark Pressable as a button

Small win for screen readers.

Apply this diff:

-    <Pressable onPress={onPress} testID={testID} className="mb-2">
+    <Pressable onPress={onPress} testID={testID} accessibilityRole="button" className="mb-2">

20-21: Consider memoizing component to reduce list re-renders

If rendered in lists, wrapping in React.memo can avoid unnecessary updates when props are referentially equal.

Example:

export const CompactCalendarItem = React.memo<CompactCalendarItemProps>(/* existing FC */);
src/translations/es.json (1)

609-616: Remove unused top-level translation keys

The codebase only ever calls

t('messages.validation.body_required')
t('messages.validation.recipients_required')
t('messages.validation.subject_required')

and never references the legacy messages.body_required, messages.recipients_required or messages.subject_required. You can safely clean up the duplicate entries in the Spanish locale.

• File: src/translations/es.json
• Remove these keys (around lines 610–616):

   },
-  "unsaved_changes": "Cambios No Guardados",
-  "unsaved_changes_message": "Tienes cambios no guardados. ¿Estás seguro de que quieres descartarlos?",
-  "validation": {
-    "body_required": "El cuerpo del mensaje es obligatorio",
-    "recipients_required": "Se requiere al menos un destinatario",
-    "subject_required": "El asunto es obligatorio"
-  }
+  "unsaved_changes": "Cambios No Guardados",
+  "unsaved_changes_message": "Tienes cambios no guardados. ¿Estás seguro de que quieres descartarlos?",
+  "validation": {
+    "body_required": "El cuerpo del mensaje es obligatorio",
+    "recipients_required": "Se requiere al menos un destinatario",
+    "subject_required": "El asunto es obligatorio"
+  }

• Delete the orphaned top-level keys:

"body_required": "...",
"recipients_required": "...",
"subject_required": "..."

This eliminates duplication and keeps only the active namespace.

src/utils/webview-html.ts (3)

1-9: Export the options interface for reuse

Exporting WebViewHtmlOptions enables consumers to type their local utilities and props.

-interface WebViewHtmlOptions {
+export interface WebViewHtmlOptions {
   content: string;
   isDarkMode: boolean;
   fontSize?: number;
   lineHeight?: number;
   padding?: number;
   backgroundColor?: string;
   textColor?: string;
 }

85-86: Clamp code font size to avoid negative or tiny values

fontSize - 2 can underflow if callers pass small values. Clamp to a sensible minimum.

-            font-size: ${fontSize - 2}px;
+            font-size: ${Math.max(fontSize - 2, 10)}px;

119-132: Type props as WebViewProps and include 'data:' in origin whitelist

  • Type the object to catch future prop typos.
  • originWhitelist typically needs 'about:' and 'data:' for inline HTML sources across platforms.
  • Consider making androidLayerType configurable; forcing 'software' globally can regress perf on some devices.
-import const
-export const defaultWebViewProps = {
+import type { WebViewProps } from 'react-native-webview';
+
+export const defaultWebViewProps: Pick<WebViewProps,
+  | 'originWhitelist'
+  | 'javaScriptEnabled'
+  | 'domStorageEnabled'
+  | 'startInLoadingState'
+  | 'mixedContentMode'
+  | 'androidLayerType'
+  | 'showsVerticalScrollIndicator'
+  | 'showsHorizontalScrollIndicator'
+> = {
   // Security: Only allow local content, no external origins
-  originWhitelist: ['about:'],
+  originWhitelist: ['about:', 'data:'],
   // Security: Disable JavaScript and DOM storage by default
   javaScriptEnabled: false,
   domStorageEnabled: false,
   // Performance and UX
   startInLoadingState: false,
   mixedContentMode: 'compatibility' as const,
   androidLayerType: 'software' as const,
   // Scroll behavior
   showsVerticalScrollIndicator: true,
   showsHorizontalScrollIndicator: false,
 };
src/lib/__tests__/utils.test.ts (2)

180-207: Test title mentions null/undefined but doesn’t assert them

The "handles null and undefined gracefully" case only asserts a happy-path call. Either rename or add assertions for null/undefined inputs where the API accepts union types.

-  describe('Edge Cases and Error Handling', () => {
+  describe('Edge Cases and Error Handling', () => {
@@
-    it('handles null and undefined gracefully', () => {
-      const validDate = new Date(2024, 0, 15);
-      
-      // These might be passed due to type issues in JavaScript
-      expect(() => formatLocalDateString(validDate)).not.toThrow();
-    });
+    it('handles null and undefined gracefully (no throws, false comparisons)', () => {
+      const validDate = new Date(2024, 0, 15);
+      // @ts-expect-error deliberate invalid inputs to verify runtime robustness
+      expect(() => isSameDate(null, validDate)).not.toThrow();
+      // @ts-expect-error deliberate invalid inputs to verify runtime robustness
+      expect(() => isSameDate(validDate, undefined)).not.toThrow();
+      // @ts-expect-error runtime returns false for invalids
+      expect(isSameDate(null, validDate)).toBe(false);
+      // @ts-expect-error runtime returns false for invalids
+      expect(isSameDate(validDate, undefined)).toBe(false);
+    });

108-116: Add an invalid Date object case

Covers NaN Dates constructed via new Date('invalid'), which can leak into code paths from external inputs.

     it('handles ISO strings with time correctly', () => {
@@
       expect(isSameDate(date1, date3)).toBe(false);
     });
+
+    it('handles invalid Date objects (NaN) without throwing', () => {
+      // @ts-expect-error constructing invalid date intentionally
+      const badDate = new Date('invalid');
+      const goodDate = new Date(2024, 0, 15);
+      expect(() => isSameDate(badDate, goodDate)).not.toThrow();
+      expect(isSameDate(badDate as unknown as Date, goodDate)).toBe(false);
+    });
src/components/contacts/contact-notes-list.tsx (4)

33-36: Remove unused variable and lean on shared utils

backgroundColor is computed but unused. Also, instead of re-deriving colors for WebView styling here, prefer the centralized generator in src/utils/webview-html.ts for consistency.

-  const { colorScheme } = useColorScheme();
-  const textColor = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
-  const backgroundColor = colorScheme === 'dark' ? '#374151' : '#F9FAFB';
+  const { colorScheme } = useColorScheme();

236-261: Track analytics after data loads to avoid noisy events

As written, the effect can fire multiple times (notes.length / hasNotes change). Track once post-load per contactId, or when isNotesLoading transitions to false.

-  // Track when contact notes list is rendered
-  React.useEffect(() => {
-    if (contactId) {
-      trackEvent('contact_notes_list_rendered', {
-        contactId: contactId,
-        notesCount: notes.length,
-        hasNotes: hasNotes,
-        isLoading: isNotesLoading,
-      });
-    }
-  }, [trackEvent, contactId, notes.length, hasNotes, isNotesLoading]);
+  // Track when notes are loaded for a contact
+  React.useEffect(() => {
+    if (contactId && !isNotesLoading) {
+      trackEvent('contact_notes_list_rendered', {
+        contactId,
+        notesCount: notes.length,
+        hasNotes,
+      });
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [contactId, isNotesLoading]);

27-35: Memoize ContactNoteCard to reduce re-renders

Notes list can be long; memoize by props to avoid unnecessary rerenders on list updates.

-const ContactNoteCard: React.FC<ContactNoteCardProps> = ({ note }) => {
+const ContactNoteCardComponent: React.FC<ContactNoteCardProps> = ({ note }) => {
   ...
-};
+};
+
+const ContactNoteCard = React.memo(ContactNoteCardComponent);

303-313: Remove unused styles to reduce noise

styles.container is defined but not used.

 const styles = StyleSheet.create({
-  container: {
-    width: '100%',
-    backgroundColor: 'transparent',
-  },
   webView: {
     height: 200, // Fixed height with scroll capability
     backgroundColor: 'transparent',
     width: '100%',
   },
 });
docs/calendar-card-webview-refactoring.md (1)

76-90: Example should reflect shared defaults

Mirror the shared defaults, including origin whitelist for data/about.

-<WebView
-  {...defaultWebViewProps}
+<WebView
+  {...defaultWebViewProps}
   source={{
     html: generateWebViewHtml({
       content: htmlContent,
       isDarkMode: colorScheme === 'dark',
     }),
   }}
 />
docs/calendar-item-details-personnel-loading-enhancement.md (1)

41-51: Scope the loading UI to avoid masking known names

Current guidance returns t('loading') whenever isInitializing || isPersonnelLoading, which can briefly replace a previously resolved creator name during background refetches. Prefer returning the known name if it’s already in the store, and only show loading when a lookup is actually required.

Example adjustment to the snippet:

-// Loading detection in getCreatorName
-if (isInitializing || isPersonnelLoading) {
-  return t('loading');
-}
+// Loading detection in getCreatorName
+const creator = personnel.find(p => p.UserId === item.CreatorUserId);
+if (creator) {
+  return `${creator.FirstName} ${creator.LastName}`.trim();
+}
+if (isInitializing || isPersonnelLoading) {
+  return t('loading');
+}
+return t('unknown_user');
src/components/messages/__tests__/compose-message-sheet.test.tsx (2)

551-575: Prefer stable testIDs over translation-dependent selectors

getByText('messages.send') and placeholders tied to translation keys are brittle. Expose testIDs in ComposeMessageSheet (e.g., send-button, subject-input) and use them here for stability across locales.

Example change in the test:

-const sendButton = getByText('messages.send');
+const sendButton = getByTestId('send-button');

And add in ComposeMessageSheet:

-<Button ... onPress={handleSend}>
+<Button testID="send-button" ... onPress={handleSend}>

33-51: Pluralization mock is not representative; improve count handling

The i18n mock replaces '{{count}}' within the key string, which most keys won’t contain. Returning a deterministic string that includes count helps assertions for pluralized messages.

- if (options && typeof options === 'object' && 'count' in options) {
-   return `${key.replace('{{count}}', options.count)}`;
- }
+ if (options && typeof options === 'object' && 'count' in options) {
+   return `${key}:${options.count}`;
+ }
src/components/calendar/__tests__/calendar-item-details-sheet-minimal.test.tsx (2)

35-43: Be cautious with broad react-native mocks

Overriding react-native with a minimal surface can hide regressions if the component starts using unmocked APIs (e.g., Platform, Dimensions). Consider leveraging the default RN Jest preset and only mocking specific primitives you need.


145-163: Analytics assertions look solid

The payload fields align with likely production tracking. Consider adding one test covering error handling (e.g., trackEvent throwing) similar to the compose message tests to assert graceful degradation.

src/app/(app)/calendar.tsx (4)

109-116: Memoize and null-guard selected-date filtering

getItemsForSelectedDate() is recomputed multiple times per render and assumes selectedMonthItems is defined. Memoize and guard to avoid repeated work and potential NPEs.

-const getItemsForSelectedDate = () => {
-  if (!selectedDate) return [];
-
-  return selectedMonthItems.filter((item) => {
-    // Use Start field for consistent date comparison with .NET backend timezone-aware dates
-    return isSameDate(item.Start, selectedDate);
-  });
-};
+const itemsForSelectedDate = React.useMemo(() => {
+  if (!selectedDate) return [];
+  const src = selectedMonthItems ?? [];
+  return src.filter((item) => isSameDate(item.Start, selectedDate));
+}, [selectedDate, selectedMonthItems]);

And update usages below to reference itemsForSelectedDate.


139-140: Stabilize renderItem callbacks with useCallback

To align with the project guideline “Avoid anonymous functions in renderItem or event handlers,” wrap renderers in useCallback. This reduces churn in FlatList and can improve perf.

-const renderCompactCalendarItem = ({ item }: { item: CalendarItemResultData }) => <CompactCalendarItem item={item} onPress={() => handleItemPress(item)} />;
+const renderCompactCalendarItem = React.useCallback(
+  ({ item }: { item: CalendarItemResultData }) => (
+    <CompactCalendarItem item={item} onPress={() => handleItemPress(item)} />
+  ),
+  [handleItemPress]
+);

Similarly consider wrapping handleItemPress itself in useCallback.


205-217: Avoid repeated filtering in render; simplify conditional

Use the memoized itemsForSelectedDate to compute once and simplify the ternary.

- {isLoading ? (
-   <Loading text={t('calendar.loading.date')} />
- ) : getItemsForSelectedDate().length === 0 ? (
-   <Text className="py-8 text-center text-gray-500 dark:text-gray-400">{t('calendar.selectedDate.empty')}</Text>
- ) : (
-   <FlatList data={getItemsForSelectedDate()} renderItem={renderCompactCalendarItem} keyExtractor={(item) => item.CalendarItemId} showsVerticalScrollIndicator={false} />
- )}
+ {isLoading ? (
+   <Loading text={t('calendar.loading.date')} />
+ ) : itemsForSelectedDate.length === 0 ? (
+   <Text className="py-8 text-center text-gray-500 dark:text-gray-400">{t('calendar.selectedDate.empty')}</Text>
+ ) : (
+   <FlatList
+     data={itemsForSelectedDate}
+     renderItem={renderCompactCalendarItem}
+     keyExtractor={(item) => item.CalendarItemId}
+     showsVerticalScrollIndicator={false}
+   />
+ )}

160-170: Optional: tune FlatList perf

Add props like removeClippedSubviews, windowSize, and maxToRenderPerBatch for long lists. Not mandatory, but aligns with app guidelines.

   <FlatList
     data={todayCalendarItems}
     renderItem={renderCalendarItem}
     keyExtractor={(item) => item.CalendarItemId}
     className="flex-1"
     contentContainerStyle={{ padding: 16 }}
     showsVerticalScrollIndicator={false}
+    removeClippedSubviews
+    windowSize={5}
+    maxToRenderPerBatch={10}
     refreshControl={<RefreshControl refreshing={isTodaysLoading} onRefresh={handleRefresh} colors={['#3B82F6']} tintColor="#3B82F6" />}
   />

Apply similarly to the upcoming list if needed.

Also applies to: 192-201

src/utils/__tests__/webview-html.test.ts (3)

29-45: Add guard/test for code block font-size floor

generateWebViewHtml derives code font-size as fontSize - 2. If fontSize is small (<= 2), this can underflow to 0px or negative. Add a floor in the generator (e.g., Math.max(fontSize - 2, 10)) and a test for small font sizes.

Apply this test and generator adjustment:

-      expect(html).toContain('font-size: 18px');
+      expect(html).toContain('font-size: 18px');
       expect(html).toContain('line-height: 1.8');
       expect(html).toContain('padding: 12px');
       expect(html).toContain('color: #FF0000');
       expect(html).toContain('background-color: #00FF00');

Outside this file (generator), clamp the code font-size:

-          pre, code {
+          pre, code {
             background-color: ${codeBackgroundColor};
             padding: 8px;
             border-radius: 4px;
             font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
-            font-size: ${fontSize - 2}px;
+            font-size: ${Math.max(fontSize - 2, 10)}px;
           }

47-54: Consider adding a CSP meta test (privacy hardening)

Remote images/scripts in HTML could leak requests. You’ve mitigated script execution via javaScriptEnabled=false, but subresource loads (e.g., ) can still occur. Consider injecting a restrictive Content Security Policy and add an assertion here to ensure it’s present.

Example CSP to add in the generator (outside this file):

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: blob:; style-src 'unsafe-inline'; frame-ancestors 'none'; form-action 'none'">

Then assert its presence in this test.


71-81: Prop names and secure defaults look correct; add a couple more props if needed

originWhitelist, javaScriptEnabled, and domStorageEnabled are asserted correctly. If your usage doesn’t rely on it, consider also setting allowsInlineMediaPlayback=false and allowingFileAccessFromFileURLs=false in defaultWebViewProps and asserting them here for defense-in-depth.

src/components/calendar/__tests__/calendar-card.test.tsx (2)

18-26: Avoid duplicating the WebView utility mock across test suites

You redefine generateWebViewHtml/defaultWebViewProps here while similar mocks exist elsewhere (and jest-setup). Centralize this in jest-setup to reduce drift and keep behavior consistent between suites.


128-135: Add dark mode signal and status icon tests

Two targeted additions will increase confidence:

  • Verify isDarkMode=true is forwarded to generateWebViewHtml when color scheme is dark.
  • Verify the CheckCircle icon renders when Attending && canSignUp.

Apply this diff near the end of the suite (before the closing "});"):

+  it('passes isDarkMode=true to generateWebViewHtml when color scheme is dark', () => {
+    const item = createMockItem();
+    const { useColorScheme } = require('nativewind');
+    (useColorScheme as jest.Mock).mockReturnValue({ colorScheme: 'dark' });
+
+    render(<CalendarCard item={item} onPress={mockOnPress} />);
+    const { generateWebViewHtml } = require('@/utils/webview-html');
+    expect(generateWebViewHtml).toHaveBeenCalledWith(
+      expect.objectContaining({ isDarkMode: true })
+    );
+  });
+
+  it('shows CheckCircle icon when user is signed up and can sign up', () => {
+    const item = createMockItem({ SignupType: 1, LockEditing: false, Attending: true });
+    const { getByText } = render(<CalendarCard item={item} onPress={mockOnPress} />);
+    expect(getByText('CheckCircle')).toBeTruthy();
+  });
src/stores/calendar/store.ts (3)

121-135: Log range used for the fetch (helps triage mismatches)

Include startDate/endDate in the success log to correlate server results with the client-side filter.

Apply this diff:

-        context: {
-          totalCount: response.Data.length,
-          filteredCount: todayItems.length,
-          //startDate,
-          //endDate,
+        context: {
+          totalCount: response.Data.length,
+          filteredCount: todayItems.length,
+          startDate,
+          endDate,
           items: todayItems.map((item) => ({
             id: item.CalendarItemId,
             title: item.Title,
             start: item.Start,
             startDate: new Date(item.Start).toDateString(),
           })),
         },

110-116: Client-side date filter is a good safety net; consider documenting API expectations

Using isSameDate against item.Start adds resilience against backend TZ quirks. Add a brief comment noting the backend’s Start semantics (.NET timezone-aware) to prevent “simplifications” later.


171-179: Optional: unify date range format across loaders

loadUpcomingCalendarItems uses 'yyyy-MM-dd HH:mm:ss', while loadCalendarItems sends 'YYYY-MM-DD' only. Consider normalizing to start-of-day/end-of-day with the same format for predictability.

Apply this optional diff:

-      const startDate = subDays(new Date(), 90).toISOString().split('T')[0];
-      const endDate = addDays(new Date(), 120).toISOString().split('T')[0];
+      const startDate = format(startOfDay(subDays(new Date(), 90)), 'yyyy-MM-dd HH:mm:ss');
+      const endDate = format(endOfDay(addDays(new Date(), 120)), 'yyyy-MM-dd HH:mm:ss');
src/translations/ar.json (2)

62-62: Shortened calendar.confirmSignup string

The change to "تأكيد" is consistent with the English “Confirm.” Verify button width/truncation in RTL layouts.


711-714: Remove unused translation key “generale” from ar.json
The key settings.generale is not referenced anywhere in the codebase and duplicates the existing settings.general. To keep the translations tidy, you can safely remove it.

• File src/translations/ar.json
– Remove the line:
diff - "generale": "عام",

src/components/calendar/__tests__/component-comparison.test.tsx (2)

24-27: Don’t override the global WebView mock here

This local mock returns null and prevents asserting WebView presence. Remove it and rely on jest-setup’s WebView mock to enable richer assertions across suites.

Apply this diff to delete the local mock:

- jest.mock('react-native-webview', () => ({
-   __esModule: true,
-   default: () => null,
- }));

177-192: Turn the commentary into an assertion

You note the compact component shouldn’t rely on WebView. Assert it explicitly by checking CalendarCard has description-webview while CompactCalendarItem does not.

After removing the local WebView mock, append this test:

+  it('CalendarCard renders description WebView, CompactCalendarItem does not', () => {
+    const mockOnPress = jest.fn();
+    const fullCard = render(<CalendarCard item={mockItem} onPress={mockOnPress} />);
+    expect(fullCard.getByTestId('description-webview')).toBeTruthy();
+
+    const compactCard = render(<CompactCalendarItem item={mockItem} onPress={mockOnPress} />);
+    expect(compactCard.queryByTestId('description-webview')).toBeNull();
+  });
docs/calendar-item-details-webview-implementation.md (2)

20-27: Doc/code drift: scroll indicator and security props differ from implementation

  • Doc states showsVerticalScrollIndicator={false}, but defaultWebViewProps sets it to true. Either override it in components (as calendar-card does) or update the doc to say “components may override scroll indicators.”
  • Doc doesn’t mention javaScriptEnabled/domStorageEnabled being disabled by default; add a note so readers don’t re-enable by accident.

I can submit a PR to this doc aligning it with src/utils/webview-html.ts defaults if you prefer.


93-98: Performance guidance: clarify height caps per component

Doc mentions a 120px cap; calendar-card uses 60px while details sheets may use taller. Clarify that the compact card preview uses 60px and the details sheet uses 120px to avoid confusion when QA verifies layouts.

src/components/calendar/calendar-card.tsx (2)

95-114: Memoize the HTML and explicitly hide scroll indicators to reduce WebView churn

Generating the HTML string inline causes a new string each render, which can trigger unnecessary WebView reloads in FlatLists. Also, defaultWebViewProps enables the vertical indicator; the card disables scrolling, so show no indicators.

Apply this refactor:

-import React from 'react';
+import React, { useMemo } from 'react';
@@
-  const isDarkMode = colorScheme === 'dark';
+  const isDarkMode = colorScheme === 'dark';
+
+  // Avoid regenerating HTML on every render
+  const descriptionHtml = useMemo(
+    () =>
+      item.Description
+        ? generateWebViewHtml({
+            content: item.Description,
+            isDarkMode,
+            fontSize: 14,
+            lineHeight: 1.4,
+            padding: 6,
+          })
+        : '',
+    [item.Description, isDarkMode]
+  );
@@
-              <Box className="w-full rounded bg-gray-50 p-1 dark:bg-gray-700">
+              <Box className="w-full rounded bg-gray-50 p-1 dark:bg-gray-700">
                 <WebView
                   style={styles.webView}
                   {...defaultWebViewProps}
-                  scrollEnabled={false}
-                  source={{
-                    html: generateWebViewHtml({
-                      content: item.Description,
-                      isDarkMode,
-                      fontSize: 14,
-                      lineHeight: 1.4,
-                      padding: 6,
-                    }),
-                  }}
+                  scrollEnabled={false}
+                  showsVerticalScrollIndicator={false}
+                  showsHorizontalScrollIndicator={false}
+                  source={{ html: descriptionHtml }}
                   testID="description-webview"
                 />
               </Box>

29-42: Timezone correctness for user-visible times

Using new Date(dateString).toLocaleTimeString/DateString without an explicit timezone may render unexpected times if the backend date strings are not normalized to the device’s local zone. If your API guarantees ISO strings in local time, you’re fine. Otherwise, consider centralizing formatting in a small utility (e.g., using date-fns-tz) to ensure consistent local-time display and to simplify testing.

Would you like me to wire a tiny formatLocalDate/formatLocalTime helper and unit tests?

src/components/calendar/__tests__/calendar-item-details-sheet-analytics.test.tsx (1)

136-141: Add an assertion that personnel auto-fetch occurs on open when store is empty

The mock component triggers fetchPersonnel when opened and the store is empty. The test suite sets up mockFetchPersonnel but never asserts it’s called. Adding a focused test increases confidence in the side effect.

Apply this test addition:

@@
   it('tracks analytics when sheet becomes visible', () => {
@@
   });
 
+  it('fetches personnel when opened with empty store', () => {
+    render(<CalendarItemDetailsSheet item={mockCalendarItem} isOpen={true} onClose={mockOnClose} />);
+    expect(mockFetchPersonnel).toHaveBeenCalledTimes(1);
+  });
+
src/app/(app)/__tests__/calendar.test.tsx (1)

646-672: Assert store interaction when opening details

In addition to checking analytics and sheet visibility, assert that viewCalendarItemAction was called with the item to validate the store update path.

You can add:

@@
   fireEvent.press(getByTestId('calendar-card'));
@@
   expect(mockTrackEvent).toHaveBeenCalledWith('calendar_item_viewed', {
@@
   });
+  expect(mockStore.viewCalendarItemAction).toHaveBeenCalledWith(mockCalendarItem);
src/components/calendar/__tests__/enhanced-calendar-view.test.tsx (3)

145-154: Strengthen assertion: also verify currentMonth update via Calendar’s current prop

You already validate setSelectedDate(today). Consider also asserting the rendered month reflects “today” to catch regressions where state and calendar get out of sync.

Example (adds an extra check):

   fireEvent.press(getByText('Today'));
   const expectedDate = getTodayLocalString();
   expect(mockStore.setSelectedDate).toHaveBeenCalledWith(expectedDate);
   expect(expectedDate).toBe('2024-01-15');
+  // Optional: assert Calendar current month reflects today
+  // Our Calendar mock renders "Current: YYYY-MM-01" text
+  expect(getByText(`Current: ${expectedDate.slice(0,7)}-01`)).toBeTruthy();

244-256: Assert exact month-range boundaries for February 2024

You currently check the YYYY-MM-DD shape. Given the input (year: 2024, month: 2), assert exact values to lock the end-of-month logic and catch off-by-one/timezone regressions.

- expect(mockStore.loadCalendarItemsForDateRange).toHaveBeenCalledWith(
-   expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
-   expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/)
- );
+ expect(mockStore.loadCalendarItemsForDateRange).toHaveBeenCalledWith(
+   '2024-02-01',
+   '2024-02-29'
+ );

367-407: Marking assertions are incomplete

Both “marks dates that have events” and “handles multi-day events” tests don’t assert anything about markedDates. Since the Calendar is mocked, expose the markedDates prop on the mock to assert that event dates are marked as expected.

Update the Calendar mock to render JSON.stringify(markedDates) in a testID, then assert presence of the relevant keys (2024-01-15, 2024-01-16, 2024-01-17, etc.).

src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx (2)

75-77: Use React Native primitives in mocks for RN environment

Using <div> in the Box mock may be brittle in RN test environments. Prefer View from react-native for consistency.

-jest.mock('@/components/ui/box', () => ({
-  Box: ({ children }: any) => <div>{children}</div>,
-}));
+jest.mock('@/components/ui/box', () => {
+  const { View } = require('react-native');
+  return { Box: ({ children }: any) => <View>{children}</View> };
+});

969-989: Make unknown-creator assertion more robust

Stringifying bottomSheet.props.children is fragile. Assert the translated fallback is rendered instead (your t mock returns keys).

-const bottomSheet = getByTestId('bottom-sheet');
-const bottomSheetText = bottomSheet.props.children.toString();
-expect(bottomSheetText).not.toContain('definitely-not-in-personnel-list');
+// Since t('unknown_user') returns the key, verify it appears
+// and that the raw ID is not leaked.
+expect(getByTestId('bottom-sheet')).toBeTruthy();
+// You can refine your mocks to make this exact:
+// expect(getByText(/unknown_user/)).toBeTruthy();
src/components/messages/compose-message-sheet.tsx (6)

439-441: Avoid currentColor with lucide-react-native

currentColor isn’t a React Native color and may render unpredictably. Use a concrete color token for the non-error state.

-<ChevronDown size={20} color={errors.recipients ? '#dc2626' : 'currentColor'} />
+<ChevronDown size={20} color={errors.recipients ? '#dc2626' : '#6B7280'} />

313-342: Tighten types for recipients list rendering

Avoid any[] to align with codebase guidelines. Introduce a lightweight type covering what you use (Id, Name).

-const renderRecipientList = (recipients: any[], type: string) => (
+type BasicRecipient = { Id: string; Name: string };
+const renderRecipientList = (recipients: BasicRecipient[], type: string) => (

If the dispatch store exposes richer types, import and use them directly.


370-405: Inline handlers create avoidable re-renders

Multiple inline handlers (onValueChange, onPress) violate the project guideline to avoid anonymous functions in event handlers. Use useCallback to memoize recurrent handlers.

Example:

-<Select
-  selectedValue={messageType.toString()}
-  onValueChange={(value) => {
-    const newType = parseInt(value);
-    const oldType = messageType;
-    setMessageType(newType);
-    // track...
-  }}
->
+const onChangeMessageType = useCallback((value: string) => {
+  const newType = parseInt(value, 10);
+  setMessageType((oldType) => {
+    // track with oldType/newType here...
+    return newType;
+  });
+}, [trackEvent]);
+...
+<Select selectedValue={messageType.toString()} onValueChange={onChangeMessageType}>

Repeat similarly for the tabs and recipients sheet open handler.


176-238: Consider react-hook-form for forms

Project guidelines prefer react-hook-form. Migrating would simplify validation, error messaging, and reduce local state plumbing, especially as the form grows (poll options, attachments, etc.).

I can sketch a react-hook-form refactor using Controller for InputField/TextareaInput if you want.


181-193: Reduce repeated recipient lookups

recipientsList construction and analytics both rebuild and search across the same four arrays. Memoize the combined list.

Add near imports:

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

Add once inside the component:

const allRecipients = useMemo(
  () => [...dispatchData.users, ...dispatchData.groups, ...dispatchData.roles, ...dispatchData.units],
  [dispatchData.users, dispatchData.groups, dispatchData.roles, dispatchData.units]
);

Then replace subsequent spreads with allRecipients.

Also applies to: 286-301


487-493: Accessibility: add roles/labels

Consider accessibilityRole="button" and descriptive accessibilityLabel for the Send button to improve screen reader UX.

-<Button ... onPress={handleSend} disabled={isSending} size="lg">
+<Button accessibilityRole="button" accessibilityLabel={t('messages.send')} onPress={handleSend} disabled={isSending} size="lg" className="w-full bg-primary-600 shadow-lg dark:bg-primary-500">
docs/compose-message-sheet-improvements.md (3)

110-124: Ensure consistency across language examples

The “English (en.json)” block is correct. Consider mirroring exact nesting for ES/AR or explicitly stating they follow the same messages.validation.* structure.


8-18: Copy tweaks for clarity and concision

Several headings have minor grammar/style nits flagged by tooling. If you want, I can run a quick pass to tighten phrasing and fix small grammatical issues.


129-151: Claim vs. code: type-safety statement

The doc claims “Proper TypeScript typing for all new features,” but the component still uses any[] in renderRecipientList. Either update the doc or tighten the types (recommended).

src/stores/calendar/__tests__/store.test.ts (7)

28-31: Prefer partial mocking for utils to avoid dropping other exports.

Current mock replaces the entire module with { isSameDate }. If the store (now or later) imports additional util exports, they’ll be undefined in tests. Use requireActual and override only what you need.

-jest.mock('@/lib/utils', () => ({
-  isSameDate: jest.fn(),
-}));
+jest.mock('@/lib/utils', () => {
+  const actual = jest.requireActual('@/lib/utils');
+  return { ...actual, isSameDate: jest.fn() };
+});

206-217: Ensure explicit mock restoration between tests when overriding isSameDate.

You override isSameDate within this test. While your beforeEach reassigns an implementation, adding a mockReset() in afterEach inside this describe prevents accidental bleed should test order change.

 afterEach(() => {
-  jest.useRealTimers();
+  jest.useRealTimers();
+  mockedUtils.isSameDate.mockReset();
 });

224-228: Avoid hard-coding the ISO timestamp; derive it from the frozen system time.

Hard-coding ties the assertion to a specific literal and can be brittle if the frozen time changes. Use the value computed from the fake clock to keep intent while staying resilient.

-expect(mockedApi.getCalendarItemsForDateRange).toHaveBeenCalledWith(
-  '2024-01-15T10:00:00.000Z',
-  '2024-01-15T10:00:00.000Z'
-);
+const iso = new Date().toISOString();
+expect(mockedApi.getCalendarItemsForDateRange).toHaveBeenCalledWith(iso, iso);

255-291: Timezone-variance scenario is valuable; consider asserting loading flags as well.

The test correctly verifies both UTC and offset-based times map to “today.” For completeness and parity with other tests, you could also assert isTodaysLoading === false and error === null after the call.

 expect(result.current.todayCalendarItems).toHaveLength(2);
+expect(result.current.isTodaysLoading).toBe(false);
+expect(result.current.error).toBeNull();

652-679: Use domain-accurate “not attending” status (4) instead of 0.

The store treats any status other than 1 as “not attending”, but the documented value is 4. Using 4 here keeps tests aligned with API semantics and reduces risk if the implementation later tightens allowed values.

-await result.current.setCalendarItemAttendingStatus('123', '', 0);
+await result.current.setCalendarItemAttendingStatus('123', '', 4);

499-507: Nice initialization test; consider asserting loading/error flags reset post-init.

init() fans out to three async actions and toggles multiple loading flags. Adding simple assertions for isTypesLoading, isTodaysLoading, and isUpcomingLoading being false (and error === null) would lock in UX guarantees.

 await result.current.init();
 expect(result.current.itemTypes).toEqual(mockTypesResponse.Data);
 expect(result.current.todayCalendarItems).toEqual([mockCalendarItem]);
 expect(result.current.upcomingCalendarItems).toEqual([mockCalendarItem]);
+expect(result.current.isTypesLoading).toBe(false);
+expect(result.current.isTodaysLoading).toBe(false);
+expect(result.current.isUpcomingLoading).toBe(false);
+expect(result.current.error).toBeNull();

Also applies to: 523-531


127-135: Ensure UTC date comparisons and pin test timezone to UTC

To make your date tests deterministic across all environments:

• In src/stores/calendar/__tests__/store.test.ts, switch your mock to use UTC getters.
• In your global setup (jest-setup.ts), pin process.env.TZ = 'UTC' so every test runs in UTC.

Locations needing updates:

  • src/stores/calendar/__tests__/store.test.ts (lines ~127–135)
  • jest-setup.ts (repo root)

Suggested diffs:

--- a/src/stores/calendar/__tests__/store.test.ts
+++ b/src/stores/calendar/__tests__/store.test.ts
@@ 127,8
- mockedUtils.isSameDate.mockImplementation((date1: string | Date, date2: string | Date) => {
-   const d1 = new Date(date1);
-   const d2 = new Date(date2);
-   return d1.getFullYear() === d2.getFullYear() && 
-          d1.getMonth() === d2.getMonth() && 
-          d1.getDate() === d2.getDate();
- });
+ mockedUtils.isSameDate.mockImplementation((date1: string | Date, date2: string | Date) => {
+   const d1 = new Date(date1);
+   const d2 = new Date(date2);
+   return d1.getUTCFullYear() === d2.getUTCFullYear() &&
+          d1.getUTCMonth() === d2.getUTCMonth() &&
+          d1.getUTCDate() === d2.getUTCDate();
+ });
--- a/jest-setup.ts
+++ b/jest-setup.ts
+// Force Jest to use UTC timezone for all date operations
+process.env.TZ = 'UTC';

 import '@testing-library/react-native/extend-expect';
 // …rest of your mocks…

Comment on lines +31 to +35
**Security Features:**
- Disabled JavaScript execution
- Restricted to local content only (`about:` origins)
- Proper content sanitization through HTML generation utility

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Documentation claims sanitization that the utility does not implement

The doc states "Proper content sanitization through HTML generation utility," but generateWebViewHtml currently interpolates content without sanitizing. Update the doc or implement sanitization (preferred).

-**Security Features:**
-- Disabled JavaScript execution
-- Restricted to local content only (`about:` origins)
-- Proper content sanitization through HTML generation utility
+**Security Features:**
+- Disabled JavaScript execution
+- Restricted to local content only (`about:` origins)
+- Content must be sanitized before passing to the WebView HTML generator (see Security notes)

Add a Security Notes section with an example:

+## Security Notes
+
+- Sanitize HTML before calling `generateWebViewHtml`. We recommend using an allowlist-based sanitizer (e.g., `sanitize-html`) to strip scripts, iframes, inline event handlers, and disallowed protocols.
+- Even with JS disabled, external resources (images/links) can load; intercept navigations using `onShouldStartLoadWithRequest` to open external URLs in the system browser and prevent in-WebView navigation.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Security Features:**
- Disabled JavaScript execution
- Restricted to local content only (`about:` origins)
- Proper content sanitization through HTML generation utility
**Security Features:**
- Disabled JavaScript execution
- Restricted to local content only (`about:` origins)
- Content must be sanitized before passing to the WebView HTML generator (see Security notes)
## Security Notes
- Sanitize HTML before calling `generateWebViewHtml`. We recommend using an allowlist-based sanitizer (e.g., `sanitize-html`) to strip scripts, iframes, inline event handlers, and disallowed protocols.
- Even with JS disabled, external resources (images/links) can load; intercept navigations using `onShouldStartLoadWithRequest` to open external URLs in the system browser and prevent in-WebView navigation.
🧰 Tools
🪛 LanguageTool

[grammar] ~31-~31: There might be a mistake here.
Context: ...e for card preview Security Features: - Disabled JavaScript execution - Restrict...

(QB_NEW_EN)


[grammar] ~32-~32: There might be a mistake here.
Context: ...tures:** - Disabled JavaScript execution - Restricted to local content only (`about...

(QB_NEW_EN)


[grammar] ~33-~33: There might be a mistake here.
Context: ...to local content only (about: origins) - Proper content sanitization through HTML...

(QB_NEW_EN)

Comment on lines +55 to +63
useEffect(() => {
if (isOpen && personnel.length === 0 && !isPersonnelLoading) {
setIsInitializing(true);
fetchPersonnel().finally(() => {
setIsInitializing(false);
});
}
}, [isOpen, personnel.length, isPersonnelLoading, fetchPersonnel]);
```
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden the effect against unmounts and errors

The doc snippet sets isInitializing in finally, but does not guard against setState after unmount or handle fetch errors. Add a mounted flag and catch to avoid noisy warnings and allow telemetry/logging.

-useEffect(() => {
-  if (isOpen && personnel.length === 0 && !isPersonnelLoading) {
-    setIsInitializing(true);
-    fetchPersonnel().finally(() => {
-      setIsInitializing(false);
-    });
-  }
-}, [isOpen, personnel.length, isPersonnelLoading, fetchPersonnel]);
+useEffect(() => {
+  let mounted = true;
+  const needsFetch = isOpen && personnel.length === 0 && !isPersonnelLoading;
+  if (needsFetch) {
+    setIsInitializing(true);
+    fetchPersonnel()
+      .catch((err) => {
+        // optional: track/log error
+        console.warn('Failed to fetch personnel', err);
+      })
+      .finally(() => {
+        if (mounted) setIsInitializing(false);
+      });
+  }
+  return () => {
+    mounted = false;
+  };
+}, [isOpen, personnel.length, isPersonnelLoading, fetchPersonnel]);

If you want, I can update the doc with this safer snippet and PR it separately.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (isOpen && personnel.length === 0 && !isPersonnelLoading) {
setIsInitializing(true);
fetchPersonnel().finally(() => {
setIsInitializing(false);
});
}
}, [isOpen, personnel.length, isPersonnelLoading, fetchPersonnel]);
```
useEffect(() => {
let mounted = true;
const needsFetch = isOpen && personnel.length === 0 && !isPersonnelLoading;
if (needsFetch) {
setIsInitializing(true);
fetchPersonnel()
.catch((err) => {
// optional: track/log error
console.warn('Failed to fetch personnel', err);
})
.finally(() => {
if (mounted) setIsInitializing(false);
});
}
return () => {
mounted = false;
};
}, [isOpen, personnel.length, isPersonnelLoading, fetchPersonnel]);
🤖 Prompt for AI Agents
In docs/calendar-item-details-personnel-loading-enhancement.md around lines 55
to 63, the useEffect sets isInitializing in a finally block without guarding
against component unmount or handling fetch errors; add a mounted flag (let
mounted = true; set to false in cleanup) and only call state setters if still
mounted, change the fetch call to include a .catch(...) to handle/log/telemetry
the error, and move setIsInitializing(false) into the finally while still
checking mounted before setting state so no setState-after-unmount warnings
occur and errors get reported.

Comment on lines +36 to +46
```typescript
<WebView
style={[styles.container, { height: 120 }]}
originWhitelist={['*']}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
source={{ html: dynamicHTMLContent }}
androidLayerType="software"
testID="webview"
/>
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tighten the WebView example: avoid originWhitelist="*" and align with defaultWebViewProps

The snippet uses originWhitelist=['*'] and omits other hardening flags. In code, defaultWebViewProps sets safer defaults (originWhitelist: ['about:'], javaScriptEnabled: false, domStorageEnabled: false). The doc should reflect that to prevent unintended navigation or script/storage execution.

Apply this doc diff to mirror production defaults and discourage permissive settings:

-<WebView
-  style={[styles.container, { height: 120 }]}
-  originWhitelist={['*']}
-  scrollEnabled={false}
-  showsVerticalScrollIndicator={false}
-  source={{ html: dynamicHTMLContent }}
-  androidLayerType="software"
-  testID="webview"
-/>
+<WebView
+  style={[styles.container, { height: 120 }]}
+  {...defaultWebViewProps}
+  scrollEnabled={false}
+  showsVerticalScrollIndicator={false}
+  source={{ html: dynamicHTMLContent }}
+  testID="webview"
+/>

Optional: explicitly show an onShouldStartLoadWithRequest example that blocks any navigation not starting with about: to reinforce intent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```typescript
<WebView
style={[styles.container, { height: 120 }]}
originWhitelist={['*']}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
source={{ html: dynamicHTMLContent }}
androidLayerType="software"
testID="webview"
/>
```
<WebView
style={[styles.container, { height: 120 }]}
{...defaultWebViewProps}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
source={{ html: dynamicHTMLContent }}
testID="webview"
/>
🤖 Prompt for AI Agents
In docs/calendar-item-details-webview-implementation.md around lines 36 to 46,
tighten the WebView example to mirror production defaults: replace
originWhitelist=['*'] with originWhitelist={['about:']}, explicitly set
javaScriptEnabled={false} and domStorageEnabled={false}, and add or mention the
defaultWebViewProps pattern; optionally include a short
onShouldStartLoadWithRequest handler that only allows requests starting with
"about:" to block external navigation.

Comment on lines +40 to +48
#### Validation Translation Keys
Added new translation keys for form validation:
```json
"validation": {
"subject_required": "Subject is required",
"body_required": "Message body is required",
"recipients_required": "At least one recipient is required"
}
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix translation key path to match implementation

In code, validation messages live under messages.validation.*. This snippet shows a top-level validation key, which is inconsistent and may mislead.

-```json
-"validation": {
-  "subject_required": "Subject is required",
-  "body_required": "Message body is required", 
-  "recipients_required": "At least one recipient is required"
-}
-```
+```json
+{
+  "messages": {
+    "validation": {
+      "subject_required": "Subject is required",
+      "body_required": "Message body is required",
+      "recipients_required": "At least one recipient is required"
+    }
+  }
+}
+```
🧰 Tools
🪛 LanguageTool

[grammar] ~40-~40: There might be a mistake here.
Context: ... issue #### Validation Translation Keys Added new translation keys for form vali...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/compose-message-sheet-improvements.md around lines 40 to 48, the example
JSON places validation keys at the top level but the code expects them under
messages.validation; update the JSON structure to nest the validation keys
inside a messages object (messages.validation.*) so the translation path matches
implementation, i.e., move subject_required, body_required and
recipients_required under messages -> validation.

Comment on lines +99 to +109
// Use ISO date format for better timezone handling
//const startDate = format(startOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");
//const endDate = format(endOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");

logger.info({
message: "Loading today's calendar items",
context: { todayISO: today.toISOString() },
});

const response = await getCalendarItemsForDateRange(today.toISOString(), today.toISOString());

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: start and end set to the same instant (ISO) will likely return no items

Passing today.toISOString() for both start and end collapses the range to a single timestamp. The backend range API probably expects a day span. Use startOfDay/endOfDay with a consistent format (matching upcoming) and log both for traceability.

Apply this diff:

-      // Use ISO date format for better timezone handling
-      //const startDate = format(startOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");
-      //const endDate = format(endOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");
-
-      logger.info({
-        message: "Loading today's calendar items",
-        context: { todayISO: today.toISOString() },
-      });
-
-      const response = await getCalendarItemsForDateRange(today.toISOString(), today.toISOString());
+      // Use explicit local day range to avoid TZ drift on the backend
+      const startDate = format(startOfDay(today), 'yyyy-MM-dd HH:mm:ss');
+      const endDate = format(endOfDay(today), 'yyyy-MM-dd HH:mm:ss');
+
+      logger.info({
+        message: "Loading today's calendar items",
+        context: { startDate, endDate },
+      });
+
+      const response = await getCalendarItemsForDateRange(startDate, endDate);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use ISO date format for better timezone handling
//const startDate = format(startOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");
//const endDate = format(endOfDay(today), "yyyy-MM-dd'T'HH:mm:ss");
logger.info({
message: "Loading today's calendar items",
context: { todayISO: today.toISOString() },
});
const response = await getCalendarItemsForDateRange(today.toISOString(), today.toISOString());
// Use explicit local day range to avoid TZ drift on the backend
const startDate = format(startOfDay(today), 'yyyy-MM-dd HH:mm:ss');
const endDate = format(endOfDay(today), 'yyyy-MM-dd HH:mm:ss');
logger.info({
message: "Loading today's calendar items",
context: { startDate, endDate },
});
const response = await getCalendarItemsForDateRange(startDate, endDate);
🤖 Prompt for AI Agents
In src/stores/calendar/store.ts around lines 99 to 109, the code currently
passes today.toISOString() for both start and end which collapses the range to a
single instant; compute a startDate using startOfDay(today) and an endDate using
endOfDay(today), format them consistently (e.g. format(...,
"yyyy-MM-dd'T'HH:mm:ss") to match the existing conventions), log both startDate
and endDate in the logger.info call for traceability, and pass those two
variables to getCalendarItemsForDateRange instead of today.toISOString().

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: 11

Caution

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

⚠️ Outside diff range comments (2)
src/components/messages/compose-message-sheet.tsx (1)

515-539: Units tab is unreachable

currentRecipientTab supports 'units', content renders for 'units', but the tabs only map over ['personnel','groups','roles']. Add 'units' so users can select Unit recipients.

- {['personnel', 'groups', 'roles'].map((tab) => (
+ {['personnel', 'groups', 'roles', 'units'].map((tab) => (
src/components/calendar/enhanced-calendar-view.tsx (1)

51-55: Fix multi-day event marking in EnhancedCalendarView

The calendar is currently using markingType="multi-dot", which only renders dots (and ignores period props like startingDay/endingDay). As a result, multi-day events only display a single dot on the start date rather than on each day of the range.

Please update the logic in src/components/calendar/enhanced-calendar-view.tsx (around lines 50–60) to correctly mark all days of multi-day events. You have two options:

• Option A: Keep markingType="multi-dot" and push the event dot to every date in the range
• Option B: Switch to markingType="multi-period" and build period markings spanning start → end

Below is the minimal refactor for Option A—add this in place of the existing single-dot snippet:

-      // Add a dot for this event (different colors based on event type)
-      marked[startDate].dots.push({
-        key: item.CalendarItemId,
-        color: item.TypeColor || '#3B82F6',
-      });
-
-      // If it's a multi-day event, mark the range
-      if (startDate !== endDate) {
-        const start = new Date(startDate);
-        const end = new Date(endDate);
-        const current = new Date(start);
-
-        while (current <= end) {
-          const dateStr = formatLocalDateString(current);
-          if (!marked[dateStr]) {
-            marked[dateStr] = {
-              marked: true,
-              dots: [],
-            };
-          }
-
-          // Add period marking for multi-day events
-          if (dateStr === startDate) {
-            marked[dateStr].startingDay = true;
-            marked[dateStr].color = item.TypeColor || '#3B82F6';
-          } else if (dateStr === endDate) {
-            marked[dateStr].endingDay = true;
-            marked[dateStr].color = item.TypeColor || '#3B82F6';
-          } else {
-            marked[dateStr].color = item.TypeColor || '#3B82F6';
-          }
-
-          current.setDate(current.getDate() + 1);
-        }
-      }
+      // Add a dot for every day in the event range (for markingType="multi-dot")
+      const start = new Date(startDateObj.getFullYear(), startDateObj.getMonth(), startDateObj.getDate());
+      const end = new Date(endDateObj.getFullYear(), endDateObj.getMonth(), endDateObj.getDate());
+      if (end < start) return; // Defensive: invalid range
+
+      const eventDot = { key: item.CalendarItemId, color: item.TypeColor || '#3B82F6' };
+      for (let day = new Date(start); day <= end; day.setDate(day.getDate() + 1)) {
+        const dateStr = formatLocalDateString(day);
+        if (!marked[dateStr]) {
+          marked[dateStr] = { marked: true, dots: [] };
+        }
+        marked[dateStr].dots = Array.isArray(marked[dateStr].dots) ? marked[dateStr].dots : [];
+        marked[dateStr].dots.push(eventDot);
+      }

If the design instead requires a continuous highlight (a “bar” spanning multiple days), switch the component prop to markingType="multi-period" and replace the dot logic with period markings. Let me know which visual style the product team prefers, and I can provide the corresponding multi-period implementation.

♻️ Duplicate comments (4)
src/components/calendar/__tests__/compact-calendar-item.test.tsx (1)

222-230: Good fix: press the Pressable by testID to avoid event-bubbling flakiness.

This addresses the prior flakiness concern around pressing child Text nodes. Nice.

src/stores/calendar/__tests__/store.test.ts (1)

41-44: Mocks are now correctly hoisted before importing the store.

This resolves the earlier issue where real modules could leak into the store under ESM/ts-jest.

src/components/calendar/calendar-item-details-sheet.tsx (1)

23-23: Do not use the regex-based sanitizer; centralize on sanitize-html utility

The custom sanitizer in html-sanitizer.ts is fragile. We already have a robust sanitizeHtmlContent/generateWebViewHtml in src/utils/webview-html.ts. Use that instead to avoid bypasses and duplication.

Apply:

- import { sanitizeHtml } from '@/utils/html-sanitizer';
+ import { generateWebViewHtml, defaultWebViewProps } from '@/utils/webview-html';
src/components/calendar/enhanced-calendar-view.tsx (1)

226-226: Nice: date comparison now uses TZ-safe helper

Switching to isSameDate() addresses the earlier drift concern and ensures consistency with other calendar filtering.

🧹 Nitpick comments (37)
src/components/calendar/__tests__/compact-calendar-item.test.tsx (6)

14-19: Make icons testable: return elements with testIDs instead of string mocks.

String-based component mocks prevent asserting icon presence. Switch to component mocks that render a View/Text with deterministic testIDs so the tests can verify the CheckCircle (and others) reliably.

Apply this diff to your lucide mock:

-jest.mock('lucide-react-native', () => ({
-  Calendar: 'Calendar',
-  Clock: 'Clock',
-  MapPin: 'MapPin',
-  CheckCircle: 'CheckCircle',
-}));
+jest.mock('lucide-react-native', () => {
+  const React = require('react');
+  const { View } = require('react-native');
+  const Icon = ({ testID }: any) => React.createElement(View, { testID });
+  return {
+    Calendar: (props: any) => React.createElement(Icon, { ...props, testID: 'icon-calendar' }),
+    Clock: (props: any) => React.createElement(Icon, { ...props, testID: 'icon-clock' }),
+    MapPin: (props: any) => React.createElement(Icon, { ...props, testID: 'icon-map-pin' }),
+    CheckCircle: (props: any) => React.createElement(Icon, { ...props, testID: 'icon-check-circle' }),
+  };
+});

265-279: Test name says “shows check circle icon” but the assertion only checks text. Add an explicit icon assertion.

With the improved lucide mock, assert the icon is present to match the test intent.

Apply this diff:

-    // When signed up, should show the "Signed Up" text and signup section
-    expect(getByText('Signed Up')).toBeTruthy();
-    expect(getByText('Sign-up available')).toBeTruthy();
+    // When signed up, should show the "Signed Up" text and the check icon
+    expect(getByText('Signed Up')).toBeTruthy();
+    expect(getByText('Sign-up available')).toBeTruthy();
+    expect(getByTestId('icon-check-circle')).toBeTruthy();

281-295: Also assert the absence of the check icon when not signed up.

This aligns with the component logic that only renders the icon if Attending && canSignUp.

Apply this diff:

-    // When not signed up, should show "Tap to sign up" but not "Signed Up"
+    // When not signed up, should show "Tap to sign up" but not "Signed Up" nor the check icon
     expect(getByText('Tap to sign up')).toBeTruthy();
     expect(queryByText('Signed Up')).toBeNull();
+    expect(queryByTestId('icon-check-circle')).toBeNull();

71-77: Pressable mock: add accessibilityRole='button' for better a11y queries.

This enables future tests to use getByA11yRole('button') and improves semantics with no behavior change.

-    Pressable: ({ children, onPress, testID, ...props }: any) => React.createElement(View, { ...props, onPress, testID: testID || 'pressable' }, children),
+    Pressable: ({ children, onPress, testID, ...props }: any) =>
+      React.createElement(
+        View,
+        { ...props, onPress, accessibilityRole: 'button', testID: testID || 'pressable' },
+        children
+      ),

208-221: Locked-editing case: assert the icon is hidden too.

When LockEditing is true, canSignUp is false; the check icon should not render even if Attending. Add a negative assertion for the icon for completeness.

Example:

-    expect(queryByText('Sign-up available')).toBeNull();
+    expect(queryByText('Sign-up available')).toBeNull();
+    // With refactored icon mock:
+    expect(queryByTestId('icon-check-circle')).toBeNull();

297-310: Date regex is tolerant enough; consider pinning timezone if CI ever flakes.

The locale-agnostic regex is fine. If CI environments with different TZs cause flakiness later, pin TZ via Jest config (e.g., env.TZ='UTC') or stub Date/Intl.

src/stores/calendar/__tests__/store.test.ts (5)

45-45: Remove unused type import.

CalendarItemResultData is imported but not used. Drop it to avoid linter noise.

-import { CalendarItemResultData } from '@/models/v4/calendar/calendarItemResultData';

127-135: Stabilize isSameDate mock with UTC fields to avoid locale/TZ flakiness.

getFullYear/getMonth/getDate depend on host timezone. Using UTC variants makes the comparison deterministic across CI environments.

-  mockedUtils.isSameDate.mockImplementation((date1: string | Date, date2: string | Date) => {
-    const d1 = new Date(date1);
-    const d2 = new Date(date2);
-    return d1.getFullYear() === d2.getFullYear() && 
-           d1.getMonth() === d2.getMonth() && 
-           d1.getDate() === d2.getDate();
-  });
+  mockedUtils.isSameDate.mockImplementation((date1: string | Date, date2: string | Date) => {
+    const d1 = new Date(date1);
+    const d2 = new Date(date2);
+    return (
+      d1.getUTCFullYear() === d2.getUTCFullYear() &&
+      d1.getUTCMonth() === d2.getUTCMonth() &&
+      d1.getUTCDate() === d2.getUTCDate()
+    );
+  });

381-409: Assert updateCalendarItems flag transitions during attendance updates.

Store sets updateCalendarItems true while updating and resets it to false after. Add expectations to prevent regressions.

   await act(async () => {
     await result.current.setCalendarItemAttendingStatus('123', 'Test note', 1);
   });
 
   expect(result.current.isAttendanceLoading).toBe(false);
   expect(result.current.attendanceError).toBeNull();
+  expect(result.current.updateCalendarItems).toBe(false);

You can also briefly assert it flips to true by spying state before await if you expose set/get or by splitting the act with a microtask tick.


425-443: Add coverage for loadCalendarItems (the -90/+120 day fetch) and dismissModal.

Two public behaviors remain untested: bulk load across the extended window and modal dismissal. Suggested additions below.

Append these tests near similar sections:

+  describe('loadCalendarItems', () => {
+    beforeEach(() => {
+      jest.useFakeTimers();
+      jest.setSystemTime(new Date('2024-01-15T10:00:00Z'));
+    });
+    afterEach(() => {
+      jest.useRealTimers();
+    });
+    it('should fetch items across -90/+120 day window', async () => {
+      mockedDateFns.subDays.mockReturnValue(new Date('2023-10-17T10:00:00Z'));
+      mockedDateFns.addDays.mockReturnValue(new Date('2024-05-14T10:00:00Z'));
+      const mockResponse = { Data: [mockCalendarItem], ...createMockBaseResponse() };
+      mockedApi.getCalendarItemsForDateRange.mockResolvedValue(mockResponse);
+
+      const { result } = renderHook(() => useCalendarStore());
+      await act(async () => {
+        await result.current.loadCalendarItems();
+      });
+
+      expect(mockedApi.getCalendarItemsForDateRange).toHaveBeenCalledWith(
+        '2023-10-17',
+        '2024-05-14'
+      );
+      expect(result.current.calendarItems).toEqual([mockCalendarItem]);
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.error).toBeNull();
+    });
+  });
+
+  describe('dismissModal', () => {
+    it('should clear viewCalendarItem', () => {
+      useCalendarStore.setState({ viewCalendarItem: mockCalendarItem });
+      const { result } = renderHook(() => useCalendarStore());
+      act(() => {
+        result.current.dismissModal();
+      });
+      expect(result.current.viewCalendarItem).toBeNull();
+    });
+  });

If you want, I can push a follow-up commit with these tests.


498-541: Init test looks good; consider asserting both API calls count.

Optional: verify getCalendarItemsForDateRange was called twice (today + upcoming) to guard against accidental removal of one branch.

Example:

expect(mockedApi.getCalendarItemsForDateRange).toHaveBeenCalledTimes(2);
src/utils/html-sanitizer.ts (2)

48-53: Use the same sanitizer to derive safe plain text (handles malformed HTML better).

Regex tag stripping can miss edge cases and leave entities. Using sanitize-html with an empty allowlist is more robust.

Apply:

 export function htmlToPlainText(html: string): string {
-  if (!html) return '';
-
-  // Remove all HTML tags
-  return html.replace(/<[^>]*>/g, '').trim();
+  if (!html) return '';
+  // Remove all tags/attributes; keep only text content
+  return sanitizeLib(html, { allowedTags: [], allowedAttributes: {} }).trim();
 }

12-41: Add targeted tests for tricky cases that regex missed.

Once using sanitize-html, please add tests for:

  • </script > and mixed-case tags
  • iframe srcdoc attribute
  • img srcset with data: and javascript: URLs
  • SVG tags (ensure they’re stripped)

I can draft these tests after you confirm adopting the library-based sanitizer.

src/utils/__tests__/html-sanitizer.test.ts (3)

44-49: Future-proof the link test: assert safety, not exact markup.

When switching to sanitize-html, the default behavior is to drop unsafe href entirely rather than convert to href="". Prefer intent-based assertions.

Apply:

-      expect(result).not.toContain('javascript:');
-      expect(result).toContain('<a href="">Click me</a>');
+      expect(result).not.toMatch(/javascript:/i);
+      // Anchor should remain present
+      expect(result).toMatch(/<a[\s>]/i);
+      // And no href should carry a dangerous scheme
+      expect(result).not.toMatch(/href\s*=\s*["']\s*(javascript:|data:)/i);

5-26: Add a case for script end tags with whitespace/attributes.

This catches a classic bypass that regex-based filters often miss and documents expected behavior under sanitize-html.

Suggested addition:

@@
   describe('sanitizeHtml', () => {
+    it('should remove script tags even with attributes/whitespace in end tag', () => {
+      const input = '<script type="text/javascript">alert(1)</script   ><p>ok</p>';
+      const result = sanitizeHtml(input);
+      expect(result).not.toMatch(/<script/i);
+      expect(result).toContain('<p>ok</p>');
+    });

83-101: Consider a positive safety check using the sanitizer config.

Once sanitize-html backs the utility, a strong invariant is that unsafe input ≠ sanitized output. Add a test asserting isHtmlSafe returns false when sanitizer mutates input (e.g., dropping an unsafe href).

Example:

@@
   describe('isHtmlSafe', () => {
     it('should return false for dangerous content', () => {
@@
     });
+
+    it('should return false when sanitizer strips unsafe attributes', () => {
+      const input = '<a href="javascript:alert(1)">Click</a>';
+      expect(isHtmlSafe(input)).toBe(false);
+    });
docs/webview-html-security-implementation.md (2)

79-89: Standardize “about:blank” vs “about:” across code and docs

Elsewhere (tests and other docs) you use about:blank for the base URL and whitelisting. Keep it consistent here and in the implementation to avoid subtle allow/deny mismatches in navigation checks.

Apply this minimal doc tweak:

-  return `<body>${sanitizedContent}</body>`;
+  // Consider using about:blank consistently across docs & code for base/whitelist
+  return `<body>${sanitizedContent}</body>`;

Action item: confirm defaultWebViewProps and all navigation checks use 'about:blank' consistently.


62-66: Clarify URL and CSS policies to match real-world needs

Current text blocks data: URIs for images and allows only inline CSS properties. If your CSP (in the other doc) uses default-src 'none', external images over https will be blocked anyway. Either:

  • Allow img-src https: data: in CSP and keep img allowed here, or
  • Remove img from allowed tags if images should never load.

Spell this out to avoid confusion between sanitizer allowlist and CSP behavior.

Also applies to: 67-72

src/translations/es.json (1)

62-66: Minor Spanish grammar fix for confirmation message

Prefer “¿Estás seguro de que quieres…?” in es-ES/es-419. Optional polish:

-      "message": "¿Estás seguro que quieres cancelar tu asistencia a este evento?",
+      "message": "¿Estás seguro de que quieres cancelar tu asistencia a este evento?",
docs/webview-security-implementation.md (2)

46-49: Use “about:blank” consistently for whitelisting

Align this with all call sites and shared defaults to avoid edge-case mismatches.


68-75: CSP example likely breaks images; propose a safer policy

With default-src 'none' you block images/fonts. If images are allowed by the sanitizer, update CSP accordingly.

Suggested CSP:

<meta http-equiv="Content-Security-Policy"
  content="
    default-src 'none';
    img-src https: data:;
    style-src 'unsafe-inline';
    base-uri 'none';
    form-action 'none';
  ">
src/components/contacts/contact-notes-list.tsx (4)

34-37: Remove unused theme variables

textColor and backgroundColor aren’t used. Delete to reduce noise.

-  const { colorScheme } = useColorScheme();
-  const textColor = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
-  const backgroundColor = colorScheme === 'dark' ? '#374151' : '#F9FAFB';
+  const { colorScheme } = useColorScheme();

80-107: Avoid inline handler; memoize navigation guard with useCallback

Inline functions in JSX create a new closure per render and violate the repo’s TSX guidelines. Lift it into a memoized callback.

+  const handleShouldStartLoadWithRequest = React.useCallback((request: { url: string }) => {
+    const isLocal = request.url.startsWith('about:') || request.url.startsWith('data:');
+    if (isLocal) return true;
+    const allowed = ['http://', 'https://', 'mailto:', 'tel:'];
+    if (allowed.some((s) => request.url.startsWith(s))) {
+      Linking.openURL(request.url).catch(() => {});
+    }
+    return false;
+  }, []);
...
-            <WebView
+            <WebView
               {...defaultWebViewProps}
               style={styles.webView}
               source={{
                 html: generateWebViewHtml({
                   content: noteContent,
                   isDarkMode: colorScheme === 'dark',
                 }),
               }}
-              onShouldStartLoadWithRequest={(request) => {
-                const isLocal = request.url.startsWith('about:') || request.url.startsWith('data:');
-                if (isLocal) return true;
-                const allowed = ['http://', 'https://', 'mailto:', 'tel:'];
-                if (allowed.some((s) => request.url.startsWith(s))) {
-                  Linking.openURL(request.url).catch(() => {});
-                }
-                return false;
-              }}
+              onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
             />

208-218: Drop unused style entry

styles.container isn’t referenced.

 const styles = StyleSheet.create({
-  container: {
-    width: '100%',
-    backgroundColor: 'transparent',
-  },
   webView: {
     height: 200, // Fixed height with scroll capability
     backgroundColor: 'transparent',
     width: '100%',
   },
 });

88-95: Ensure shared defaults disable file access by default

The security tests expect allowFileAccess=false and allowUniversalAccessFromFileURLs=false. Confirm defaultWebViewProps includes these and uses originWhitelist={['about:blank']} for consistency.

If missing, update src/utils/webview-html.ts:

 export const defaultWebViewProps = {
-  originWhitelist: ['about:'],
+  originWhitelist: ['about:blank'],
   javaScriptEnabled: false,
   domStorageEnabled: false,
+  allowFileAccess: false,
+  allowUniversalAccessFromFileURLs: false,
   startInLoadingState: false,
   mixedContentMode: 'compatibility' as const,
   androidLayerType: 'software' as const,
   showsVerticalScrollIndicator: true,
   showsHorizontalScrollIndicator: false,
 };
src/utils/__tests__/webview-html.test.ts (2)

41-47: Broaden link protocol coverage in tests

Consider adding positive tests that explicitly allow safe link schemes (mailto, tel) and verify that javascript: is stripped while http/https/mailto/tel remain intact. This guards against regressions in the sanitizer’s allowedSchemes.


162-169: Avoid locking zoom (user-scalable=no) for accessibility

Requiring user-scalable=no can hinder accessibility/zoom. Recommend relaxing this expectation and updating the generator to omit user-scalable=no. Example test change:

- expect(html).toContain('<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">');
+ expect(html).toContain('<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0');
src/components/calendar/calendar-item-details-sheet.tsx (2)

297-302: onNavigationStateChange return value is ignored

RN WebView’s onNavigationStateChange is a notification-only callback; returning false does nothing. Rely on onShouldStartLoadWithRequest for blocking, and make this handler void or remove it.


100-114: Timezone-safe formatting

formatDateTime uses device locale/timezone directly on UTC strings, which may be desired or may drift from server expectations. If the app introduced “local-time utilities,” consider reusing them here to ensure consistency with other calendar views and DST edge cases.

Would you like me to wire this to the new timezone-safe helpers if they exist in src/utils?

src/utils/webview-html.ts (3)

61-69: Allow safe link attributes and add rel/target hardening

Permit rel and target, and set secure defaults so external links cannot reference the opener.

-    allowedAttributes: {
-      a: ['href', 'title'],
+    allowedAttributes: {
+      a: ['href', 'title', 'rel', 'target'],

93-109: Harden anchor tags via transformTags

Add noopener/noreferrer and open in a new context. This is inert with navigation blocked but future-proofs content.

     transformTags: {
+      a: sanitizeHtml.simpleTransform('a', { rel: 'noopener noreferrer', target: '_blank' }),
       '*': (tagName, attribs) => {
         const cleanAttribs: { [key: string]: string } = {};
         Object.keys(attribs).forEach((key) => {
           // Remove event handler attributes
           if (!key.toLowerCase().startsWith('on')) {
             cleanAttribs[key] = attribs[key];
           }
         });
         return {
           tagName,
           attribs: cleanAttribs,
         };
       },
     },

134-135: Consider removing user-scalable=no from viewport

To support accessibility zoom, prefer not to disable user scaling.

-        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

If you accept this, also relax the corresponding test expectation in src/utils/tests/webview-html.test.ts.

src/components/messages/compose-message-sheet.tsx (2)

176-239: Close sheet on successful send?

After sendNewMessage and analytics, the sheet remains open. Confirm desired UX. Many flows close the composer upon success and optionally toast. If you choose to close, call closeCompose() after resetForm().


313-342: Tighten types for recipients list rendering

renderRecipientList(recipients: any[], type: string) weakens type-safety. Consider a RecipientItem interface with Id, Name, Type to align with dispatchData.* shapes and catch regressions at compile time.

src/components/calendar/enhanced-calendar-view.tsx (3)

12-12: Good move adopting timezone-safe utils

Importing formatLocalDateString, getTodayLocalString, and isSameDate aligns with the new TZ-safe approach and the coding guidelines. One small follow-up: use formatLocalDateString consistently below to avoid manual YYYY-MM-DD assembly.


32-41: Replace any with a precise type for marked dates

The guidelines ask to avoid any. CalendarProps exposes the right shape; use it (or define a narrow local type).

Apply one of these:

-    const marked: Record<string, any> = {};
+    const marked: NonNullable<CalendarProps['markedDates']> = {};

Or define a local type if you want to decouple from lib types:

type MultiDotMarking = {
  marked?: boolean;
  dots?: Array<{ key: string | number; color: string }>;
  selected?: boolean;
  selectedColor?: string;
};
const marked: Record<string, MultiDotMarking> = {};

206-236: Prefer ?: over && and avoid IIFEs in render; memoize derived values

  • Coding guidelines prefer ?: for conditional rendering instead of &&.
  • Two IIFEs compute strings and filters on every render. Move them to useMemo to reduce work and avoid re-creation.

Apply this diff to the conditional:

-      {selectedDate && (
+      {selectedDate ? (
         <View className="border-t border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
           <Text className="text-sm font-medium text-gray-900 dark:text-white">
             {t('calendar.selectedDate.title', {
-              date: (() => {
-                // Parse the date string properly to avoid timezone issues
-                const [year, month, day] = selectedDate.split('-').map(Number);
-                const localDate = new Date(year, month - 1, day); // month is 0-indexed
-                return localDate.toLocaleDateString([], {
-                  weekday: 'long',
-                  year: 'numeric',
-                  month: 'long',
-                  day: 'numeric',
-                });
-              })(),
+              date: selectedDateLabel,
             })}
           </Text>
-          {(() => {
-            const eventsForDay = selectedMonthItems.filter((item) => {
-              // Use isSameDate for timezone-safe date comparison with .NET backend timezone-aware dates
-              return selectedDate ? isSameDate(item.Start, selectedDate) : false;
-            });
-
-            if (eventsForDay.length > 0) {
-              return <Text className="mt-1 text-xs text-gray-600 dark:text-gray-300">{t('calendar.eventsCount', { count: eventsForDay.length })}</Text>;
-            }
-
-            return <Text className="mt-1 text-xs text-gray-500 dark:text-gray-400">{t('calendar.noEvents')}</Text>;
-          })()}
+          {eventsForSelectedDay.length > 0
+            ? <Text className="mt-1 text-xs text-gray-600 dark:text-gray-300">{t('calendar.eventsCount', { count: eventsForSelectedDay.length })}</Text>
+            : <Text className="mt-1 text-xs text-gray-500 dark:text-gray-400">{t('calendar.noEvents')}</Text>}
         </View>
-      )}
+      ) : null}

Add these memoized helpers near other hooks:

// Memoized label for the selected date
const selectedDateLabel = useMemo(() => {
  if (!selectedDate) return '';
  const [year, month, day] = selectedDate.split('-').map(Number);
  return new Date(year, month - 1, day).toLocaleDateString([], {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
}, [selectedDate]);

// Memoized events for the selected day
const eventsForSelectedDay = useMemo(
  () => selectedDate ? selectedMonthItems.filter((item) => isSameDate(item.Start, selectedDate)) : [],
  [selectedMonthItems, selectedDate]
);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3f4669f and 943bab1.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (22)
  • docs/webview-html-security-implementation.md (1 hunks)
  • docs/webview-security-implementation.md (1 hunks)
  • jest-setup.ts (1 hunks)
  • package.json (2 hunks)
  • src/app/_layout.tsx (1 hunks)
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1 hunks)
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx (10 hunks)
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx (1 hunks)
  • src/components/calendar/calendar-item-details-sheet.tsx (9 hunks)
  • src/components/calendar/compact-calendar-item.tsx (1 hunks)
  • src/components/calendar/enhanced-calendar-view.tsx (7 hunks)
  • src/components/contacts/contact-notes-list.tsx (7 hunks)
  • src/components/messages/__tests__/compose-message-sheet.test.tsx (2 hunks)
  • src/components/messages/compose-message-sheet.tsx (6 hunks)
  • src/stores/calendar/__tests__/store.test.ts (17 hunks)
  • src/translations/ar.json (3 hunks)
  • src/translations/en.json (3 hunks)
  • src/translations/es.json (3 hunks)
  • src/utils/__tests__/html-sanitizer.test.ts (1 hunks)
  • src/utils/__tests__/webview-html.test.ts (1 hunks)
  • src/utils/html-sanitizer.ts (1 hunks)
  • src/utils/webview-html.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/translations/en.json
  • src/components/calendar/compact-calendar-item.tsx
  • jest-setup.ts
  • src/components/messages/tests/compose-message-sheet.test.tsx
🧰 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/_layout.tsx
  • src/utils/__tests__/html-sanitizer.test.ts
  • src/components/contacts/contact-notes-list.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/utils/html-sanitizer.ts
  • src/components/calendar/calendar-item-details-sheet.tsx
  • src/utils/__tests__/webview-html.test.ts
  • src/components/calendar/enhanced-calendar-view.tsx
  • src/components/messages/compose-message-sheet.tsx
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
  • src/utils/webview-html.ts
  • src/stores/calendar/__tests__/store.test.ts
**/*.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/_layout.tsx
  • src/components/contacts/contact-notes-list.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/components/calendar/calendar-item-details-sheet.tsx
  • src/components/calendar/enhanced-calendar-view.tsx
  • src/components/messages/compose-message-sheet.tsx
  • src/components/calendar/__tests__/compact-calendar-item.test.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/_layout.tsx
  • src/utils/__tests__/html-sanitizer.test.ts
  • src/components/contacts/contact-notes-list.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/utils/html-sanitizer.ts
  • src/components/calendar/calendar-item-details-sheet.tsx
  • src/utils/__tests__/webview-html.test.ts
  • src/components/calendar/enhanced-calendar-view.tsx
  • src/components/messages/compose-message-sheet.tsx
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
  • src/translations/es.json
  • src/translations/ar.json
  • src/utils/webview-html.ts
  • src/stores/calendar/__tests__/store.test.ts
**/*.{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/utils/__tests__/html-sanitizer.test.ts
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/utils/__tests__/webview-html.test.ts
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
  • src/stores/calendar/__tests__/store.test.ts
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/utils/__tests__/html-sanitizer.test.ts
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/utils/__tests__/webview-html.test.ts
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
  • src/stores/calendar/__tests__/store.test.ts
src/translations/**/*.json

📄 CodeRabbit inference engine (.cursorrules)

Store translation dictionary files under src/translations as JSON resources

Files:

  • 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/es.json
  • src/translations/ar.json
🧠 Learnings (5)
📚 Learning: 2025-08-21T02:46:42.705Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.705Z
Learning: Applies to **/*.tsx : Use lucide-react-native for icons directly; do not use the gluestack-ui icon component

Applied to files:

  • src/components/contacts/contact-notes-list.tsx
  • src/components/calendar/calendar-item-details-sheet.tsx
📚 Learning: 2025-08-21T02:47:24.070Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.070Z
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/contacts/contact-notes-list.tsx
  • src/components/calendar/calendar-item-details-sheet.tsx
📚 Learning: 2025-08-21T02:47:24.070Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.070Z
Learning: Applies to src/**/*.test.{ts,tsx} : Create and use Jest tests to validate all generated components

Applied to files:

  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx
  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
📚 Learning: 2025-08-21T02:47:24.070Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.070Z
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/calendar/calendar-item-details-sheet.tsx
📚 Learning: 2025-08-21T02:46:42.705Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.705Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Create Jest tests for all generated components, services, and logic

Applied to files:

  • src/components/calendar/__tests__/compact-calendar-item.test.tsx
🧬 Code graph analysis (10)
src/utils/__tests__/html-sanitizer.test.ts (1)
src/utils/html-sanitizer.ts (3)
  • sanitizeHtml (12-41)
  • htmlToPlainText (48-53)
  • isHtmlSafe (60-76)
src/components/contacts/contact-notes-list.tsx (1)
src/utils/webview-html.ts (2)
  • defaultWebViewProps (224-237)
  • generateWebViewHtml (117-219)
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1)
src/components/calendar/calendar-item-details-sheet.tsx (1)
  • CalendarItemDetailsSheet (31-420)
src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx (3)
src/stores/calendar/store.ts (1)
  • useCalendarStore (61-337)
src/stores/personnel/store.ts (1)
  • usePersonnelStore (37-121)
src/components/calendar/calendar-item-details-sheet.tsx (1)
  • CalendarItemDetailsSheet (31-420)
src/components/calendar/calendar-item-details-sheet.tsx (3)
src/stores/calendar/store.ts (1)
  • useCalendarStore (61-337)
src/stores/personnel/store.ts (1)
  • usePersonnelStore (37-121)
src/utils/html-sanitizer.ts (1)
  • sanitizeHtml (12-41)
src/utils/__tests__/webview-html.test.ts (1)
src/utils/webview-html.ts (3)
  • sanitizeHtmlContent (18-110)
  • generateWebViewHtml (117-219)
  • defaultWebViewProps (224-237)
src/components/calendar/enhanced-calendar-view.tsx (1)
src/lib/utils.ts (3)
  • formatLocalDateString (243-248)
  • getTodayLocalString (254-256)
  • isSameDate (258-281)
src/components/calendar/__tests__/compact-calendar-item.test.tsx (1)
src/components/calendar/compact-calendar-item.tsx (1)
  • CompactCalendarItem (20-121)
src/utils/webview-html.ts (1)
src/utils/html-sanitizer.ts (1)
  • sanitizeHtml (12-41)
src/stores/calendar/__tests__/store.test.ts (1)
src/stores/calendar/store.ts (1)
  • useCalendarStore (61-337)
🪛 LanguageTool
docs/webview-security-implementation.md

[grammar] ~24-~24: There might be a mistake here.
Context: ... JavaScript execution within the WebView - Prevents script-based XSS attacks - Elim...

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ...View - Prevents script-based XSS attacks - Eliminates DOM manipulation through inje...

(QB_NEW_EN)


[grammar] ~32-~32: There might be a mistake here.
Context: ...s localStorage and sessionStorage access - Blocks potential data persistence attack...

(QB_NEW_EN)


[grammar] ~33-~33: There might be a mistake here.
Context: ...locks potential data persistence attacks - Eliminates cross-session data leakage #...

(QB_NEW_EN)


[grammar] ~42-~42: There might be a mistake here.
Context: ...file system - Blocks file:// URL schemes - Eliminates potential local file inclusio...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...``` - Restricts allowed origins to only about:blank - Prevents navigation to external domains ...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ... Prevents navigation to external domains - Blocks potential redirect attacks #### ...

(QB_NEW_EN)


[grammar] ~93-~93: There might be a mistake here.
Context: ... dangerous HTML content: #### Features: - Script Tag Removal: Removes <script>...

(QB_NEW_EN)


[grammar] ~112-~112: There might be a mistake here.
Context: ...ips all HTML tags for plain text display - isHtmlSafe(): Checks if HTML content contains danger...

(QB_NEW_EN)


[grammar] ~113-~113: There might be a mistake here.
Context: ...HTML content contains dangerous patterns - sanitizeEventForLogging(): Sanitizes events for logging purposes ...

(QB_NEW_EN)


[grammar] ~120-~120: There might be a mistake here.
Context: ....test.tsx`) Comprehensive tests ensure: - WebView security properties are correctl...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ..._/html-sanitizer.test.ts`) Tests cover: - Removal of dangerous HTML elements - Eve...

(QB_NEW_EN)


[grammar] ~129-~129: There might be a mistake here.
Context: ...er: - Removal of dangerous HTML elements - Event handler stripping - JavaScript URL...

(QB_NEW_EN)


[grammar] ~130-~130: There might be a mistake here.
Context: ... HTML elements - Event handler stripping - JavaScript URL blocking - CSS expression...

(QB_NEW_EN)


[grammar] ~131-~131: There might be a mistake here.
Context: ...dler stripping - JavaScript URL blocking - CSS expression prevention - Edge case ha...

(QB_NEW_EN)


[grammar] ~132-~132: There might be a mistake here.
Context: ...URL blocking - CSS expression prevention - Edge case handling ## Best Practices Im...

(QB_NEW_EN)


[grammar] ~137-~137: There might be a mistake here.
Context: ...ty (sanitization + WebView restrictions) 2. Least Privilege: Only minimum necessar...

(QB_NEW_EN)


[grammar] ~138-~138: There might be a mistake here.
Context: ...ly minimum necessary permissions granted 3. Input Validation: All HTML content san...

(QB_NEW_EN)


[grammar] ~139-~139: There might be a mistake here.
Context: ... HTML content sanitized before rendering 4. Content Security Policy: Strict CSP he...

(QB_NEW_EN)


[grammar] ~140-~140: There might be a mistake here.
Context: ...ict CSP headers prevent resource loading 5. Navigation Control: Explicit allow-lis...

(QB_NEW_EN)


[grammar] ~141-~141: There might be a mistake here.
Context: ...ol**: Explicit allow-list for navigation 6. Error Handling: Graceful degradation f...

(QB_NEW_EN)


[grammar] ~158-~158: There might be a mistake here.
Context: ...hen creating calendar item descriptions: - Avoid inline JavaScript or event handler...

(QB_NEW_EN)

docs/webview-html-security-implementation.md

[grammar] ~16-~16: There might be a mistake here.
Context: ...This allowed for several attack vectors: - Script injection: `<script>alert('XSS'...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...attack vectors: - Script injection: <script>alert('XSS')</script> - iframe embedding: `<iframe src="https:...

(QB_NEW_EN)


[grammar] ~18-~18: There might be a mistake here.
Context: ...XSS')</script>- **iframe embedding**:<iframe src="https://evil.com"></iframe>- **Meta refresh attacks**:<meta http-equi...

(QB_NEW_EN)


[grammar] ~19-~19: There might be a mistake here.
Context: ...></iframe>- **Meta refresh attacks**:- **Event handler attributes**:<img onclic...

(QB_NEW_EN)


[grammar] ~20-~20: There might be a mistake here.
Context: ....com">- **Event handler attributes**:- **Data URIs**:<img src="data:text/html,<...

(QB_NEW_EN)


[grammar] ~21-~21: There might be a mistake here.
Context: ...lert('XSS')" src="x">- **Data URIs**:- **JavaScript URIs**:<a href="javascript:...

(QB_NEW_EN)


[grammar] ~40-~40: There might be a mistake here.
Context: ...ecurity Configuration #### Allowed Tags Only safe, commonly used HTML elements a...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ...mmonly used HTML elements are permitted: - Text elements: p, div, span, br,...

(QB_NEW_EN)


[grammar] ~42-~42: There might be a mistake here.
Context: ...ext elements: p, div, span, br, hr - Headers: h1 through h6 - Formatting:...

(QB_NEW_EN)


[grammar] ~43-~43: There might be a mistake here.
Context: ...an, br, hr- Headers:h1throughh6- Formatting:strong, b, em, i, u...

(QB_NEW_EN)


[grammar] ~44-~44: There might be a mistake here.
Context: ..., em, i, u, s, small, sub, sup- Lists:ul, ol, li- Other:blockq...

(QB_NEW_EN)


[grammar] ~45-~45: There might be a mistake here.
Context: ...all, sub, sup- Lists:ul, ol, li- Other:blockquote, pre, code, tab...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...mg, dl, dt, dd` #### Blocked Tags Dangerous elements are completely remove...

(QB_NEW_EN)


[grammar] ~55-~55: There might be a mistake here.
Context: ...esh - Redirects #### Allowed Attributes Strict allowlist per tag: - Links (a):...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...wed Attributes Strict allowlist per tag: - Links (a): href, title - Images (`...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ...lowlist per tag: - Links (a): href, title - Images (img): src, alt, title, `...

(QB_NEW_EN)


[grammar] ~58-~58: There might be a mistake here.
Context: ...img): src, alt, title, width, height - Tables (table, th, td): Basic tabl...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ...le, th, td): Basic table attributes - All elements (*): style, class` (wi...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...trictions) #### URL Scheme Restrictions - General allowed schemes: http, `http...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...d schemes**: http, https, mailto, tel - Images: Only http and https (block...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ttps(blocksdata:andjavascript:) - **Links**: http, https, mailto, tel...

(QB_NEW_EN)


[grammar] ~67-~67: There might be a mistake here.
Context: ...vascript:`) #### CSS Property Allowlist Only safe CSS properties are permitted i...

(QB_NEW_EN)


[grammar] ~68-~68: There might be a mistake here.
Context: ...ies are permitted in style attributes: - Colors: color, background-color (val...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...ound-color(validated hex/rgb patterns) - Typography:font-size, font-weight`, ...

(QB_NEW_EN)


[grammar] ~73-~73: There might be a mistake here.
Context: ...h, height` #### Event Handler Removal All attributes starting with "on" are au...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ...rity measures: ### XSS Prevention Tests - Script tag removal - Event handler attri...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...SS Prevention Tests - Script tag removal - Event handler attribute stripping - Java...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...oval - Event handler attribute stripping - JavaScript URI neutralization - Data URI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...tripping - JavaScript URI neutralization - Data URI blocking ### Content Preservat...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...blocking ### Content Preservation Tests - Safe HTML elements are preserved - Basic...

(QB_NEW_EN)


[grammar] ~129-~129: There might be a mistake here.
Context: ...duction dependency for HTML sanitization - @types/sanitize-html (v2.16.0): Develo...

(QB_NEW_EN)


[grammar] ~153-~153: There might be a mistake here.
Context: ...n**: Eliminates script injection attacks 2. Content Isolation: Prevents iframe-bas...

(QB_NEW_EN)


[grammar] ~154-~154: There might be a mistake here.
Context: ...olation**: Prevents iframe-based attacks 3. URL Safety: Blocks dangerous data: and...

(QB_NEW_EN)


[grammar] ~155-~155: There might be a mistake here.
Context: ...cks dangerous data: and javascript: URIs 4. Event Blocking: Removes all event hand...

(QB_NEW_EN)


[grammar] ~156-~156: There might be a mistake here.
Context: ...**: Removes all event handler attributes 5. CSS Safety: Restricts styling to safe ...

(QB_NEW_EN)


[grammar] ~157-~157: There might be a mistake here.
Context: ...estricts styling to safe properties only 6. Redirect Prevention: Blocks meta refre...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...n maintains full backward compatibility: - All existing function signatures unchang...

(QB_NEW_EN)


[grammar] ~170-~170: There might be a mistake here.
Context: ...l performance overhead from sanitization - One-time processing per content render -...

(QB_NEW_EN)


[grammar] ~171-~171: There might be a mistake here.
Context: ...- One-time processing per content render - Cached sanitization library - No impact ...

(QB_NEW_EN)


[grammar] ~172-~172: There might be a mistake here.
Context: ...ent render - Cached sanitization library - No impact on React Native WebView perfor...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
docs/webview-security-implementation.md

93-93: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


102-102: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

🪛 GitHub Check: CodeQL
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx

[failure] 130-130: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.


[failure] 130-130: Bad HTML filtering regexp
This regular expression does not match script end tags like </script >.

src/utils/html-sanitizer.ts

[failure] 16-16: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.


[failure] 16-16: Bad HTML filtering regexp
This regular expression does not match script end tags like </script >.


[failure] 19-19: Incomplete multi-character sanitization
This string may still contain <iframe, which may cause an HTML element injection vulnerability.


[failure] 29-29: Incomplete multi-character sanitization
This string may still contain on, which may cause an HTML attribute injection vulnerability.


[failure] 52-52: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.

⏰ 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/stores/calendar/__tests__/store.test.ts (1)

308-339: Upcoming range expectations are precise and correctly stubbed.

The date-fns mocks for startOfDay/addDays/endOfDay/format drive a deterministic range. LGTM.

package.json (2)

161-161: Dependency addition looks correct; aligns with WebView sanitization goal.

Adding sanitize-html is a good move for untrusted content. This will let us drop brittle regex-based sanitization. I’m proposing concrete refactors in src/utils/html-sanitizer.ts to actually use this lib.


180-180: Types added appropriately.

@types/sanitize-html matches the new runtime dep and will improve DX. No issues.

src/app/_layout.tsx (1)

75-75: Verified: splash screen is hidden elsewhere

The audit script confirms that after calling SplashScreen.preventAutoHideAsync() in src/app/_layout.tsx (line 75), the app subsequently calls SplashScreen.hideAsync() in the alternate layout file. No further changes are needed.

• Hide call found at src/app/(app)/_layout.tsx:67await SplashScreen.hideAsync();

src/translations/ar.json (3)

62-62: Terminology tweak LGTM.

calendar.confirmSignup → "تأكيد" reads fine and matches the shorter English "Confirm".


342-342: New common.discard key LGTM.

Matches the unsaved-changes flow introduced elsewhere.


607-613: No outdated translation key references found

I’ve searched the entire codebase for calls to the old flat keys (body_required, recipients_required, subject_required) as well as the previous namespace (messages.body_required, messages.recipients_required, messages.subject_required). There are no remaining references—everything now correctly uses messages.validation.<key>.

src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1)

191-197: Keep origin whitelist consistent with implementation

You assert ['about:blank']. Verify the component (and shared defaults) actually set about:blank, not about:. If defaults use about:, this test will be brittle.

src/translations/es.json (1)

607-613: Validation key relocation verified

I ran searches across .ts, .tsx, .js, and .jsx files for any lingering references to the old flat keys (messages.body_required, messages.recipients_required, messages.subject_required, and their t('messages.<key>') usages) and found no matches. It appears all calls have been updated to use the nested messages.validation.* paths. No further changes are needed here.

src/utils/__tests__/webview-html.test.ts (1)

5-13: Solid XSS coverage for script-stripping — LGTM

These assertions correctly validate script removal while preserving safe content.

src/components/calendar/calendar-item-details-sheet.tsx (1)

62-71: Personnel auto-fetch logic — LGTM

The guard on isOpen, empty personnel, and not loading avoids duplicate fetches and churn.

src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx (2)

830-840: WebView prop assertion may need alignment with shared defaults

If you adopt defaultWebViewProps in the component (recommended), update this test to assert those defaults (e.g., originWhitelist ['about:']). Otherwise, keep the current expectation of ['about:blank'].


818-828: Dark theme CSS assertions — LGTM

The checks for dark text and background colors align with the generator and component behavior.

src/utils/webview-html.ts (2)

3-11: Typed options and clear API — LGTM

Interface and defaults are straightforward and consistent with usage.


221-237: Secure defaults object — LGTM

The defaults are appropriately locked down for inline HTML (about:), no JS, no DOM storage, and software layer type for Android.

src/components/calendar/enhanced-calendar-view.tsx (3)

111-111: Month-end calculation is TZ-safe and correct

Using new Date(year, month, 0) with a 1-based month from DateData yields the last day of that month. Paired with formatLocalDateString this avoids UTC drift.


129-130: LGTM on initial load range

Using formatLocalDateString(new Date(now.getFullYear(), now.getMonth() + 1, 0)) correctly computes the last day of the current month in local time.


120-123: Ensure local date is used for initial currentMonth state

The initial state in src/components/calendar/enhanced-calendar-view.tsx still appears to use a UTC-based derivation (new Date().toISOString().slice(0, 7)), which can drift around local midnight. This should be updated to use the same local-safe helper as goToToday.

• Location to verify: around line 29 in enhanced-calendar-view.tsx
• Apply this diff to align with getTodayLocalString():

- const [currentMonth, setCurrentMonth] = useState(new Date().toISOString().slice(0, 7)); // YYYY-MM format
+ const [currentMonth, setCurrentMonth] = useState(getTodayLocalString().slice(0, 7)); // YYYY-MM format (local TZ-safe)

This ensures both the initial render and the “go to today” action use local time and avoid UTC month drift.

Comment on lines +53 to +63
#### 5. Navigation Control
```tsx
onShouldStartLoadWithRequest={(request) => {
return request.url === 'about:blank' || request.url.startsWith('data:');
}}
onNavigationStateChange={(navState) => {
if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
return false;
}
}}
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

onNavigationStateChange cannot cancel navigation; remove misleading return

onNavigationStateChange is informational. return false here has no effect. Rely on onShouldStartLoadWithRequest to block.

-onNavigationStateChange={(navState) => {
-  if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
-    return false;
-  }
-}}
+// Keep this purely for analytics/logging if needed; blocking happens in onShouldStartLoadWithRequest
+onNavigationStateChange={() => {}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### 5. Navigation Control
```tsx
onShouldStartLoadWithRequest={(request) => {
return request.url === 'about:blank' || request.url.startsWith('data:');
}}
onNavigationStateChange={(navState) => {
if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
return false;
}
}}
```
#### 5. Navigation Control
🤖 Prompt for AI Agents
In docs/webview-security-implementation.md around lines 53 to 63, the
onNavigationStateChange handler contains a misleading "return false" which
cannot cancel navigation; remove the return statement (or remove the handler
entirely) and update the text to state that onNavigationStateChange is
informational only and that onShouldStartLoadWithRequest must be used to block
navigation.

Comment on lines +102 to +109
#### Usage:
```tsx
import { sanitizeHtml } from '@/utils/html-sanitizer';

// In the WebView HTML
<body>${sanitizeHtml(item.Description)}</body>
```

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix API name in example to match util export

Docs show sanitizeHtml(...); the util exports sanitizeHtmlContent(...). Align the example (or re-export an alias).

-import { sanitizeHtml } from '@/utils/html-sanitizer';
+import { sanitizeHtmlContent } from '@/utils/html-sanitizer';
 ...
-<body>${sanitizeHtml(item.Description)}</body>
+<body>${sanitizeHtmlContent(item.Description)}</body>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### Usage:
```tsx
import { sanitizeHtml } from '@/utils/html-sanitizer';
// In the WebView HTML
<body>${sanitizeHtml(item.Description)}</body>
```
#### Usage:
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

102-102: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

🤖 Prompt for AI Agents
In docs/webview-security-implementation.md around lines 102 to 109, the example
uses sanitizeHtml(...) but the utility actually exports
sanitizeHtmlContent(...); update the example to call
sanitizeHtmlContent(item.Description) to match the exported API, or
alternatively add a re-export/alias named sanitizeHtml in the utility module and
keep the doc unchanged; ensure the import statement and usage in the code block
both reflect the chosen fix.

//useAuth().hydrate();
// Prevent the splash screen from auto-hiding before asset loading is complete.
//SplashScreen.preventAutoHideAsync();
SplashScreen.preventAutoHideAsync();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

preventAutoHideAsync without a guaranteed hide can leave the app stuck on the splash screen.

Right now we prevent auto-hide but I don’t see a corresponding SplashScreen.hideAsync() that’s guaranteed to run after initial work. Add a safe hide on the first root layout to avoid regressions while keeping your current initialization intact.

Apply this minimal diff to ensure the splash is always hidden once the UI mounts:

@@
-  return (
-    <SafeAreaProvider>
-      <GestureHandlerRootView>
-        <KeyboardProvider>{Env.APTABASE_APP_KEY && !__DEV__ ? <AptabaseProviderWrapper appKey={Env.APTABASE_APP_KEY}>{renderContent()}</AptabaseProviderWrapper> : renderContent()}</KeyboardProvider>
-      </GestureHandlerRootView>
-    </SafeAreaProvider>
-  );
+  return (
+    <SafeAreaProvider>
+      <GestureHandlerRootView
+        onLayout={() => {
+          // idempotent; safe to call multiple times
+          SplashScreen.hideAsync().catch(() => {});
+        }}
+      >
+        <KeyboardProvider>
+          {Env.APTABASE_APP_KEY && !__DEV__ ? (
+            <AptabaseProviderWrapper appKey={Env.APTABASE_APP_KEY}>{renderContent()}</AptabaseProviderWrapper>
+          ) : (
+            renderContent()
+          )}
+        </KeyboardProvider>
+      </GestureHandlerRootView>
+    </SafeAreaProvider>
+  );

If you prefer to wait for all startup promises, we can also gate hideAsync behind a Promise.allSettled of the tasks initiated in useEffect. I can sketch that out if you want.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SplashScreen.preventAutoHideAsync();
return (
<SafeAreaProvider>
<GestureHandlerRootView
onLayout={() => {
// idempotent; safe to call multiple times
SplashScreen.hideAsync().catch(() => {});
}}
>
<KeyboardProvider>
{Env.APTABASE_APP_KEY && !__DEV__ ? (
<AptabaseProviderWrapper appKey={Env.APTABASE_APP_KEY}>
{renderContent()}
</AptabaseProviderWrapper>
) : (
renderContent()
)}
</KeyboardProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
);
🤖 Prompt for AI Agents
In src/app/_layout.tsx around line 75, you call
SplashScreen.preventAutoHideAsync() but never guarantee a corresponding
SplashScreen.hideAsync(), which can leave the app stuck on the splash screen;
fix this by adding a hide on the first root layout mount (e.g., in a useEffect
that runs once) that calls SplashScreen.hideAsync() and handles errors
(try/catch or .catch), and include a safe timeout fallback (clearable) so
hideAsync is forced after a short delay if other initialization hangs;
alternatively, if you prefer waiting for startup tasks, gate hideAsync behind
Promise.allSettled of those tasks and then call hideAsync in the settled
handler.

Comment on lines +176 to +197
it('should sanitize HTML content in description', () => {
const onCloseMock = jest.fn();

const { getByTestId } = render(
<CalendarItemDetailsSheet
item={mockItem}
isOpen={true}
onClose={onCloseMock}
/>
);

// Check that WebView is present
const webview = getByTestId('webview');
expect(webview).toBeTruthy();

// Verify WebView props include security settings
expect(webview.props['data-js-enabled']).toBe(false);
expect(webview.props['data-dom-storage']).toBe(false);
expect(webview.props['data-file-access']).toBe(false);
expect(webview.props['data-universal-access']).toBe(false);
expect(JSON.parse(webview.props['data-origin-whitelist'])).toEqual(['about:blank']);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Assert that sanitized HTML actually reaches the WebView

The test title says “sanitize HTML…”, but it only checks security props. Add a content assertion to prove <script> is gone while safe content remains.

   // Check that WebView is present
   const webview = getByTestId('webview');
   expect(webview).toBeTruthy();

+  // Verify sanitized HTML was passed to WebView
+  expect(webview.props.source?.html).toBeDefined();
+  expect(webview.props.source.html).not.toMatch(/<script/i);
+  expect(webview.props.source.html).toMatch(/Safe content/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should sanitize HTML content in description', () => {
const onCloseMock = jest.fn();
const { getByTestId } = render(
<CalendarItemDetailsSheet
item={mockItem}
isOpen={true}
onClose={onCloseMock}
/>
);
// Check that WebView is present
const webview = getByTestId('webview');
expect(webview).toBeTruthy();
// Verify WebView props include security settings
expect(webview.props['data-js-enabled']).toBe(false);
expect(webview.props['data-dom-storage']).toBe(false);
expect(webview.props['data-file-access']).toBe(false);
expect(webview.props['data-universal-access']).toBe(false);
expect(JSON.parse(webview.props['data-origin-whitelist'])).toEqual(['about:blank']);
});
it('should sanitize HTML content in description', () => {
const onCloseMock = jest.fn();
const { getByTestId } = render(
<CalendarItemDetailsSheet
item={mockItem}
isOpen={true}
onClose={onCloseMock}
/>
);
// Check that WebView is present
const webview = getByTestId('webview');
expect(webview).toBeTruthy();
// Verify sanitized HTML was passed to WebView
expect(webview.props.source?.html).toBeDefined();
expect(webview.props.source.html).not.toMatch(/<script/i);
expect(webview.props.source.html).toMatch(/Safe content/);
// Verify WebView props include security settings
expect(webview.props['data-js-enabled']).toBe(false);
expect(webview.props['data-dom-storage']).toBe(false);
expect(webview.props['data-file-access']).toBe(false);
expect(webview.props['data-universal-access']).toBe(false);
expect(JSON.parse(webview.props['data-origin-whitelist'])).toEqual(['about:blank']);
});
🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
around lines 176 to 197, the test only verifies WebView security props but does
not assert that sanitized HTML is actually passed to the WebView; update the
test to inspect the WebView's content prop (e.g., source.html or the prop your
component uses to pass HTML) and add assertions that any injected <script> tags
are removed/absent while expected safe content (e.g., specific text or markup
from mockItem.description) remains present, using string checks or parsing to
validate both the absence of dangerous tags and presence of safe content.

Comment on lines 283 to 333
<Box className="w-full rounded-lg bg-gray-50 p-1 dark:bg-gray-700">
<WebView
style={[styles.container, { height: 120 }]}
originWhitelist={['about:blank']}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
javaScriptEnabled={false}
domStorageEnabled={false}
allowFileAccess={false}
allowUniversalAccessFromFileURLs={false}
onShouldStartLoadWithRequest={(request) => {
// Only allow the initial HTML load with about:blank or data URLs
return request.url === 'about:blank' || request.url.startsWith('data:');
}}
onNavigationStateChange={(navState) => {
// Prevent any navigation away from the initial HTML
if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
return false;
}
}}
source={{
html: `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<style>
body {
color: ${colorScheme === 'dark' ? '#E5E7EB' : '#1F2937'};
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 8px;
font-size: 16px;
line-height: 1.5;
background-color: ${colorScheme === 'dark' ? '#374151' : '#F9FAFB'};
}
* {
max-width: 100%;
}
</style>
</head>
<body>${sanitizeHtml(item.Description)}</body>
</html>
`,
baseUrl: 'about:blank',
}}
androidLayerType="software"
testID="webview"
/>
</Box>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace inlined HTML and ad-hoc WebView props with shared utilities

Leverage generateWebViewHtml + defaultWebViewProps to standardize security, styling, and behavior. Also remove the onNavigationStateChange boolean return (ignored by RN WebView).

- <Box className="w-full rounded-lg bg-gray-50 p-1 dark:bg-gray-700">
-   <WebView
-     style={[styles.container, { height: 120 }]}
-     originWhitelist={['about:blank']}
-     scrollEnabled={false}
-     showsVerticalScrollIndicator={false}
-     javaScriptEnabled={false}
-     domStorageEnabled={false}
-     allowFileAccess={false}
-     allowUniversalAccessFromFileURLs={false}
-     onShouldStartLoadWithRequest={(request) => {
-       // Only allow the initial HTML load with about:blank or data URLs
-       return request.url === 'about:blank' || request.url.startsWith('data:');
-     }}
-     onNavigationStateChange={(navState) => {
-       // Prevent any navigation away from the initial HTML
-       if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
-         return false;
-       }
-     }}
-     source={{
-       html: `
-         <!DOCTYPE html>
-         <html>
-           <head>
-             <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
-             <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
-             <style>
-               body {
-                 color: ${colorScheme === 'dark' ? '#E5E7EB' : '#1F2937'};
-                 font-family: system-ui, -apple-system, sans-serif;
-                 margin: 0;
-                 padding: 8px;
-                 font-size: 16px;
-                 line-height: 1.5;
-                 background-color: ${colorScheme === 'dark' ? '#374151' : '#F9FAFB'};
-               }
-               * { max-width: 100%; }
-             </style>
-           </head>
-           <body>${sanitizeHtml(item.Description)}</body>
-         </html>
-       `,
-       baseUrl: 'about:blank',
-     }}
-     androidLayerType="software"
-     testID="webview"
-   />
- </Box>
+ <Box className="w-full rounded-lg bg-gray-50 p-1 dark:bg-gray-700">
+   <WebView
+     style={[styles.container, { minHeight: 120 }]}
+     {...defaultWebViewProps}
+     // Keep scroll disabled for layout expectations
+     scrollEnabled={false}
+     showsVerticalScrollIndicator={false}
+     onShouldStartLoadWithRequest={(request) => request.url === 'about:blank' || request.url.startsWith('data:')}
+     source={{
+       html: generateWebViewHtml({
+         content: item.Description,
+         isDarkMode: colorScheme === 'dark',
+       }),
+       baseUrl: 'about:blank',
+     }}
+     androidLayerType="software"
+     testID="webview"
+   />
+ </Box>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Box className="w-full rounded-lg bg-gray-50 p-1 dark:bg-gray-700">
<WebView
style={[styles.container, { height: 120 }]}
originWhitelist={['about:blank']}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
javaScriptEnabled={false}
domStorageEnabled={false}
allowFileAccess={false}
allowUniversalAccessFromFileURLs={false}
onShouldStartLoadWithRequest={(request) => {
// Only allow the initial HTML load with about:blank or data URLs
return request.url === 'about:blank' || request.url.startsWith('data:');
}}
onNavigationStateChange={(navState) => {
// Prevent any navigation away from the initial HTML
if (navState.url !== 'about:blank' && !navState.url.startsWith('data:')) {
return false;
}
}}
source={{
html: `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<style>
body {
color: ${colorScheme === 'dark' ? '#E5E7EB' : '#1F2937'};
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 8px;
font-size: 16px;
line-height: 1.5;
background-color: ${colorScheme === 'dark' ? '#374151' : '#F9FAFB'};
}
* {
max-width: 100%;
}
</style>
</head>
<body>${sanitizeHtml(item.Description)}</body>
</html>
`,
baseUrl: 'about:blank',
}}
androidLayerType="software"
testID="webview"
/>
</Box>
<Box className="w-full rounded-lg bg-gray-50 p-1 dark:bg-gray-700">
<WebView
style={[styles.container, { minHeight: 120 }]}
{...defaultWebViewProps}
// Keep scroll disabled for layout expectations
scrollEnabled={false}
showsVerticalScrollIndicator={false}
onShouldStartLoadWithRequest={(request) =>
request.url === 'about:blank' || request.url.startsWith('data:')
}
source={{
html: generateWebViewHtml({
content: item.Description,
isDarkMode: colorScheme === 'dark',
}),
baseUrl: 'about:blank',
}}
androidLayerType="software"
testID="webview"
/>
</Box>

Comment on lines 60 to 76
export function isHtmlSafe(html: string): boolean {
if (!html) return true;

// Check for dangerous patterns
const dangerousPatterns = [
/<script/i,
/<iframe/i,
/<object/i,
/<embed/i,
/<form/i,
/javascript:/i,
/on\w+\s*=/i, // Event handlers
/expression\s*\(/i, // CSS expressions
];

return !dangerousPatterns.some((pattern) => pattern.test(html));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make safety check policy-driven rather than regex-driven.

Return whether the sanitized output equals the input under the same SANITIZE_CONFIG. This avoids chasing new vectors with ad-hoc patterns.

Apply:

 export function isHtmlSafe(html: string): boolean {
-  if (!html) return true;
-
-  // Check for dangerous patterns
-  const dangerousPatterns = [
-    /<script/i,
-    /<iframe/i,
-    /<object/i,
-    /<embed/i,
-    /<form/i,
-    /javascript:/i,
-    /on\w+\s*=/i, // Event handlers
-    /expression\s*\(/i, // CSS expressions
-  ];
-
-  return !dangerousPatterns.some((pattern) => pattern.test(html));
+  if (!html) return true;
+  const cleaned = sanitizeLib(html, SANITIZE_CONFIG).trim();
+  // If sanitization changed anything, input wasn't safe
+  return cleaned === html.trim();
 }

This keeps tests conceptually intact while eliminating regex maintenance and covering attributes like srcdoc, srcset, malformed end-tags, etc.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/utils/html-sanitizer.ts around lines 60 to 76, replace the regex-based
isHtmlSafe check with a policy-driven comparison: import/require the project
SANITIZE_CONFIG and the same sanitization function used elsewhere, run the
sanitizer on the input using that config, and return true only if the sanitized
output strictly equals the original input (handle empty/null input by returning
true). Ensure you use the identical config instance (not a hardcoded set of
patterns) so attributes like srcdoc/srcset and malformed tags are covered and
tests remain unchanged.

sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High test

This string may still contain
<script
, which may cause an HTML element injection vulnerability.

Copilot Autofix

AI 5 months ago

The most reliable fix is to repeatedly apply the regular expression replacement until no further <script> tags remain. This technique ensures that dangerous tags and their remnants cannot reappear in the output due to partial matching or nested/malformed HTML. To achieve this, we should change the mocked sanitizeHtmlContent implementation to use a loop, as recommended in the Reference section. Specifically, replace the script tag matching line so that it repeatedly replaces matches until the input is unchanged, and then returns the fully sanitized string. Only the mock implementation at lines 130–134 of the test file needs updating.


Suggested changeset 1
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx b/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
--- a/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
+++ b/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
@@ -129,8 +129,13 @@
 jest.mock('@/utils/webview-html', () => ({
   sanitizeHtmlContent: (html: string) => {
     if (!html) return '';
-    // Strip script tags robustly with non-greedy pattern that handles newlines and nested content
-    return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
+    // Strip script tags robustly by repeatedly replacing until no matches remain
+    let previous;
+    do {
+      previous = html;
+      html = html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
+    } while (html !== previous);
+    return html;
   },
 }));
 
EOF
@@ -129,8 +129,13 @@
jest.mock('@/utils/webview-html', () => ({
sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
// Strip script tags robustly by repeatedly replacing until no matches remain
let previous;
do {
previous = html;
html = html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
} while (html !== previous);
return html;
},
}));

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');

Check failure

Code scanning / CodeQL

Bad HTML filtering regexp High test

This regular expression does not match script end tags like </script\t\n bar>.

Copilot Autofix

AI 5 months ago

The best way to fix this problem is to avoid custom regex-based HTML sanitization altogether and directly use a well-tested third-party sanitization library like sanitize-html, even in mock/test situations. However, if third-party libraries cannot be used in the test context, the regex should be improved to match closing tags with any characters between </script and > (e.g., attributes, whitespace), matching how browsers parse HTML. This can be done by changing the closing tag pattern from </script\s*> to </script[^>]*>. Specifically, in src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx, line 133, replace:

html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');

with:

html.replace(/<script\b[^>]*>[\s\S]*?<\/script[^>]*>/gi, '');

No further changes are needed, as this robustifies the regex in place.


Suggested changeset 1
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx b/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
--- a/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
+++ b/src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
@@ -129,8 +129,8 @@
 jest.mock('@/utils/webview-html', () => ({
   sanitizeHtmlContent: (html: string) => {
     if (!html) return '';
-    // Strip script tags robustly with non-greedy pattern that handles newlines and nested content
-    return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
+    // Strip script tags robustly with non-greedy pattern that matches permissive script closing tags
+    return html.replace(/<script\b[^>]*>[\s\S]*?<\/script[^>]*>/gi, '');
   },
 }));
 
EOF
@@ -129,8 +129,8 @@
jest.mock('@/utils/webview-html', () => ({
sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
// Strip script tags robustly with non-greedy pattern that matches permissive script closing tags
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script[^>]*>/gi, '');
},
}));

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
function isSafeScheme(url: string): boolean {
if (!url) return false;
const normalizedUrl = url.toLowerCase().trim();
return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');

Check failure

Code scanning / CodeQL

Incomplete URL scheme check High

This check does not consider vbscript:.

Copilot Autofix

AI 5 months ago

To resolve the issue, the function isSafeScheme should be updated to also check and block URLs that start with the "vbscript:" scheme (case-insensitive, trimmed, as with the others). This can be accomplished by extending the logical check in the return statement on line 16 to include !normalizedUrl.startsWith('vbscript:'). No further structural changes are necessary, as this is a simple string prefix check as for the other schemes.

Changes:

  • Edit the return statement (line 16) in isSafeScheme(url: string): boolean to add "vbscript:" to the blocked schemes, alongside "javascript:" and "data:".
  • No new imports, method definitions, or variable definitions are needed.

Suggested changeset 1
src/utils/html-sanitizer.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/utils/html-sanitizer.ts b/src/utils/html-sanitizer.ts
--- a/src/utils/html-sanitizer.ts
+++ b/src/utils/html-sanitizer.ts
@@ -13,7 +13,7 @@
 function isSafeScheme(url: string): boolean {
   if (!url) return false;
   const normalizedUrl = url.toLowerCase().trim();
-  return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');
+  return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:') && !normalizedUrl.startsWith('vbscript:');
 }
 
 /**
EOF
@@ -13,7 +13,7 @@
function isSafeScheme(url: string): boolean {
if (!url) return false;
const normalizedUrl = url.toLowerCase().trim();
return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');
return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:') && !normalizedUrl.startsWith('vbscript:');
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
@ucswift
Copy link
Member Author

ucswift commented Aug 23, 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.

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: 3

Caution

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

⚠️ Outside diff range comments (2)
src/components/calendar/enhanced-calendar-view.tsx (2)

186-186: Bug: period fields are ignored with markingType="multi-dot"

You set startingDay, endingDay, and color (period marking) but the Calendar is configured with markingType="multi-dot". In react-native-calendars, period fields are ignored unless markingType is "period" (single period per day) or "multi-period" (multiple overlapping periods). Result: multi-day range highlighting won’t render.

Quick fix (keeps current data structure and single-period semantics):

-          markingType="multi-dot"
+          markingType="period"

Alternative (if you need multiple overlapping ranges and dots): switch to "multi-period" and build per-day periods: [{ startingDay, endingDay, color }] arrays, or consider "custom" marking to combine dots and shading. I can provide a concrete refactor if desired.


29-29: Avoid UTC month drift in currentMonth initialization

new Date().toISOString().slice(0, 7) uses UTC and can select the wrong month for users near month boundaries in negative/positive time zones. Prefer a local YYYY-MM.

Apply this diff:

-  const [currentMonth, setCurrentMonth] = useState(new Date().toISOString().slice(0, 7)); // YYYY-MM format
+  const [currentMonth, setCurrentMonth] = useState(formatLocalDateString(new Date()).slice(0, 7)); // YYYY-MM (local)
♻️ Duplicate comments (4)
src/components/calendar/enhanced-calendar-view.tsx (2)

37-41: TZ-safe parsing and local range iteration looks correct; add a defensive end<start guard

Parsing Start/End as Date and formatting via formatLocalDateString is the right fix. While iterating the multi-day range, add a fast-fail for bad data (end before start) to avoid unnecessary work and subtle marks.

Apply this diff:

       if (startDate !== endDate) {
         // Use local Date constructors to avoid timezone issues
         const start = new Date(startDateObj.getFullYear(), startDateObj.getMonth(), startDateObj.getDate());
         const end = new Date(endDateObj.getFullYear(), endDateObj.getMonth(), endDateObj.getDate());
+        if (end < start) {
+          return; // skip invalid ranges defensively
+        }
         const current = new Date(start);

         while (current <= end) {
           const dateStr = formatLocalDateString(current);

Also applies to: 59-66


227-227: Switched to isSameDate for equality — good call

This matches the backend’s timezone semantics and prevents split('T') pitfalls addressed previously.

src/utils/html-sanitizer.ts (1)

232-251: isHtmlSafe should be policy-driven equality, not length heuristic

Heuristic ratios can yield false positives/negatives (e.g., short content with one unsafe attr). Safer: sanitize and compare strict equality.

Apply:

 export function isHtmlSafe(html: string): boolean {
   if (!html) return true;
-
-  // Sanitize the content and compare with original
-  const sanitized = sanitizeDescription(html);
-  const originalLength = html.replace(/\s+/g, ' ').trim().length;
-  const sanitizedLength = sanitized.replace(/\s+/g, ' ').trim().length;
-
-  // If sanitization significantly reduced the content length, it likely contained unsafe elements
-  // Based on testing:
-  // - Safe content: ratio = 1.0 (no change)
-  // - onclick removal: ratio = ~0.39
-  // - expression removal: ratio = ~0.27
-  // - javascript: removal: ratio = ~0.36
-  const lengthRatio = originalLength > 0 ? sanitizedLength / originalLength : 1;
-
-  // Content is considered unsafe if the length ratio drops below 0.8
-  // This catches cases where significant content was removed due to security concerns
-  return lengthRatio >= 0.8 && sanitized.trim().length > 0;
+  const sanitized = sanitizeDescription(html);
+  return sanitized.trim() === html.trim();
 }
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1)

191-201: Actually assert sanitized HTML reaches WebView

The test name claims sanitization, but assertions only inspect security flags. Verify that <script> is removed and safe content remains.

Apply:

   // Check that WebView is present
   const webview = getByTestId('webview');
   expect(webview).toBeTruthy();
 
   // Verify WebView props include security settings
   expect(webview.props['data-js-enabled']).toBe(false);
   expect(webview.props['data-dom-storage']).toBe(false);
   expect(webview.props['data-file-access']).toBe(false);
   expect(webview.props['data-universal-access']).toBe(false);
   expect(JSON.parse(webview.props['data-origin-whitelist'])).toEqual(['about:blank']);
+
+  // Verify sanitized HTML was passed to WebView
+  expect(webview.props.source?.html).toBeDefined();
+  expect(webview.props.source.html).not.toMatch(/<script\b/i);
+  expect(webview.props.source.html).toMatch(/Safe content/);
+  // Base URL should remain constrained
+  expect(webview.props.baseUrl).toBe('about:blank');
🧹 Nitpick comments (8)
src/components/calendar/enhanced-calendar-view.tsx (3)

33-33: Type markedDates precisely; avoid any

Use the prop’s declared type to tighten typing and get IDE help.

-    const marked: Record<string, any> = {};
+    const marked: NonNullable<CalendarProps['markedDates']> = {};

51-55: Ensure dot key is a string

react-native-calendars expects dot keys as strings. CalendarItemId may be numeric.

-      marked[startDate].dots.push({
-        key: item.CalendarItemId,
+      marked[startDate].dots.push({
+        key: String(item.CalendarItemId),
         color: item.TypeColor || '#3B82F6', // Use event type color or default blue
       });

211-221: Move inline computations out of render; use i18n locale for date formatting; prefer ternary over &&

The IIFEs allocate work on every render and violate our guideline to avoid anonymous functions/heavy computations during render. Also, use the active i18n locale for toLocaleDateString. Replace the render-time IIFEs with useMemo and the conditional with a ternary.

Apply this diff in the render block:

-      {/* Selected Date Info */}
-      {selectedDate && (
+      {/* Selected Date Info */}
+      {selectedDate ? (
         <View className="border-t border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
           <Text className="text-sm font-medium text-gray-900 dark:text-white">
             {t('calendar.selectedDate.title', {
-              date: (() => {
-                // Parse the date string properly to avoid timezone issues
-                const [year, month, day] = selectedDate.split('-').map(Number);
-                const localDate = new Date(year, month - 1, day); // month is 0-indexed
-                return localDate.toLocaleDateString([], {
-                  weekday: 'long',
-                  year: 'numeric',
-                  month: 'long',
-                  day: 'numeric',
-                });
-              })(),
+              date: selectedDateLabel,
             })}
           </Text>
-          {(() => {
-            const eventsForDay = selectedMonthItems.filter((item) => {
-              // Use isSameDate for timezone-safe date comparison with .NET backend timezone-aware dates
-              return selectedDate ? isSameDate(item.Start, selectedDate) : false;
-            });
-
-            if (eventsForDay.length > 0) {
-              return <Text className="mt-1 text-xs text-gray-600 dark:text-gray-300">{t('calendar.eventsCount', { count: eventsForDay.length })}</Text>;
-            }
-
-            return <Text className="mt-1 text-xs text-gray-500 dark:text-gray-400">{t('calendar.noEvents')}</Text>;
-          })()}
+          {eventsForDay.length > 0 ? (
+            <Text className="mt-1 text-xs text-gray-600 dark:text-gray-300">
+              {t('calendar.eventsCount', { count: eventsForDay.length })}
+            </Text>
+          ) : (
+            <Text className="mt-1 text-xs text-gray-500 dark:text-gray-400">
+              {t('calendar.noEvents')}
+            </Text>
+          )}
         </View>
-      )}
+      ) : null}

And add these memoized values above the return (for example right after the markedDates useMemo):

// use the active i18n locale for formatting
const { i18n } = useTranslation();

const selectedDateLabel = useMemo(() => {
  if (!selectedDate) return '';
  const [year, month, day] = selectedDate.split('-').map(Number);
  const localDate = new Date(year, month - 1, day);
  return localDate.toLocaleDateString(i18n.language ? [i18n.language] : [], {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
}, [selectedDate, i18n.language]);

const eventsForDay = useMemo(
  () => (selectedDate ? selectedMonthItems.filter((item) => isSameDate(item.Start, selectedDate)) : []),
  [selectedDate, selectedMonthItems]
);

Also applies to: 224-235

src/utils/html-sanitizer.ts (3)

22-42: Attribute value assumptions — guard against non-string values

filterDangerousAttributes assumes attribute values are strings (uses toLowerCase, includes). sanitize-html typically provides strings, but defensive checks avoid accidental crashes if a boolean/number slips in.

Apply:

-function filterDangerousAttributes(tag: string, name: string, value: string): boolean {
+function filterDangerousAttributes(tag: string, name: string, value: unknown): boolean {
+  // Coerce to string for uniform checks; undefined/null -> ''
+  const strVal = String(value ?? '');
   // Block all event handlers (onclick, onload, etc.)
   if (name.toLowerCase().startsWith('on')) {
     return false;
   }
 
   // Block style attributes with expressions or javascript
   if (name.toLowerCase() === 'style') {
-    const normalizedValue = value.toLowerCase();
+    const normalizedValue = strVal.toLowerCase();
     if (normalizedValue.includes('expression(') || normalizedValue.includes('javascript:')) {
       return false;
     }
   }
 
   // For URL attributes, ensure safe schemes
   if (['href', 'src', 'srcset', 'srcdoc'].includes(name.toLowerCase())) {
-    return isSafeScheme(value);
+    return isSafeScheme(strVal);
   }
 
   return true;
 }

Also applies to: 165-184


118-128: Make allowedStyles color/font patterns precise (optional polish)

The color regex accepts arbitrary hex lengths (e.g., #ff). Tighten to 3/6/8 hex; add rgba/hsla for common safe usage. Allow fractional font-sizes.

Apply:

   allowedStyles: {
     '*': {
-      color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/],
+      color: [
+        /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i,
+        /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i,
+        /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0?\.\d+)\s*\)$/i
+      ],
       'text-align': [/^left$/, /^right$/, /^center$/, /^justify$/],
-      'font-size': [/^\d+(?:px|em|%)$/],
+      'font-size': [/^\d*\.?\d+(?:px|em|%)$/],
       'font-weight': [/^(?:normal|bold|bolder|lighter|\d+)$/],
       margin: [/^\d+(?:px|em|%)$/],
       padding: [/^\d+(?:px|em|%)$/],
     },
   },

102-110: Keep config self-consistent and minimal

  • allowedIframeHostnames/allowedScriptHostnames are redundant since iframe/script are disallowed; harmless but can be removed for clarity.
  • Consider adding tel to allowedSchemes only if you intend to support dial links. WebView navigation is blocked for anything except about:blank and data:, so leaving tel out is consistent with current behavior.

If you want to keep them, no change required. If you prefer minimalism, remove these keys. Also confirm product intent for tel links.

Also applies to: 114-117, 130-133

src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (2)

216-222: Broaden navigation security assertions (optional)

Confirm tel/mailto are blocked by onShouldStartLoadWithRequest to match current policy.

Apply:

   expect(shouldLoad({ url: 'about:blank' })).toBe(true);
   expect(shouldLoad({ url: 'data:text/html,<html></html>' })).toBe(true);
   expect(shouldLoad({ url: 'http://evil.com' })).toBe(false);
   expect(shouldLoad({ url: 'javascript:alert(1)' })).toBe(false);
+  expect(sh ouldLoad({ url: 'mailto:test@example.com' })).toBe(false);
+  expect(shouldLoad({ url: 'tel:+15551234567' })).toBe(false);

59-74: WebView mock is solid; small improvement to expose source.html in data- for easier debugging (optional)*

You already pass through all props, which is sufficient. If you want easier snapshots/logging later, you can mirror source.html into a data attribute. Optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 943bab1 and d4d6220.

📒 Files selected for processing (7)
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1 hunks)
  • src/components/calendar/__tests__/calendar-item-details-sheet.test.tsx (10 hunks)
  • src/components/calendar/calendar-item-details-sheet.tsx (9 hunks)
  • src/components/calendar/enhanced-calendar-view.tsx (6 hunks)
  • src/utils/__tests__/html-sanitizer.test.ts (1 hunks)
  • src/utils/html-sanitizer.ts (1 hunks)
  • src/utils/webview-html.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/utils/webview-html.ts
  • src/utils/tests/html-sanitizer.test.ts
  • src/components/calendar/calendar-item-details-sheet.tsx
  • src/components/calendar/tests/calendar-item-details-sheet.test.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{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/calendar/enhanced-calendar-view.tsx
  • src/utils/html-sanitizer.ts
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.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/calendar/enhanced-calendar-view.tsx
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.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/calendar/enhanced-calendar-view.tsx
  • src/utils/html-sanitizer.ts
  • src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
**/*.{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/calendar/__tests__/calendar-item-details-sheet.security.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/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
🧬 Code graph analysis (2)
src/components/calendar/enhanced-calendar-view.tsx (1)
src/lib/utils.ts (3)
  • formatLocalDateString (243-248)
  • getTodayLocalString (254-256)
  • isSameDate (258-281)
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx (1)
src/components/calendar/calendar-item-details-sheet.tsx (1)
  • CalendarItemDetailsSheet (31-420)
🪛 GitHub Check: CodeQL
src/utils/html-sanitizer.ts

[failure] 16-16: Incomplete URL scheme check
This check does not consider vbscript:.

src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx

[failure] 133-133: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.


[failure] 133-133: Bad HTML filtering regexp
This regular expression does not match script end tags like </script\t\n bar>.

⏰ 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). (2)
  • GitHub Check: test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
src/components/calendar/enhanced-calendar-view.tsx (4)

12-12: Good: centralized, timezone-safe date utils adoption

Importing formatLocalDateString, getTodayLocalString, and isSameDate aligns the file with the new locale-aware helpers and prior feedback.


110-113: LGTM: local month boundaries

Constructing start/end via local Date constructors and formatting with formatLocalDateString correctly avoids TZ drift.


121-121: LGTM: goToToday uses local helper

Using getTodayLocalString keeps the selection/month sync correct across timezones.


129-131: LGTM: initial load range uses local month boundaries

Correctly mirrors the month-change logic on mount.

Comment on lines +128 to +135
// Mock HTML sanitizer
jest.mock('@/utils/webview-html', () => ({
sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
},
}));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace regex-based sanitizer mock with pass-through to the real util

The test mock reintroduces brittle regex sanitization flagged by CodeQL. For this suite, use the production sanitizer to avoid regex maintenance and get realistic behavior.

Apply:

-// Mock HTML sanitizer
-jest.mock('@/utils/webview-html', () => ({
-  sanitizeHtmlContent: (html: string) => {
-    if (!html) return '';
-    // Strip script tags robustly with non-greedy pattern that handles newlines and nested content
-    return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
-  },
-}));
+// Use real sanitizer to avoid regex pitfalls and align with prod policy
+jest.mock('@/utils/webview-html', () => {
+  const actual = jest.requireActual('@/utils/webview-html');
+  return { ...actual, sanitizeHtmlContent: actual.sanitizeHtmlContent };
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mock HTML sanitizer
jest.mock('@/utils/webview-html', () => ({
sanitizeHtmlContent: (html: string) => {
if (!html) return '';
// Strip script tags robustly with non-greedy pattern that handles newlines and nested content
return html.replace(/<script\s*[^>]*>[\s\S]*?<\/script\s*>/gi, '');
},
}));
// Use real sanitizer to avoid regex pitfalls and align with prod policy
jest.mock('@/utils/webview-html', () => {
const actual = jest.requireActual('@/utils/webview-html');
return { ...actual, sanitizeHtmlContent: actual.sanitizeHtmlContent };
});
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 133-133: Incomplete multi-character sanitization
This string may still contain <script, which may cause an HTML element injection vulnerability.


[failure] 133-133: Bad HTML filtering regexp
This regular expression does not match script end tags like </script\t\n bar>.

🤖 Prompt for AI Agents
In
src/components/calendar/__tests__/calendar-item-details-sheet.security.test.tsx
around lines 128-135, the test currently mocks the sanitizer with a brittle
regex; replace that mock so the test uses the real production sanitizer instead.
Remove or replace the jest.mock block and either import the real
sanitizeHtmlContent from '@/utils/webview-html' or implement the mock to forward
to jest.requireActual('@/utils/webview-html') so the real sanitizeHtmlContent
implementation is used in this test; ensure no other tests rely on the regex
mock by scoping/removing the mock only in this file.

Comment on lines +13 to +17
function isSafeScheme(url: string): boolean {
if (!url) return false;
const normalizedUrl = url.toLowerCase().trim();
return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Blocklist misses vbscript: (and similar) — tighten scheme guard

Current check only blocks javascript: and data:. CodeQL flagged vbscript:. Tighten the guard to cover vbscript: as well (case-insensitive). This is low-risk and aligns with the rest of the policy.

Apply:

-function isSafeScheme(url: string): boolean {
-  if (!url) return false;
-  const normalizedUrl = url.toLowerCase().trim();
-  return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');
-}
+function isSafeScheme(url: string): boolean {
+  if (!url) return false;
+  const normalizedUrl = url.trim().toLowerCase();
+  // Block common scriptable schemes; sanitize-html allowlist enforces final acceptance.
+  return !/^(?:javascript|vbscript|data)\s*:/i.test(normalizedUrl);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isSafeScheme(url: string): boolean {
if (!url) return false;
const normalizedUrl = url.toLowerCase().trim();
return !normalizedUrl.startsWith('javascript:') && !normalizedUrl.startsWith('data:');
}
function isSafeScheme(url: string): boolean {
if (!url) return false;
const normalizedUrl = url.trim().toLowerCase();
// Block common scriptable schemes; sanitize-html allowlist enforces final acceptance.
return !/^(?:javascript|vbscript|data)\s*:/i.test(normalizedUrl);
}
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 16-16: Incomplete URL scheme check
This check does not consider vbscript:.

🤖 Prompt for AI Agents
In src/utils/html-sanitizer.ts around lines 13 to 17, the scheme guard only
blocks "javascript:" and "data:"; update it to also reject "vbscript:"
(case-insensitive) so URLs starting with "vbscript:" are treated as unsafe.
Modify the normalized check to include a startsWith('vbscript:') condition (or a
small RegExp/array of blocked schemes) and return false for those cases, keeping
the existing lowercase/trim normalization and behavior.

Comment on lines +90 to +100
allowedAttributes: {
a: ['href', 'title'],
img: ['src', 'alt', 'title', 'width', 'height'],
span: ['style'],
div: ['style'],
p: ['style'],
table: ['width', 'cellpadding', 'cellspacing'],
th: ['scope', 'colspan', 'rowspan'],
td: ['colspan', 'rowspan'],
'*': ['class'],
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

rel/target are stripped by allowedAttributes — add them to anchor allowlist

transformTags sets target="_blank" and rel="noopener noreferrer", but allowedAttributes.a excludes both, so they are dropped post-transform. Include them explicitly so the intended hardening actually reaches output.

Apply:

   allowedAttributes: {
-    a: ['href', 'title'],
+    a: ['href', 'title', 'rel', 'target', 'name'],

No other changes needed in transformTags; once allowed, the attributes will persist.

Also applies to: 135-157

🤖 Prompt for AI Agents
In src/utils/html-sanitizer.ts around lines 90 to 100 (and also apply the same
change at lines 135 to 157), the allowedAttributes configuration for anchors
omits rel and target so transformTags' target="_blank" and rel="noopener
noreferrer" are stripped; update the allowlist to include 'rel' and 'target' for
the 'a' tag so those attributes are preserved in the final output—no changes to
transformTags required, just add 'rel' and 'target' to the list for a (and
mirror the same addition in the later block at 135-157).

@ucswift ucswift merged commit 3c88d4f into master Aug 23, 2025
10 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