Skip to content

Comments

RU-T47 Fix html content render, fixing mapbox issue on call view.#218

Merged
ucswift merged 2 commits intomasterfrom
develop
Feb 16, 2026
Merged

RU-T47 Fix html content render, fixing mapbox issue on call view.#218
ucswift merged 2 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Introduced HtmlRenderer for consistent HTML content rendering across the app
    • Added configurable initial map center and zoom options
  • Bug Fixes & Improvements

    • Improved location parsing with robust validation and fallbacks
    • Maps render only when valid coordinates exist and are more stable with error handling and size sync
    • Updated map marker visuals and refined cross-platform style handling for UI components

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

This PR replaces multiple WebView usages with a new cross-platform HtmlRenderer, adds style prop support to several web UI primitives, updates tests/mocks for HtmlRenderer, and hardens map initialization, camera and marker rendering with size observation and coordinate validation.

Changes

Cohort / File(s) Summary
WebView → HtmlRenderer Migration
src/app/call/[id].tsx, src/components/calls/call-card.tsx, src/components/notes/note-details-sheet.tsx, src/components/protocols/protocol-details-sheet.tsx, src/components/contacts/contact-notes-list.tsx
Replaced inline WebView HTML wrappers with HtmlRenderer(html={...}); removed many WebView-specific props and color-scheme inline logic; updated imports and rendering keys where applicable.
HtmlRenderer Implementation (native + web + tests)
src/components/ui/html-renderer/index.tsx, src/components/ui/html-renderer/index.web.tsx, src/components/ui/html-renderer/__tests__/html-renderer.test.tsx
Added a cross-platform HtmlRenderer component (native uses WebView, web uses iframe/srcDoc) with theming, customCSS, rendererKey, and link interception; includes tests covering light/dark defaults and overrides.
Map Initialization & Resilience
src/components/maps/map-view.web.tsx, src/components/maps/static-map.tsx
Added initialCenter/initialZoom, ResizeObserver-driven initialization, guards against NaN/invalid coordinates, try/catch around mapbox-gl operations, camera animationMode support, and a composed marker pin.
UI Primitives — style prop support
src/components/ui/box/index.web.tsx, src/components/ui/card/index.web.tsx, src/components/ui/center/index.web.tsx, src/components/ui/hstack/index.web.tsx, src/components/ui/vstack/index.web.tsx
Extended component props to accept style?: StyleProp<ViewStyle>, flatten array styles with StyleSheet.flatten, apply flattened styles to DOM elements, and export components explicitly.
Tests & Mocks Updated for HtmlRenderer
src/app/call/__tests__/[id].test.tsx, src/app/call/__tests__/[id].security.test.tsx, src/components/contacts/__tests__/*, src/components/notes/__tests__/*, src/components/protocols/__tests__/*
Replaced react-native-webview mocks with @/components/ui/html-renderer mocks (named HtmlRenderer); added StyleSheet.flatten helper to mocked RN StyleSheet; updated testIDs and mock shapes.
Call page: location parsing & conditional map render
src/app/call/[id].tsx
Improved location parsing: prefer numeric Latitude/Longitude (non-NaN, at least one non-zero), fallback to Geolocation string parsing; render StaticMap only when coordinates present; handleRoute now checks for null coordinates.
Shared tabs layout adjustments
src/components/ui/shared-tabs.tsx
Removed several flex-1 usages, introduced flexGrow adjustments and ScrollView flexGrow: 0 to stabilize horizontal scrolling and layout behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Develop #193: Touches protocol-details-sheet rendering/remount behavior; related to the protocol → HtmlRenderer migration and rendererKey usage.
  • Develop #202: Updates map initialization and platform map layer behavior; overlaps with map-view.web.tsx and static-map.tsx changes.
  • CU-868fdadum Expo update for And16k, bug fixes and tweaks. #166: Modifies map initialization/camera/marker logic; relevant to the ResizeObserver and coordinate validation changes.

Poem

🐰 I swapped WebViews for HtmlRenderer light,
Feeding HTML safely, day and night.
Maps now wait for size and sane coords tight,
UI boxes accept styles just right.
A little hop, the code feels bright! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (20 files):

⚔️ src/app/(app)/index.tsx (content)
⚔️ src/app/call/[id].tsx (content)
⚔️ src/app/call/__tests__/[id].security.test.tsx (content)
⚔️ src/app/call/__tests__/[id].test.tsx (content)
⚔️ src/components/calls/call-card.tsx (content)
⚔️ src/components/contacts/__tests__/contact-details-sheet.test.tsx (content)
⚔️ src/components/contacts/__tests__/contact-notes-list.test.tsx (content)
⚔️ src/components/contacts/contact-notes-list.tsx (content)
⚔️ src/components/maps/map-view.web.tsx (content)
⚔️ src/components/maps/static-map.tsx (content)
⚔️ src/components/notes/__tests__/note-details-sheet.test.tsx (content)
⚔️ src/components/notes/note-details-sheet.tsx (content)
⚔️ src/components/protocols/__tests__/protocol-details-sheet.test.tsx (content)
⚔️ src/components/protocols/protocol-details-sheet.tsx (content)
⚔️ src/components/ui/box/index.web.tsx (content)
⚔️ src/components/ui/card/index.web.tsx (content)
⚔️ src/components/ui/center/index.web.tsx (content)
⚔️ src/components/ui/hstack/index.web.tsx (content)
⚔️ src/components/ui/shared-tabs.tsx (content)
⚔️ src/components/ui/vstack/index.web.tsx (content)

These conflicts must be resolved before merging into master.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: migration from WebView to HtmlRenderer for HTML rendering, plus mapbox stability improvements for the call view.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch develop
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@src/app/call/`[id].tsx:
- Line 312: The HtmlRenderer call should guard against null/undefined values so
it doesn't render "null"/"undefined" as text; update the jsx that uses
HtmlRenderer (e.g., HtmlRenderer html={call.Note ...} and the similar usage for
call.Nature) to pass a safe default like html={call.Note ?? ''} (and
html={call.Nature ?? ''}) so the component always receives a string; preserve
the existing style prop (StyleSheet.flatten([styles.container, { height: 200
}])) when making this change.
- Around line 463-468: The visibility checks for the map incorrectly use
truthiness on coordinates (coordinates.latitude && coordinates.longitude and
!coordinates.latitude || !coordinates.longitude) which fails for valid 0 values;
update both checks to explicit null checks (e.g., ensure coordinates.latitude
!== null && coordinates.longitude !== null when showing the StaticMap and use
!== null checks when gating behavior elsewhere) so 0 is treated as a valid
coordinate — look for references to coordinates.latitude, coordinates.longitude
and the StaticMap usage to apply the change.

In `@src/components/calls/call-card.tsx`:
- Around line 1-5: Remove the stray blank line and reorder the import groups so
external packages and framework imports are grouped before local/project
imports: keep imports for 'lucide-react-native', 'react', and 'react-native'
together, then import '@/components/ui/html-renderer' after them; after
adjusting the ordering, run the project's linter autofix (npx eslint --fix) to
ensure the import sorting (simple-import-sort/imports) passes CI.

In `@src/components/maps/map-view.web.tsx`:
- Around line 243-251: The current map error handler in the newMap.on('error',
...) callback relies on a fragile minified symbol check ('r[3]'); remove that
check and stop matching on minified internals — instead only special-case the
stable "Invalid LngLat" message (if you still want to suppress that silently)
and for all other errors log them at warn level without rethrowing. Update the
handler inside MapView.web.tsx (the newMap.on('error' ...) callback) to drop the
msg.includes('r[3]') branch, keep or add a single stable string check for
"Invalid LngLat", and change the console warning code to consistently log
remaining mapbox-gl errors at warn level.
- Around line 386-421: The effect currently unconditionally skips the first
centerCoordinate update via hasInitialized, which prevents maps without an
initialCenter prop from being moved on first render; change the logic in the
useEffect inside map-view.web.tsx so you only return early when an initialCenter
was actually provided to the MapView. Concretely, detect whether initialCenter
(or a boolean like initialCenterProvided) exists, then update the block around
hasInitialized.current to set hasInitialized.current = true and only return if
initialCenter was provided; otherwise allow the subsequent camera flyTo/easeTo
to run on the first render. Reference: hasInitialized, useEffect, map,
centerCoordinate, initialCenter, animationMode, animationDuration, flyTo,
easeTo.

In `@src/components/maps/static-map.tsx`:
- Around line 30-36: The current truthiness check in the StaticMap component
(variables latitude and longitude) uses "!latitude || !longitude", which treats
0 as falsy and hides valid coordinates; change the guard to explicitly check for
null/undefined (e.g., latitude == null || longitude == null or typeof latitude
=== 'undefined' || latitude === null || ... ) so that 0 is accepted as a valid
coordinate and only missing values trigger the "no_location" fallback.
- Line 40: Remove the non-API props initialCenter and initialZoom from the
Mapbox.MapView invocation (currently passing initialCenter={[longitude,
latitude]} and initialZoom={zoom}); these props are not supported by
`@rnmapbox/maps` and cause inconsistency with the existing Mapbox.Camera usage—use
Mapbox.Camera alone to control position/zoom across native and web; keep the
existing mapStyle, styles.map/height, logoEnabled, attributionEnabled,
compassEnabled, zoomEnabled, and rotateEnabled props intact and ensure Camera is
configured with longitude, latitude and zoom.

In `@src/components/notes/note-details-sheet.tsx`:
- Around line 66-72: Guard against null/undefined selectedNote.Body before
passing it to HtmlRenderer by using a safe fallback (e.g. selectedNote?.Body ??
'' or String(selectedNote?.Body || '')) so HtmlRenderer never receives
undefined/null; update the HtmlRenderer prop in the Note details sheet component
(where HtmlRenderer is rendered with html={selectedNote.Body}) to use the
fallback expression instead.

In `@src/components/protocols/protocol-details-sheet.tsx`:
- Around line 87-95: HtmlRenderer is being passed selectedProtocol.ProtocolText
which can be null/undefined and will cause the iframe body to render the literal
"undefined"/"null"; fix by ensuring the prop is a safe string (e.g., pass an
empty string when missing) or conditionally render the HtmlRenderer only when
ProtocolText is present. Update the call site around HtmlRenderer (the component
using selectedProtocol.ProtocolText and selectedProtocolId) to either use a
default like selectedProtocol.ProtocolText || '' or wrap the HtmlRenderer render
in a guard that verifies selectedProtocol.ProtocolText before rendering.

In `@src/components/ui/html-renderer/index.web.tsx`:
- Around line 109-120: The message handler handleMessage must validate the
message origin/source before acting; update handleMessage to only accept link
messages where event.data?.type === 'html-renderer-link' && event.data.url and
event.source === iframeRef.current?.contentWindow (or additionally check
event.origin if you need origin verification) so only the trusted iframe
triggers onLinkPress or Linking.openURL; reference iframeRef, onLinkPress, and
Linking.openURL in the updated logic and keep the rest of the callback
unchanged.
- Line 135: The inline ESLint disable comment "// eslint-disable-next-line
react-native/no-inline-styles" in the iframe render of the HtmlRenderer web
component is referencing a rule that isn't configured and breaks CI; remove that
invalid disable comment in src/components/ui/html-renderer/index.web.tsx (the
iframe element inside the HtmlRenderer/HtmlRendererWeb render function) and
either move the inline style into a named constant (e.g. iframeStyle) or, if you
must suppress a linter, replace it with a valid web-specific rule disable that
exists in our config; ensure the change targets the iframe render location and
that no invalid eslint-disable lines remain.
🧹 Nitpick comments (13)
src/components/ui/shared-tabs.tsx (1)

17-20: Shared global zustand store will cause cross-instance state conflicts.

useTabStore is a module-level singleton. If two or more SharedTabs instances on the same screen both use the onChange prop, they'll read and write the same activeIndex, causing tabs to sync incorrectly across unrelated tab groups. This is pre-existing, but since this file is being touched, it's worth noting.

Consider either removing the global store in favor of local state only, or creating a store per instance (e.g., via useRef(() => create(...))).

Also applies to: 58-59, 65-65

src/components/maps/map-view.web.tsx (2)

306-316: minHeight fallback silently overrides intentional zero-height.

style?.height || style?.minHeight || 100 uses falsy coercion, so a deliberate height: 0 from a parent would still produce minHeight: 100. This is likely acceptable for the map use-case (zero-height map is never useful), but style?.height ?? style?.minHeight ?? 100 would be more precise if numeric zero ever needs to propagate.


593-619: Dual cleanup returns in UserLocation effect are correct but fragile.

The if (map.loaded()) branch falls through to the cleanup at line 613, while the else branch returns its own cleanup at line 603. Both paths correctly remove the geolocate control. However, this structure is easy to misread and could be broken by future edits.

A single cleanup return at the end would be clearer:

Suggested simplification
     if (map.loaded()) {
       geolocate.trigger();
     } else {
-      const onMapLoad = () => {
-        geolocate.trigger();
-      };
+      const onMapLoad = () => geolocate.trigger();
       map.on('load', onMapLoad);
-
-      return () => {
-        try {
-          map.off('load', onMapLoad);
-          map.removeControl(geolocate);
-        } catch {
-          // map may already be destroyed during route transitions
-        }
-      };
     }
 
     return () => {
       try {
+        map.off?.('load', undefined); // no-op if no listener
         map.removeControl(geolocate);
       } catch {
         // map may already be destroyed during route transitions
       }
     };

Alternatively, store onMapLoad in a ref or local variable and conditionally clean it up in a single return block.

src/components/contacts/__tests__/contact-details-sheet.test.tsx (1)

7-14: Stale testID "mock-webview" in the HtmlRenderer mock.

The mock renders with testID="mock-webview" which is a leftover from the WebView era. While no test in this file queries it, it's inconsistent with the migration and could mislead future test authors. Consider renaming to "html-renderer" for consistency with other test files in this PR.

Proposed fix
-    HtmlRenderer: (props: any) => <View testID="mock-webview" {...props} />,
+    HtmlRenderer: (props: any) => <View testID="html-renderer" {...props} />,
src/components/contacts/contact-notes-list.tsx (2)

85-152: Consider memoizing the customCSS string.

The large customCSS template literal is recreated on every render of ContactNoteCard. Since it only depends on colorScheme, wrapping it in useMemo would avoid unnecessary string allocations, especially when rendering multiple cards in a list.


90-90: Redundant onLinkPress — HtmlRenderer already defaults to Linking.openURL.

The HtmlRenderer component defaults its link handler to Linking.openURL when onLinkPress is not provided (see index.tsx line ~93). Passing (url) => Linking.openURL(url) is functionally identical but creates a new closure each render and enables onShouldStartLoadWithRequest/navigation interception logic unnecessarily.

You can either omit onLinkPress entirely (to use the default) or keep it if you want the explicit link-interception behavior on native.

src/components/protocols/__tests__/protocol-details-sheet.test.tsx (1)

28-63: Overly detailed mock couples tests to HtmlRenderer internals.

This mock replicates the full HTML document structure (DOCTYPE, viewport meta, CSS styles) that the real HtmlRenderer produces. Tests in the "WebView Content" describe block (lines 281-314) then assert on those mock-generated strings — effectively testing the mock, not the component under test.

A simpler mock (like the one in contact-notes-list.test.tsx) that just exposes the html prop would make these tests less brittle while still verifying the correct content is passed through.

src/components/ui/card/index.web.tsx (1)

9-11: StyleSheet.flatten already handles non-array inputs — the Array.isArray guard is redundant.

StyleSheet.flatten accepts both single styles and arrays, so you can simplify:

Simplification
-  const flatStyle = Array.isArray(style) ? StyleSheet.flatten(style) : style;
+  const flatStyle = StyleSheet.flatten(style);
src/components/ui/html-renderer/index.web.tsx (1)

55-65: linkScript is computed on every render but referenced inside useMemo deps — consider memoizing it.

linkScript is derived only from onLinkPress truthiness, yet it's a string that's recreated every render, causing fullHtml to recompute even when nothing meaningful changed. You could fold the script directly into the useMemo body (keying on !!onLinkPress instead of linkScript) to avoid unnecessary recalculations.

Suggested simplification
- const linkScript = onLinkPress
-   ? `<script>...</script>`
-   : '';
-
  const fullHtml = useMemo(
-   () => `...${html}${linkScript}</body>...`,
-   [html, scrollEnabled, showsVerticalScrollIndicator, resolvedTextColor, resolvedBgColor, customCSS, linkScript]
+   () => {
+     const script = onLinkPress
+       ? `<script>
+           document.addEventListener('click', function(e) {
+             var anchor = e.target.closest('a');
+             if (anchor && anchor.href) {
+               e.preventDefault();
+               window.parent.postMessage({ type: 'html-renderer-link', url: anchor.href }, '*');
+             }
+           });
+         </script>`
+       : '';
+     return `...${html}${script}</body>...`;
+   },
+   // eslint-disable-next-line react-hooks/exhaustive-deps
+   [html, scrollEnabled, showsVerticalScrollIndicator, resolvedTextColor, resolvedBgColor, customCSS, !!onLinkPress]
  );

Also applies to: 102-102

src/components/ui/html-renderer/index.tsx (4)

65-88: fullHtml is not memoized — inconsistent with the web variant and rebuilt every render.

The web variant wraps this template in useMemo. The native variant should do the same to avoid handing a new source object to WebView on every render, which can cause unnecessary re-renders or flickers.

Proposed fix
+ import React, { useMemo } from 'react';
  ...
- const fullHtml = `
+ const fullHtml = useMemo(() => `
    <!DOCTYPE html>
    <html>
      ...
      <body>${html}</body>
    </html>
- `;
+ `, [html, resolvedTextColor, resolvedBgColor, customCSS]);

90-118: Inline anonymous functions are recreated every render — wrap with useCallback.

handleLinkPress, onShouldStartLoadWithRequest, and onNavigationStateChange are all recreated on each render. Wrapping them in useCallback (or extracting them) would avoid unnecessary WebView prop changes. As per coding guidelines: "Avoid anonymous functions in … event handlers to prevent re-renders."

Proposed fix sketch
+ import { useCallback } from 'react';
  ...
- const handleLinkPress = onLinkPress ?? ((url: string) => Linking.openURL(url));
+ const handleLinkPress = useCallback(
+   (url: string) => {
+     if (onLinkPress) { onLinkPress(url); }
+     else { Linking.openURL(url); }
+   },
+   [onLinkPress]
+ );
+
+ const handleShouldStartLoad = useCallback(
+   (request: { url: string }) => {
+     if (request.url.startsWith('about:') || request.url.startsWith('data:')) return true;
+     handleLinkPress(request.url);
+     return false;
+   },
+   [handleLinkPress]
+ );
+
+ const handleNavStateChange = useCallback(
+   (navState: { url: string }) => {
+     if (navState.url && !navState.url.startsWith('about:') && !navState.url.startsWith('data:')) {
+       handleLinkPress(navState.url);
+     }
+   },
+   [handleLinkPress]
+ );

6-10: THEME_COLORS and HtmlRendererProps are duplicated between the native and web variants.

Both index.tsx and index.web.tsx define identical THEME_COLORS and HtmlRendererProps. Consider extracting them into a shared module (e.g. src/components/ui/html-renderer/types.ts and src/components/ui/html-renderer/constants.ts) so changes only need to happen in one place.

Also applies to: 12-36


86-86: Consider adding HTML sanitization for defense-in-depth security.

While the current implementation mitigates XSS risk by disabling JavaScript by default and using only trusted backend data sources, the HTML string is interpolated directly without sanitization. To follow defense-in-depth principles and protect against potential data corruption at the API layer, consider using a sanitization library (e.g., DOMPurify via isomorphic-dompurify) on the html prop before interpolation. Alternatively, document that the html prop accepts only trusted content from vetted backend sources.

Comment on lines 386 to 421
useEffect(() => {
if (!map) return;

if (centerCoordinate) {
map.easeTo(
{
center: centerCoordinate,
zoom: zoomLevel,
bearing: heading,
pitch: pitch,
duration: animationDuration,
},
{ _programmatic: true }
);
if (
centerCoordinate &&
centerCoordinate.length === 2 &&
isFinite(centerCoordinate[0]) &&
isFinite(centerCoordinate[1])
) {
// Skip the first render — the MapView already initialized at the correct
// position via initialCenter/initialZoom, so no programmatic move needed.
if (!hasInitialized.current) {
hasInitialized.current = true;
return;
}

// For subsequent coordinate/zoom changes, animate to the new position.
const cameraOptions = {
center: centerCoordinate as [number, number],
zoom: zoomLevel,
bearing: heading,
pitch: pitch,
duration: animationDuration,
};

try {
if (animationMode === 'flyTo') {
map.flyTo(cameraOptions, { _programmatic: true });
} else {
map.easeTo(cameraOptions, { _programmatic: true });
}
} catch {
// Suppress projection-matrix errors during resize/transition
}
}
}, [map, centerCoordinate, zoomLevel, heading, pitch, animationDuration]);
}, [map, centerCoordinate?.[0], centerCoordinate?.[1], zoomLevel, heading, pitch, animationDuration, animationMode]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f \( -name "*.tsx" -o -name "*.ts" \) | head -20

Repository: Resgrid/Unit

Length of output: 753


🏁 Script executed:

rg -n 'initialCenter' --type=tsx --type=ts -C2

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

rg -n '<MapView' --type=tsx --type=ts -A15

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

rg -n '<Camera' --type=tsx --type=ts -B3 -A10

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

fd -e tsx -e ts | head -30

Repository: Resgrid/Unit

Length of output: 981


🏁 Script executed:

rg 'initialCenter' -C2

Repository: Resgrid/Unit

Length of output: 2663


🏁 Script executed:

rg '<MapView' -A15

Repository: Resgrid/Unit

Length of output: 38


🏁 Script executed:

rg '<Camera' -B3 -A10

Repository: Resgrid/Unit

Length of output: 1331


🏁 Script executed:

cat -n src/components/maps/map-view.web.tsx | sed -n '380,425p'

Repository: Resgrid/Unit

Length of output: 1686


🏁 Script executed:

rg 'map-view.web' -A5 -B2

Repository: Resgrid/Unit

Length of output: 50369


🏁 Script executed:

rg 'from.*map-view' -A5

Repository: Resgrid/Unit

Length of output: 38


🏁 Script executed:

rg 'MapView' src/components -B2 -A15 | head -120

Repository: Resgrid/Unit

Length of output: 9290


🏁 Script executed:

cat -n src/components/maps/map-view.web.tsx | sed -n '1,100p'

Repository: Resgrid/Unit

Length of output: 3849


🏁 Script executed:

rg '<MapView' -B2 -A20 | head -200

Repository: Resgrid/Unit

Length of output: 38


🏁 Script executed:

rg 'import.*MapView' -B1 -A5

Repository: Resgrid/Unit

Length of output: 38


🏁 Script executed:

fd -name "*.tsx" -exec grep -l "MapView\|Camera" {} \; | head -20

Repository: Resgrid/Unit

Length of output: 286


🏁 Script executed:

rg 'Mapbox\.MapView' -B2 -A15

Repository: Resgrid/Unit

Length of output: 16333


🏁 Script executed:

rg 'Mapbox\.Camera' -B2 -A10

Repository: Resgrid/Unit

Length of output: 6790


🏁 Script executed:

rg 'from.*mapbox'

Repository: Resgrid/Unit

Length of output: 709


🏁 Script executed:

fd -name "*.tsx" -exec grep -H "MapView\|Camera" {} \;

Repository: Resgrid/Unit

Length of output: 286


🏁 Script executed:

cat -n src/app/\(app\)/index.tsx | sed -n '1,150p'

Repository: Resgrid/Unit

Length of output: 6982


🏁 Script executed:

cat -n src/app/\(app\)/index.tsx | sed -n '150,250p'

Repository: Resgrid/Unit

Length of output: 3904


🏁 Script executed:

cat -n src/components/maps/map-view.web.tsx | sed -n '200,280p'

Repository: Resgrid/Unit

Length of output: 3894


🏁 Script executed:

cat -n src/components/maps/map-view.web.tsx | sed -n '280,360p'

Repository: Resgrid/Unit

Length of output: 2951


🏁 Script executed:

cat -n src/components/maps/map-view.web.tsx | sed -n '340,430p'

Repository: Resgrid/Unit

Length of output: 3530


Camera effect skips first render when MapView lacks initialCenter—map may render at default US location.

The hasInitialized flag skips the first centerCoordinate update unconditionally, assuming MapView initialized via initialCenter. However, in location-picker.tsx, full-screen-location-picker.tsx, and the main map screen (src/app/(app)/index.tsx), MapView is used without initialCenter while Camera provides centerCoordinate. On first render, the Camera effect returns early (line 399) and never moves the map, leaving it at the default US center [-98.5795, 39.8283] until a subsequent update triggers the camera move.

Either pass initialCenter to all MapView instances that use Camera, or track whether initialCenter was provided and skip the first render only when it was explicitly set.

🤖 Prompt for AI Agents
In `@src/components/maps/map-view.web.tsx` around lines 386 - 421, The effect
currently unconditionally skips the first centerCoordinate update via
hasInitialized, which prevents maps without an initialCenter prop from being
moved on first render; change the logic in the useEffect inside map-view.web.tsx
so you only return early when an initialCenter was actually provided to the
MapView. Concretely, detect whether initialCenter (or a boolean like
initialCenterProvided) exists, then update the block around
hasInitialized.current to set hasInitialized.current = true and only return if
initialCenter was provided; otherwise allow the subsequent camera flyTo/easeTo
to run on the first render. Reference: hasInitialized, useEffect, map,
centerCoordinate, initialCenter, animationMode, animationDuration, flyTo,
easeTo.

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

🤖 Fix all issues with AI agents
In `@src/components/maps/map-view.web.tsx`:
- Around line 304-316: The minHeight fallback currently uses truthiness
(style?.height || style?.minHeight || 100) which treats 0 as falsy and will
override an intentional height: 0; change the fallback to use nullish coalescing
so only null/undefined are replaced (e.g., use style?.height ?? style?.minHeight
?? 100) in the JSX where mapContainer/testID/style are used to ensure explicit
zero heights are preserved.
🧹 Nitpick comments (8)
src/components/ui/vstack/index.web.tsx (1)

7-7: Minor inconsistency: ComponentProps vs ComponentPropsWithoutRef.

The other sibling components (HStack, Center, Card) use React.ComponentPropsWithoutRef<'div'>, but VStack uses React.ComponentProps<'div'>. Since forwardRef handles ref separately, ComponentPropsWithoutRef is the more precise choice and avoids a duplicate ref in the props type.

Suggested fix
-type IVStackProps = React.ComponentProps<'div'> & VariantProps<typeof vstackStyle> & { style?: StyleProp<ViewStyle> };
+type IVStackProps = React.ComponentPropsWithoutRef<'div'> & VariantProps<typeof vstackStyle> & { style?: StyleProp<ViewStyle> };
src/components/maps/static-map.tsx (1)

51-51: Use ternary operator for conditional rendering instead of &&.

Per coding guidelines, prefer ? : over && for conditional rendering.

Proposed fix
-        {showUserLocation && <Mapbox.UserLocation visible={true} showsUserHeadingIndicator={true} />}
+        {showUserLocation ? <Mapbox.UserLocation visible={true} showsUserHeadingIndicator={true} /> : null}

As per coding guidelines, "Use ternary operator ? : for conditional rendering instead of &&".

src/components/maps/map-view.web.tsx (3)

178-240: Monkey-patching mapbox-gl internals is fragile but pragmatic.

The patches to unproject, easeTo, flyTo, and _render are reasonable workarounds for real mapbox-gl edge cases. However, these are tightly coupled to mapbox-gl v3 internals (e.g., _render, canvas size checks). Any mapbox-gl upgrade could break these patches silently.

Consider adding a comment near the top of the init block documenting which mapbox-gl version these patches target (3.18.1), so future upgraders know to re-verify.


122-267: Map initialization effect depends only on [hasSize] — prop changes to styleURL, initialCenter, etc. won't reinitialize.

The dependency array is [hasSize], meaning changes to styleURL, initialCenter, initialZoom, or callback props after mount won't recreate the map. styleURL is handled separately (line 298-302), but initialCenter/initialZoom are only read during construction. This is probably fine since Camera handles subsequent moves, but it's worth noting that initialCenter/initialZoom are truly "initial-only" props — document this in the interface JSDoc or prop names (e.g., defaultCenter).


18-18: Multiple any types throughout the file.

MapContext is typed as any, map ref is any, and several function parameters use any. The coding guidelines say to avoid any and strive for precise types. While a full typing overhaul is out of scope for this bug-fix PR, consider creating a type MapInstance = mapboxgl.Map alias and using it for the context and ref to incrementally improve type safety.

As per coding guidelines, "Avoid using any; strive for precise types".

Also applies to: 86-86

src/components/ui/html-renderer/index.web.tsx (1)

55-106: linkScript defeats useMemo — it is a new string reference every render.

linkScript is recreated on every render when onLinkPress is truthy (it's a plain template literal, not memoized). Because it appears in the useMemo dependency array for fullHtml (line 105), fullHtml will be recomputed every render even though the string content is identical.

Move the script construction inside the useMemo callback or memoize linkScript separately with useMemo.

♻️ Suggested fix — move linkScript inside fullHtml's useMemo
- const linkScript = onLinkPress
-   ? `<script>
-       document.addEventListener('click', function(e) {
-         var anchor = e.target.closest('a');
-         if (anchor && anchor.href) {
-           e.preventDefault();
-           window.parent.postMessage({ type: 'html-renderer-link', url: anchor.href }, '*');
-         }
-       });
-     </script>`
-   : '';
-
  const fullHtml = useMemo(
-   () => `
+   () => {
+     const linkScript = onLinkPress
+       ? `<script>
+           document.addEventListener('click', function(e) {
+             var anchor = e.target.closest('a');
+             if (anchor && anchor.href) {
+               e.preventDefault();
+               window.parent.postMessage({ type: 'html-renderer-link', url: anchor.href }, '*');
+             }
+           });
+         </script>`
+       : '';
+     return `
     <!DOCTYPE html>
     ...
     <body>${html}${linkScript}</body>
     </html>
-  `,
-   [html, scrollEnabled, showsVerticalScrollIndicator, resolvedTextColor, resolvedBgColor, customCSS, linkScript]
+  `;
+   },
+   [html, scrollEnabled, showsVerticalScrollIndicator, resolvedTextColor, resolvedBgColor, customCSS, onLinkPress]
  );
src/components/calls/call-card.tsx (1)

102-106: Use ternary operator for conditional rendering instead of &&.

Per coding guidelines, prefer ? : over && for conditional rendering.

♻️ Suggested fix
-      {call.Nature && (
+      {call.Nature ? (
         <Box className="mt-4 rounded-lg bg-white/50 p-3">
           <HtmlRenderer html={call.Nature} style={StyleSheet.flatten([styles.container, { height: 80 }])} textColor={textColor} />
         </Box>
-      )}
+      ) : null}

As per coding guidelines, "Use ternary operator ? : for conditional rendering instead of &&".

src/components/contacts/contact-notes-list.tsx (1)

84-151: Consider extracting the onLinkPress handler and the customCSS block.

  1. The anonymous arrow function (url) => Linking.openURL(url) on line 89 creates a new reference each render, which will cause HtmlRenderer to re-render (and on web, re-attach the message listener). Extract it to a stable callback at the component level.

  2. The large customCSS template string (60+ lines) dominates the JSX and could be extracted to a module-level constant or a useMemo (since it depends on colorScheme).

♻️ Suggested extraction
 const ContactNoteCard: React.FC<ContactNoteCardProps> = ({ note }) => {
   const { t } = useTranslation();
   ...
   const { colorScheme } = useColorScheme();
+
+  const handleLinkPress = React.useCallback((url: string) => {
+    Linking.openURL(url);
+  }, []);
+
+  const noteCustomCSS = React.useMemo(() => `
+    html, body { width: 100%; height: auto; min-height: 100%; ... }
+    a { color: ${colorScheme === 'dark' ? '#60A5FA' : '#3B82F6'}; ... }
+    ...
+  `, [colorScheme]);

   ...
           <HtmlRenderer
             html={noteContent}
             style={styles.webView}
             scrollEnabled={true}
             showsVerticalScrollIndicator={true}
-            onLinkPress={(url) => Linking.openURL(url)}
-            customCSS={`...`}
+            onLinkPress={handleLinkPress}
+            customCSS={noteCustomCSS}
           />

Comment on lines 304 to +316
return (
<div ref={mapContainer} data-testid={testID} style={{ width: '100%', height: '100%', ...style }}>
<div
ref={mapContainer}
data-testid={testID}
style={{
width: '100%',
height: '100%',
position: 'relative',
...style,
// Ensure the container is never zero-height on web
minHeight: style?.height || style?.minHeight || 100,
}}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

minHeight fallback uses truthiness — height: 0 silently becomes 100.

style?.height || style?.minHeight || 100 treats 0 as falsy. If a consumer intentionally passes height: 0 (e.g., to collapse the map), it will be overridden to 100. While this is likely rare and the intent here is to prevent the zero-size crash, a nullish coalesce would be more precise:

Proposed fix
-          minHeight: style?.height || style?.minHeight || 100,
+          minHeight: style?.height ?? style?.minHeight ?? 100,
📝 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
return (
<div ref={mapContainer} data-testid={testID} style={{ width: '100%', height: '100%', ...style }}>
<div
ref={mapContainer}
data-testid={testID}
style={{
width: '100%',
height: '100%',
position: 'relative',
...style,
// Ensure the container is never zero-height on web
minHeight: style?.height || style?.minHeight || 100,
}}
>
return (
<div
ref={mapContainer}
data-testid={testID}
style={{
width: '100%',
height: '100%',
position: 'relative',
...style,
// Ensure the container is never zero-height on web
minHeight: style?.height ?? style?.minHeight ?? 100,
}}
>
🤖 Prompt for AI Agents
In `@src/components/maps/map-view.web.tsx` around lines 304 - 316, The minHeight
fallback currently uses truthiness (style?.height || style?.minHeight || 100)
which treats 0 as falsy and will override an intentional height: 0; change the
fallback to use nullish coalescing so only null/undefined are replaced (e.g.,
use style?.height ?? style?.minHeight ?? 100) in the JSX where
mapContainer/testID/style are used to ensure explicit zero heights are
preserved.

@ucswift
Copy link
Member Author

ucswift commented Feb 16, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit a3ebe66 into master Feb 16, 2026
18 of 19 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.

1 participant