Skip to content

Commit a3ebe66

Browse files
authored
Merge pull request #218 from Resgrid/develop
RU-T47 Fix html content render, fixing mapbox issue on call view.
2 parents bce67a3 + e06894f commit a3ebe66

23 files changed

+916
-453
lines changed

src/app/(app)/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,11 +503,11 @@ const styles = StyleSheet.create({
503503
markerOuterRingPulseWeb:
504504
Platform.OS === 'web'
505505
? {
506-
// @ts-ignore — web-only CSS animation properties
507-
animationName: 'pulse-ring',
508-
animationDuration: '2s',
509-
animationIterationCount: 'infinite',
510-
animationTimingFunction: 'ease-in-out',
511-
}
506+
// @ts-ignore — web-only CSS animation properties
507+
animationName: 'pulse-ring',
508+
animationDuration: '2s',
509+
animationIterationCount: 'infinite',
510+
animationTimingFunction: 'ease-in-out',
511+
}
512512
: ({} as any),
513513
});

src/app/call/[id].tsx

Lines changed: 30 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { useColorScheme } from 'nativewind';
55
import React, { useEffect, useState } from 'react';
66
import { useTranslation } from 'react-i18next';
77
import { ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
8-
import WebView from 'react-native-webview';
98

109
import { Loading } from '@/components/common/loading';
1110
import ZeroState from '@/components/common/zero-state';
@@ -16,6 +15,7 @@ import { Box } from '@/components/ui/box';
1615
import { Button, ButtonIcon, ButtonText } from '@/components/ui/button';
1716
import { Heading } from '@/components/ui/heading';
1817
import { HStack } from '@/components/ui/hstack';
18+
import { HtmlRenderer } from '@/components/ui/html-renderer';
1919
import { SharedTabs, type TabItem } from '@/components/ui/shared-tabs';
2020
import { Text } from '@/components/ui/text';
2121
import { VStack } from '@/components/ui/vstack';
@@ -145,17 +145,26 @@ export default function CallDetail() {
145145

146146
useEffect(() => {
147147
if (call) {
148+
// Try Latitude/Longitude first, but validate they are real coordinates
148149
if (call.Latitude && call.Longitude) {
149-
setCoordinates({
150-
latitude: parseFloat(call.Latitude),
151-
longitude: parseFloat(call.Longitude),
152-
});
153-
} else if (call.Geolocation) {
154-
const [lat, lng] = call.Geolocation.split(',');
155-
setCoordinates({
156-
latitude: parseFloat(lat),
157-
longitude: parseFloat(lng),
158-
});
150+
const lat = parseFloat(call.Latitude);
151+
const lng = parseFloat(call.Longitude);
152+
if (!isNaN(lat) && !isNaN(lng) && (lat !== 0 || lng !== 0)) {
153+
setCoordinates({ latitude: lat, longitude: lng });
154+
return;
155+
}
156+
}
157+
158+
// Fall through to Geolocation if Latitude/Longitude are missing or invalid
159+
if (call.Geolocation) {
160+
const parts = call.Geolocation.split(',');
161+
if (parts.length === 2) {
162+
const lat = parseFloat(parts[0].trim());
163+
const lng = parseFloat(parts[1].trim());
164+
if (!isNaN(lat) && !isNaN(lng)) {
165+
setCoordinates({ latitude: lat, longitude: lng });
166+
}
167+
}
159168
}
160169
}
161170
}, [call]);
@@ -186,7 +195,7 @@ export default function CallDetail() {
186195
* Opens the device's native maps application with directions to the call location
187196
*/
188197
const handleRoute = async () => {
189-
if (!coordinates.latitude || !coordinates.longitude) {
198+
if (coordinates.latitude === null || coordinates.longitude === null) {
190199
showToast('error', t('call_detail.no_location_for_routing'));
191200
return;
192201
}
@@ -300,37 +309,7 @@ export default function CallDetail() {
300309
<Box className="border-b border-outline-100 pb-2">
301310
<Text className="text-sm text-gray-500">{t('call_detail.note')}</Text>
302311
<Box>
303-
<WebView
304-
style={[styles.container, { height: 200 }]}
305-
originWhitelist={['*']}
306-
scrollEnabled={false}
307-
showsVerticalScrollIndicator={false}
308-
source={{
309-
html: `
310-
<!DOCTYPE html>
311-
<html>
312-
<head>
313-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
314-
<style>
315-
body {
316-
color: ${textColor};
317-
font-family: system-ui, -apple-system, sans-serif;
318-
margin: 0;
319-
padding: 0;
320-
font-size: 16px;
321-
line-height: 1.5;
322-
}
323-
* {
324-
max-width: 100%;
325-
}
326-
</style>
327-
</head>
328-
<body>${call.Note}</body>
329-
</html>
330-
`,
331-
}}
332-
androidLayerType="software"
333-
/>
312+
<HtmlRenderer html={call.Note ?? ''} style={StyleSheet.flatten([styles.container, { height: 200 }])} />
334313
</Box>
335314
</Box>
336315
</VStack>
@@ -377,37 +356,7 @@ export default function CallDetail() {
377356
<Text className="font-semibold">{protocol.Name}</Text>
378357
<Text className="text-sm text-gray-600">{protocol.Description}</Text>
379358
<Box>
380-
<WebView
381-
style={[styles.container, { height: 200 }]}
382-
originWhitelist={['*']}
383-
scrollEnabled={false}
384-
showsVerticalScrollIndicator={false}
385-
source={{
386-
html: `
387-
<!DOCTYPE html>
388-
<html>
389-
<head>
390-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
391-
<style>
392-
body {
393-
color: ${textColor};
394-
font-family: system-ui, -apple-system, sans-serif;
395-
margin: 0;
396-
padding: 0;
397-
font-size: 16px;
398-
line-height: 1.5;
399-
}
400-
* {
401-
max-width: 100%;
402-
}
403-
</style>
404-
</head>
405-
<body>${protocol.ProtocolText}</body>
406-
</html>
407-
`,
408-
}}
409-
androidLayerType="software"
410-
/>
359+
<HtmlRenderer html={protocol.ProtocolText ?? ''} style={StyleSheet.flatten([styles.container, { height: 200 }])} />
411360
</Box>
412361
</Box>
413362
))}
@@ -506,45 +455,17 @@ export default function CallDetail() {
506455
</HStack>
507456
<VStack className="space-y-1">
508457
<Box style={{ height: 80 }}>
509-
<WebView
510-
style={[styles.container, { height: 80 }]}
511-
originWhitelist={['*']}
512-
scrollEnabled={false}
513-
showsVerticalScrollIndicator={false}
514-
source={{
515-
html: `
516-
<!DOCTYPE html>
517-
<html>
518-
<head>
519-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
520-
<style>
521-
body {
522-
color: ${textColor};
523-
font-family: system-ui, -apple-system, sans-serif;
524-
margin: 0;
525-
padding: 0;
526-
font-size: 16px;
527-
line-height: 1.5;
528-
}
529-
* {
530-
max-width: 100%;
531-
}
532-
</style>
533-
</head>
534-
<body>${call.Nature}</body>
535-
</html>
536-
`,
537-
}}
538-
androidLayerType="software"
539-
/>
458+
<HtmlRenderer html={call.Nature ?? ''} style={StyleSheet.flatten([styles.container, { height: 80 }])} />
540459
</Box>
541460
</VStack>
542461
</Box>
543462

544-
{/* Map */}
545-
<Box className="w-full">
546-
{coordinates.latitude && coordinates.longitude ? <StaticMap latitude={coordinates.latitude} longitude={coordinates.longitude} address={call.Address} zoom={15} height={200} showUserLocation={true} /> : null}
547-
</Box>
463+
{/* Map - only show when valid coordinates exist */}
464+
{coordinates.latitude !== null && coordinates.longitude !== null ? (
465+
<Box className="w-full">
466+
<StaticMap latitude={coordinates.latitude} longitude={coordinates.longitude} address={call.Address} zoom={15} height={200} showUserLocation={true} />
467+
</Box>
468+
) : null}
548469

549470
{/* Action Buttons */}
550471
<HStack className={`justify-around p-4 shadow-sm ${colorScheme === 'dark' ? 'bg-neutral-900' : 'bg-neutral-100'}`}>

src/app/call/__tests__/[id].security.test.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ jest.mock('react-native', () => ({
99
ScrollView: ({ children, ...props }: any) => <div {...props}>{children}</div>,
1010
StyleSheet: {
1111
create: (styles: any) => styles,
12+
flatten: (styles: any) => {
13+
if (Array.isArray(styles)) {
14+
return Object.assign({}, ...styles.filter(Boolean));
15+
}
16+
return styles || {};
17+
},
1218
},
1319
useWindowDimensions: () => ({ width: 375, height: 812 }),
1420
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
@@ -273,10 +279,10 @@ jest.mock('@/lib/navigation', () => ({
273279
openMapsWithDirections: jest.fn().mockResolvedValue(true),
274280
}));
275281

276-
// Mock WebView
277-
jest.mock('react-native-webview', () => ({
282+
// Mock HtmlRenderer
283+
jest.mock('@/components/ui/html-renderer', () => ({
278284
__esModule: true,
279-
default: () => <div data-testid="webview">WebView</div>,
285+
HtmlRenderer: () => <div data-testid="html-renderer">HtmlRenderer</div>,
280286
}));
281287

282288
// Mock date-fns
@@ -317,45 +323,45 @@ describe('CallDetail', () => {
317323

318324
beforeEach(() => {
319325
jest.clearAllMocks();
320-
326+
321327
// Setup stores as selector-based stores
322328
useCallDetailStore.mockImplementation((selector: any) => {
323329
if (selector) {
324330
return selector(mockCallDetailStore);
325331
}
326332
return mockCallDetailStore;
327333
});
328-
334+
329335
useSecurityStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector(mockSecurityStore) : mockSecurityStore);
330-
336+
331337
useCoreStore.mockImplementation((selector: any) => {
332338
if (selector) {
333339
return selector(mockCoreStore);
334340
}
335341
return mockCoreStore;
336342
});
337-
343+
338344
useLocationStore.mockImplementation((selector: any) => {
339345
if (selector) {
340346
return selector(mockLocationStore);
341347
}
342348
return mockLocationStore;
343349
});
344-
350+
345351
useStatusBottomSheetStore.mockImplementation((selector: any) => {
346352
if (selector) {
347353
return selector(mockStatusBottomSheetStore);
348354
}
349355
return mockStatusBottomSheetStore;
350356
});
351-
357+
352358
useToastStore.mockImplementation((selector: any) => {
353359
if (selector) {
354360
return selector(mockToastStore);
355361
}
356362
return mockToastStore;
357363
});
358-
364+
359365
// Setup securityStore as a selector-based store
360366
securityStore.mockImplementation((selector: any) => {
361367
const state = {

src/app/call/__tests__/[id].test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ jest.mock('date-fns', () => ({
197197
format: jest.fn(() => '2024-01-01 12:00'),
198198
}));
199199

200-
// Mock react-native-webview
201-
jest.mock('react-native-webview', () => ({
200+
// Mock HtmlRenderer
201+
jest.mock('@/components/ui/html-renderer', () => ({
202202
__esModule: true,
203-
default: 'WebView',
203+
HtmlRenderer: 'HtmlRenderer',
204204
}));
205205

206206
jest.mock('@/hooks/use-analytics', () => ({

src/components/calls/call-card.tsx

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { AlertTriangle, MapPin, Phone } from 'lucide-react-native';
22
import React from 'react';
33
import { StyleSheet } from 'react-native';
4-
import WebView from 'react-native-webview';
54

65
import { Box } from '@/components/ui/box';
76
import { HStack } from '@/components/ui/hstack';
7+
import { HtmlRenderer } from '@/components/ui/html-renderer';
88
import { Icon } from '@/components/ui/icon';
99
import { Text } from '@/components/ui/text';
1010
import { VStack } from '@/components/ui/vstack';
@@ -101,37 +101,7 @@ export const CallCard: React.FC<CallCardProps> = ({ call, priority }) => {
101101
{/* Nature of Call */}
102102
{call.Nature && (
103103
<Box className="mt-4 rounded-lg bg-white/50 p-3">
104-
<WebView
105-
style={[styles.container, { height: 80 }]}
106-
originWhitelist={['*']}
107-
scrollEnabled={false}
108-
showsVerticalScrollIndicator={false}
109-
source={{
110-
html: `
111-
<!DOCTYPE html>
112-
<html>
113-
<head>
114-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
115-
<style>
116-
body {
117-
color: ${textColor};
118-
font-family: system-ui, -apple-system, sans-serif;
119-
margin: 0;
120-
padding: 0;
121-
font-size: 16px;
122-
line-height: 1.5;
123-
}
124-
* {
125-
max-width: 100%;
126-
}
127-
</style>
128-
</head>
129-
<body>${call.Nature}</body>
130-
</html>
131-
`,
132-
}}
133-
androidLayerType="software"
134-
/>
104+
<HtmlRenderer html={call.Nature} style={StyleSheet.flatten([styles.container, { height: 80 }])} textColor={textColor} />
135105
</Box>
136106
)}
137107
</Box>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import React from 'react';
44
import { ContactDetailsSheet } from '../contact-details-sheet';
55
import { ContactType, type ContactResultData } from '@/models/v4/contacts/contactResultData';
66

7-
// Mock react-native-webview
8-
jest.mock('react-native-webview', () => {
7+
// Mock HtmlRenderer
8+
jest.mock('@/components/ui/html-renderer', () => {
99
const { View } = require('react-native');
1010
return {
1111
__esModule: true,
12-
default: (props: any) => <View testID="mock-webview" {...props} />,
12+
HtmlRenderer: (props: any) => <View testID="mock-webview" {...props} />,
1313
};
1414
});
1515

0 commit comments

Comments
 (0)