diff --git a/src/components/calls/__tests__/call-notes-modal-new.test.tsx b/src/components/calls/__tests__/call-notes-modal-new.test.tsx
index ab584b7..20b6275 100644
--- a/src/components/calls/__tests__/call-notes-modal-new.test.tsx
+++ b/src/components/calls/__tests__/call-notes-modal-new.test.tsx
@@ -42,16 +42,24 @@ jest.mock('lucide-react-native', () => {
jest.mock('../../common/loading', () => ({
Loading: () => {
const { View, Text } = require('react-native');
- return Loading...;
+ return (
+
+ Loading...
+
+ );
},
}));
-// Mock ZeroState component
+// Mock ZeroState component
jest.mock('../../common/zero-state', () => ({
__esModule: true,
default: ({ heading }: { heading: string }) => {
const { View, Text } = require('react-native');
- return {heading};
+ return (
+
+ {heading}
+
+ );
},
}));
@@ -62,6 +70,10 @@ jest.mock('../../ui/focus-aware-status-bar', () => ({
// Mock react-native-keyboard-controller
jest.mock('react-native-keyboard-controller', () => ({
+ KeyboardProvider: ({ children }: any) => {
+ const { View } = require('react-native');
+ return {children};
+ },
KeyboardAwareScrollView: ({ children }: any) => {
const { View } = require('react-native');
return {children};
@@ -76,7 +88,11 @@ jest.mock('react-native-keyboard-controller', () => ({
jest.mock('react-native-gesture-handler', () => ({
ScrollView: ({ children, testID, ...props }: any) => {
const { ScrollView } = require('react-native');
- return {children};
+ return (
+
+ {children}
+
+ );
},
PanGestureHandler: ({ children }: any) => children,
State: {},
@@ -119,11 +135,7 @@ jest.mock('../../ui/button', () => ({
const { TouchableOpacity } = require('react-native');
const isButtonDisabled = disabled || isDisabled;
return (
-
+
{children}
);
@@ -242,15 +254,19 @@ describe('CallNotesModal', () => {
},
} as any);
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
- mockUseAuthStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector(mockAuthStore) : mockAuthStore);
+ mockUseAuthStore.mockImplementation((selector: any) => (typeof selector === 'function' ? selector(mockAuthStore) : mockAuthStore));
});
it('renders correctly when open', () => {
@@ -285,13 +301,17 @@ describe('CallNotesModal', () => {
it('handles search input correctly', () => {
const mockSearchNotes = jest.fn(() => [mockCallDetailStore.callNotes[0]]);
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- searchNotes: mockSearchNotes,
- }) : {
- ...mockCallDetailStore,
- searchNotes: mockSearchNotes,
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ searchNotes: mockSearchNotes,
+ })
+ : {
+ ...mockCallDetailStore,
+ searchNotes: mockSearchNotes,
+ }
+ );
const { getByPlaceholderText, getByText, queryByText } = render();
@@ -304,13 +324,17 @@ describe('CallNotesModal', () => {
});
it('shows loading state correctly', () => {
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- isNotesLoading: true,
- }) : {
- ...mockCallDetailStore,
- isNotesLoading: true,
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ isNotesLoading: true,
+ })
+ : {
+ ...mockCallDetailStore,
+ isNotesLoading: true,
+ }
+ );
const { getByTestId } = render();
@@ -318,15 +342,19 @@ describe('CallNotesModal', () => {
});
it('shows zero state when no notes found', () => {
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- callNotes: [],
- searchNotes: jest.fn(() => []),
- }) : {
- ...mockCallDetailStore,
- callNotes: [],
- searchNotes: jest.fn(() => []),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ callNotes: [],
+ searchNotes: jest.fn(() => []),
+ })
+ : {
+ ...mockCallDetailStore,
+ callNotes: [],
+ searchNotes: jest.fn(() => []),
+ }
+ );
const { getByText } = render();
@@ -335,15 +363,19 @@ describe('CallNotesModal', () => {
it('handles adding a new note', async () => {
const mockAddNote = jest.fn();
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
const { getByPlaceholderText, getByText } = render();
@@ -360,15 +392,19 @@ describe('CallNotesModal', () => {
it('disables add button when note input is empty', () => {
const mockAddNote = jest.fn();
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
const { getByText } = render();
@@ -382,17 +418,21 @@ describe('CallNotesModal', () => {
it('disables add button when loading', () => {
const mockAddNote = jest.fn();
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- addNote: mockAddNote,
- isNotesLoading: true,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- addNote: mockAddNote,
- isNotesLoading: true,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ isNotesLoading: true,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ isNotesLoading: true,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
const { getByText, getByPlaceholderText } = render();
@@ -417,15 +457,19 @@ describe('CallNotesModal', () => {
it('clears note input after successful submission', async () => {
const mockAddNote = jest.fn().mockResolvedValue(undefined);
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
const { getByPlaceholderText, getByText } = render();
@@ -442,15 +486,19 @@ describe('CallNotesModal', () => {
it('does not add empty note when only whitespace is entered', async () => {
const mockAddNote = jest.fn();
- mockUseCallDetailStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- }) : {
- ...mockCallDetailStore,
- addNote: mockAddNote,
- searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
- });
+ mockUseCallDetailStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ })
+ : {
+ ...mockCallDetailStore,
+ addNote: mockAddNote,
+ searchNotes: jest.fn(() => mockCallDetailStore.callNotes),
+ }
+ );
const { getByPlaceholderText, getByText } = render();
@@ -464,11 +512,15 @@ describe('CallNotesModal', () => {
});
it('handles missing user profile gracefully', () => {
- mockUseAuthStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({
- profile: null,
- }) : {
- profile: null,
- });
+ mockUseAuthStore.mockImplementation((selector: any) =>
+ typeof selector === 'function'
+ ? selector({
+ profile: null,
+ })
+ : {
+ profile: null,
+ }
+ );
const { getByText } = render();
diff --git a/src/components/calls/call-images-modal.tsx b/src/components/calls/call-images-modal.tsx
index 2b845ea..dea9296 100644
--- a/src/components/calls/call-images-modal.tsx
+++ b/src/components/calls/call-images-modal.tsx
@@ -7,7 +7,7 @@ import { useColorScheme } from 'nativewind';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Keyboard, Modal, SafeAreaView, StyleSheet, TouchableOpacity, View } from 'react-native';
-import { KeyboardStickyView } from 'react-native-keyboard-controller';
+import { KeyboardProvider, KeyboardStickyView } from 'react-native-keyboard-controller';
import { launchCallImagePicker } from '@/components/calls/call-image-picker';
import { Loading } from '@/components/common/loading';
@@ -216,6 +216,14 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call
setFullScreenImage({ source, name });
}, []);
+ const handleStartAddImage = useCallback(() => {
+ setIsAddingImage(true);
+ }, []);
+
+ const handleCloseFullScreenImage = useCallback(() => {
+ setFullScreenImage(null);
+ }, []);
+
const getActiveImage = () => {
if (!validImages || validImages.length === 0 || activeIndex < 0 || activeIndex >= validImages.length) return null;
return validImages[activeIndex];
@@ -410,30 +418,32 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call
return (
<>
-
- {/* Header */}
-
- {isAddingImage ? t('callImages.add_new') : t('callImages.title')}
-
- {!isAddingImage && !isLoadingImages ? (
-
- ) : null}
-
-
-
-
-
+
+
+ {/* Header */}
+
+ {isAddingImage ? t('callImages.add_new') : t('callImages.title')}
+
+ {!isAddingImage && !isLoadingImages ? (
+
+ ) : null}
+
+
+
+
+
- {/* Content */}
- {renderContent()}
-
+ {/* Content */}
+ {renderContent()}
+
+
{/* Full Screen Image Modal */}
- setFullScreenImage(null)} imageSource={fullScreenImage?.source || { uri: '' }} imageName={fullScreenImage?.name} />
+
>
);
};
diff --git a/src/components/calls/call-notes-modal.tsx b/src/components/calls/call-notes-modal.tsx
index 33048ba..5c5ce8b 100644
--- a/src/components/calls/call-notes-modal.tsx
+++ b/src/components/calls/call-notes-modal.tsx
@@ -3,7 +3,7 @@ import { useColorScheme } from 'nativewind';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, Keyboard, Modal, SafeAreaView, StyleSheet, TouchableOpacity, View } from 'react-native';
-import { KeyboardStickyView } from 'react-native-keyboard-controller';
+import { KeyboardProvider, KeyboardStickyView } from 'react-native-keyboard-controller';
import { useAnalytics } from '@/hooks/use-analytics';
import { useAuthStore } from '@/lib/auth';
@@ -105,64 +105,66 @@ const CallNotesModal = ({ isOpen, onClose, callId }: CallNotesModalProps) => {
return (
-
- {/* Header */}
-
- {t('callNotes.title')}
-
-
-
-
-
- {/* Search Bar */}
-
-
-
-
-
-
-
-
-
- {/* Notes List */}
-
- {isNotesLoading ? (
-
- ) : filteredNotes.length > 0 ? (
- item.CallNoteId}
- contentContainerStyle={styles.listContent}
- showsVerticalScrollIndicator={true}
- keyboardShouldPersistTaps="handled"
- />
- ) : (
-
- )}
-
-
- {/* Add Note Section - Sticks to keyboard */}
-
-
-
- {t('callNotes.addNoteLabel')}
-
-
-
-
-
-
-
+
+
+ {/* Header */}
+
+ {t('callNotes.title')}
+
+
+
-
-
+
+ {/* Search Bar */}
+
+
+
+
+
+
+
+
+
+ {/* Notes List */}
+
+ {isNotesLoading ? (
+
+ ) : filteredNotes.length > 0 ? (
+ item.CallNoteId}
+ contentContainerStyle={styles.listContent}
+ showsVerticalScrollIndicator={true}
+ keyboardShouldPersistTaps="handled"
+ />
+ ) : (
+
+ )}
+
+
+ {/* Add Note Section - Sticks to keyboard */}
+
+
+
+ {t('callNotes.addNoteLabel')}
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/notifications/NotificationDetail.tsx b/src/components/notifications/NotificationDetail.tsx
index 254dff3..56e5004 100644
--- a/src/components/notifications/NotificationDetail.tsx
+++ b/src/components/notifications/NotificationDetail.tsx
@@ -32,14 +32,19 @@ export const NotificationDetail = ({ notification, onClose, onDelete, onNavigate
const { refetch } = useNotifications();
const slideAnim = React.useRef(new Animated.Value(SIDEBAR_WIDTH)).current;
const fadeAnim = React.useRef(new Animated.Value(0)).current;
+ const markedAsReadRef = React.useRef(null);
useEffect(() => {
- // Mark as read when opened - we'll just refetch to sync with server
- if (!notification.read && notification.id) {
+ // Mark as read when opened - refetch once per notification to sync with server.
+ // Guarded by a ref so an unstable `refetch` reference cannot cause a refetch loop.
+ if (!notification.read && notification.id && markedAsReadRef.current !== notification.id) {
+ markedAsReadRef.current = notification.id;
refetch();
}
+ }, [notification.id, notification.read, refetch]);
- // Animate in
+ useEffect(() => {
+ // Animate in (slideAnim/fadeAnim are stable refs)
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 0,
@@ -52,7 +57,7 @@ export const NotificationDetail = ({ notification, onClose, onDelete, onNavigate
useNativeDriver: true,
}),
]).start();
- }, [notification, refetch, slideAnim, fadeAnim]);
+ }, [slideAnim, fadeAnim]);
const handleClose = () => {
// Animate out
diff --git a/src/components/notifications/NotificationInbox.tsx b/src/components/notifications/NotificationInbox.tsx
index b1cfe14..3a10edd 100644
--- a/src/components/notifications/NotificationInbox.tsx
+++ b/src/components/notifications/NotificationInbox.tsx
@@ -25,6 +25,86 @@ interface NotificationInboxProps {
onClose: () => void;
}
+interface NotificationRowProps {
+ item: any;
+ isSelectionMode: boolean;
+ isSelected: boolean;
+ onPress: (notification: NotificationPayload) => void;
+ onLongPress: (notificationId: string) => void;
+ onNavigateToReference: (referenceType: string, referenceId: string) => void;
+}
+
+const parseNotificationDate = (createdAt: string | undefined | null): Date | null => {
+ if (!createdAt) return null;
+ const date = new Date(createdAt);
+ return Number.isNaN(date.getTime()) ? null : date;
+};
+
+const formatNotificationDate = (createdAt: string | undefined | null): string => parseNotificationDate(createdAt)?.toLocaleDateString() ?? '';
+
+const formatNotificationTime = (createdAt: string | undefined | null): string => parseNotificationDate(createdAt)?.toLocaleTimeString() ?? '';
+
+const NotificationRow = React.memo(({ item, isSelectionMode, isSelected, onPress, onLongPress, onNavigateToReference }) => {
+ const notification: NotificationPayload = React.useMemo(
+ () => ({
+ id: item.id,
+ title: item.subject,
+ body: item.body,
+ createdAt: item.createdAt,
+ read: item.read,
+ type: item.type,
+ referenceId: item.payload?.referenceId,
+ referenceType: item.payload?.referenceType,
+ metadata: item.payload?.metadata,
+ }),
+ [item]
+ );
+
+ const formattedDate = React.useMemo(() => formatNotificationDate(notification.createdAt), [notification.createdAt]);
+ const formattedTime = React.useMemo(() => formatNotificationTime(notification.createdAt), [notification.createdAt]);
+
+ const handlePress = React.useCallback(() => onPress(notification), [onPress, notification]);
+ const handleLongPress = React.useCallback(() => onLongPress(notification.id), [onLongPress, notification.id]);
+ const handleNavigate = React.useCallback(() => {
+ if (notification.referenceType && notification.referenceId) {
+ onNavigateToReference(notification.referenceType, notification.referenceId);
+ }
+ }, [onNavigateToReference, notification.referenceType, notification.referenceId]);
+
+ return (
+
+ {!item.read ? : null}
+
+ {isSelectionMode ? (
+
+ {isSelected ? : }
+
+ ) : null}
+
+
+ {notification.title}
+
+ {formattedDate} {formattedTime}
+
+
+
+ {!isSelectionMode ? (
+ notification.referenceType && notification.referenceId ? (
+
+
+
+
+ ) : (
+
+ )
+ ) : null}
+
+ );
+});
+NotificationRow.displayName = 'NotificationRow';
+
export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) => {
const userId = useAuthStore((state) => state.userId);
const config = useCoreStore((state: any) => state.config);
@@ -78,30 +158,43 @@ export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) =
}
}, [isOpen, slideAnim, fadeAnim]);
- const handleNotificationPress = (notification: NotificationPayload) => {
- if (isSelectionMode) {
- toggleNotificationSelection(notification.id);
- } else {
- setSelectedNotification(notification);
- }
- };
-
- const toggleNotificationSelection = (notificationId: string) => {
- setSelectedNotificationIds((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(notificationId)) {
- newSet.delete(notificationId);
+ const handleNotificationPress = React.useCallback(
+ (notification: NotificationPayload) => {
+ if (isSelectionMode) {
+ setSelectedNotificationIds((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(notification.id)) {
+ newSet.delete(notification.id);
+ } else {
+ newSet.add(notification.id);
+ }
+ return newSet;
+ });
} else {
- newSet.add(notificationId);
+ setSelectedNotification(notification);
}
- return newSet;
- });
- };
+ },
+ [isSelectionMode]
+ );
+
+ const handleNotificationLongPress = React.useCallback(
+ (notificationId: string) => {
+ if (!isSelectionMode) {
+ setIsSelectionMode(true);
+ setSelectedNotificationIds((prev) => {
+ const newSet = new Set(prev);
+ newSet.add(notificationId);
+ return newSet;
+ });
+ }
+ },
+ [isSelectionMode]
+ );
- const enterSelectionMode = () => {
+ const enterSelectionMode = React.useCallback(() => {
setIsSelectionMode(true);
setSelectedNotificationIds(new Set());
- };
+ }, []);
const exitSelectionMode = React.useCallback(() => {
setIsSelectionMode(false);
@@ -154,70 +247,30 @@ export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) =
[showToast, refetch]
);
- const handleNavigateToReference = (referenceType: string, referenceId: string) => {
- // TODO: Implement navigation based on reference type
- console.log('Navigate to:', referenceType, referenceId);
- onClose();
- };
-
- const renderItem = ({ item }: { item: any }) => {
- const notification: NotificationPayload = {
- id: item.id,
- title: item.subject,
- body: item.body,
- createdAt: item.createdAt,
- read: item.read,
- type: item.type,
- referenceId: item.payload?.referenceId,
- referenceType: item.payload?.referenceType,
- metadata: item.payload?.metadata,
- };
-
- const isSelected = selectedNotificationIds.has(notification.id);
-
- return (
- handleNotificationPress(notification)}
- onLongPress={() => {
- if (!isSelectionMode) {
- enterSelectionMode();
- toggleNotificationSelection(notification.id);
- }
- }}
- style={[styles.notificationItem, !item.read ? styles.unreadNotificationItem : {}, isSelected ? styles.selectedNotificationItem : {}]}
- >
- {!item.read ? : null}
-
- {isSelectionMode ? (
-
- {isSelected ? : }
-
- ) : null}
-
-
- {notification.title}
-
- {new Date(notification.createdAt).toLocaleDateString()} {new Date(notification.createdAt).toLocaleTimeString()}
-
-
+ const handleNavigateToReference = React.useCallback(
+ (referenceType: string, referenceId: string) => {
+ // TODO: Implement navigation based on reference type
+ console.log('Navigate to:', referenceType, referenceId);
+ onClose();
+ },
+ [onClose]
+ );
- {!isSelectionMode ? (
- notification.referenceType && notification.referenceId ? (
-
-
-
-
- ) : (
-
- )
- ) : null}
-
- );
- };
+ const renderItem = React.useCallback(
+ ({ item }: { item: any }) => (
+
+ ),
+ [isSelectionMode, selectedNotificationIds, handleNotificationPress, handleNotificationLongPress, handleNavigateToReference]
+ );
- const renderFooter = () => {
+ const renderFooter = React.useCallback(() => {
if (!hasMore) return null;
return (
@@ -225,12 +278,15 @@ export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) =
);
- };
+ }, [hasMore]);
- const renderEmpty = () => (
-
- No updates available
-
+ const renderEmpty = React.useCallback(
+ () => (
+
+ No updates available
+
+ ),
+ []
);
if (!isOpen) {