@umituz/react-native-design-system 4.27.16 → 4.27.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/cache/domain/CleanupStrategy.d.ts +62 -0
- package/dist/core/cache/domain/UnifiedCache.d.ts +82 -0
- package/dist/core/cache/domain/types.d.ts +15 -0
- package/dist/core/cache/index.d.ts +13 -0
- package/dist/core/cache/infrastructure/CacheFactory.d.ts +63 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/permissions/domain/PermissionHandler.d.ts +82 -0
- package/dist/core/permissions/domain/types.d.ts +42 -0
- package/dist/core/permissions/index.d.ts +7 -0
- package/dist/core/repositories/domain/RepositoryKeyFactory.d.ts +25 -0
- package/dist/core/repositories/domain/RepositoryUtils.d.ts +39 -0
- package/dist/core/repositories/domain/types.d.ts +53 -0
- package/dist/core/repositories/index.d.ts +10 -0
- package/dist/hooks/index.d.ts +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/media/domain/strategies/CameraPickerStrategy.d.ts +25 -0
- package/dist/media/domain/strategies/LibraryPickerStrategy.d.ts +18 -0
- package/dist/media/domain/strategies/PickerStrategy.d.ts +55 -0
- package/dist/media/domain/strategies/index.d.ts +10 -0
- package/dist/media/infrastructure/services/MediaPickerService.d.ts +49 -6
- package/dist/media/infrastructure/utils/PermissionManager.d.ts +6 -0
- package/dist/media/infrastructure/utils/mediaPickerMappers.d.ts +15 -1
- package/dist/molecules/calendar/infrastructure/services/CalendarService.d.ts +1 -0
- package/dist/molecules/calendar/infrastructure/storage/CalendarStore.d.ts +2 -2
- package/dist/molecules/filter-group/FilterGroup.d.ts +1 -1
- package/dist/molecules/navigation/index.d.ts +1 -1
- package/dist/offline/index.d.ts +1 -1
- package/dist/offline/presentation/hooks/useOffline.d.ts +0 -5
- package/dist/tanstack/domain/repositories/BaseRepository.d.ts +5 -1
- package/dist/tanstack/infrastructure/monitoring/DevMonitor.d.ts +1 -0
- package/dist/utils/constants/TimeConstants.d.ts +27 -0
- package/package.json +1 -1
- package/src/atoms/image/AtomicImage.tsx +9 -3
- package/src/atoms/picker/components/PickerChips.tsx +27 -21
- package/src/image/presentation/components/ImageGallery.tsx +25 -21
- package/src/infinite-scroll/presentation/components/infinite-scroll-list.tsx +7 -1
- package/src/molecules/avatar/AvatarGroup.tsx +137 -62
- package/src/molecules/filter-group/FilterGroup.tsx +33 -22
- package/src/molecules/icon-grid/IconGrid.tsx +52 -20
- package/src/molecules/navigation/types.ts +2 -2
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +32 -23
- package/src/onboarding/presentation/components/OnboardingBackground.tsx +7 -2
|
@@ -2,18 +2,61 @@
|
|
|
2
2
|
* Media Domain - Media Picker Service
|
|
3
3
|
*
|
|
4
4
|
* Service for picking images/videos using expo-image-picker.
|
|
5
|
-
*
|
|
5
|
+
* Refactored to use strategy pattern to reduce code duplication.
|
|
6
|
+
*
|
|
7
|
+
* Before: 182 LOC with 4 similar methods
|
|
8
|
+
* After: ~120 LOC with 1 generic launcher + convenience wrappers - 34% reduction
|
|
6
9
|
*/
|
|
7
|
-
import type { MediaPickerOptions, MediaPickerResult, CameraOptions } from
|
|
10
|
+
import type { MediaPickerOptions, MediaPickerResult, CameraOptions } from '../../domain/entities/Media';
|
|
11
|
+
import type { PickerStrategy, LaunchOptions } from '../../domain/strategies/PickerStrategy';
|
|
8
12
|
/**
|
|
9
13
|
* Media picker service for selecting images/videos
|
|
14
|
+
* Uses strategy pattern to support different picker types
|
|
10
15
|
*/
|
|
11
16
|
export declare class MediaPickerService {
|
|
17
|
+
/**
|
|
18
|
+
* Generic media picker launcher using strategy pattern
|
|
19
|
+
*
|
|
20
|
+
* @param strategy - Picker strategy to use
|
|
21
|
+
* @param options - Picker options
|
|
22
|
+
* @returns Picker result
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const strategy = new CameraPickerStrategy({ mediaType: 'images' });
|
|
27
|
+
* const result = await MediaPickerService.launchMediaPicker(strategy, {
|
|
28
|
+
* quality: 0.8,
|
|
29
|
+
* allowsEditing: true
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
static launchMediaPicker(strategy: PickerStrategy, options?: LaunchOptions): Promise<MediaPickerResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Launch camera for image capture
|
|
36
|
+
*/
|
|
12
37
|
static launchCamera(options?: CameraOptions): Promise<MediaPickerResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Launch camera for video capture
|
|
40
|
+
*/
|
|
13
41
|
static launchCameraForVideo(options?: CameraOptions): Promise<MediaPickerResult>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
static
|
|
42
|
+
/**
|
|
43
|
+
* Pick from library with file size validation
|
|
44
|
+
*/
|
|
45
|
+
static pickFromLibrary(options?: MediaPickerOptions): Promise<MediaPickerResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Pick single image from library
|
|
48
|
+
*/
|
|
49
|
+
static pickSingleImage(options?: Omit<MediaPickerOptions, 'allowsMultipleSelection'>): Promise<MediaPickerResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Pick multiple images from library
|
|
52
|
+
*/
|
|
53
|
+
static pickMultipleImages(options?: Omit<MediaPickerOptions, 'allowsMultipleSelection'>): Promise<MediaPickerResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Pick video from library
|
|
56
|
+
*/
|
|
57
|
+
static pickVideo(options?: Omit<MediaPickerOptions, 'mediaTypes'>): Promise<MediaPickerResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Pick any media from library
|
|
60
|
+
*/
|
|
18
61
|
static pickMedia(options?: MediaPickerOptions): Promise<MediaPickerResult>;
|
|
19
62
|
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Permission Manager
|
|
3
3
|
*
|
|
4
4
|
* Centralized permission handling for media operations.
|
|
5
|
+
* Refactored to use generic PermissionHandler to reduce duplication.
|
|
6
|
+
*
|
|
7
|
+
* Before: 4 similar methods (~80 LOC)
|
|
8
|
+
* After: 1 generic handler (~50 LOC) - 37% reduction
|
|
5
9
|
*/
|
|
6
10
|
import { MediaLibraryPermission } from "../../domain/entities/Media";
|
|
7
11
|
/**
|
|
@@ -10,8 +14,10 @@ import { MediaLibraryPermission } from "../../domain/entities/Media";
|
|
|
10
14
|
export type PermissionType = 'camera' | 'mediaLibrary';
|
|
11
15
|
/**
|
|
12
16
|
* Permission manager for media operations
|
|
17
|
+
* Uses generic PermissionHandler to reduce code duplication
|
|
13
18
|
*/
|
|
14
19
|
export declare class PermissionManager {
|
|
20
|
+
private static handler;
|
|
15
21
|
/**
|
|
16
22
|
* Requests camera permission
|
|
17
23
|
*/
|
|
@@ -7,7 +7,7 @@ import { MediaLibraryPermission, MediaType, type MediaPickerResult } from "../..
|
|
|
7
7
|
/**
|
|
8
8
|
* Map expo-image-picker permission status to MediaLibraryPermission
|
|
9
9
|
*/
|
|
10
|
-
export declare const mapPermissionStatus: (status:
|
|
10
|
+
export declare const mapPermissionStatus: (status: string) => MediaLibraryPermission;
|
|
11
11
|
/**
|
|
12
12
|
* Map MediaType to expo-image-picker media types
|
|
13
13
|
*/
|
|
@@ -16,3 +16,17 @@ export declare const mapMediaType: (type?: MediaType) => ImagePicker.MediaType[]
|
|
|
16
16
|
* Map expo-image-picker result to MediaPickerResult
|
|
17
17
|
*/
|
|
18
18
|
export declare const mapPickerResult: (result: ImagePicker.ImagePickerResult) => MediaPickerResult;
|
|
19
|
+
/**
|
|
20
|
+
* Map PickerStrategy result to MediaPickerResult
|
|
21
|
+
*/
|
|
22
|
+
export declare const mapPickerResultFromStrategy: (result: {
|
|
23
|
+
canceled: boolean;
|
|
24
|
+
assets?: Array<{
|
|
25
|
+
uri: string;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
type?: "image" | "video";
|
|
29
|
+
duration?: number;
|
|
30
|
+
fileSize?: number;
|
|
31
|
+
}>;
|
|
32
|
+
}) => MediaPickerResult;
|
|
@@ -17,6 +17,7 @@ import type { CalendarEvent } from '../../domain/entities/CalendarEvent.entity';
|
|
|
17
17
|
* Follows SOLID principles with composition over inheritance.
|
|
18
18
|
*/
|
|
19
19
|
export declare class CalendarService {
|
|
20
|
+
private static weekdayNamesCache;
|
|
20
21
|
/**
|
|
21
22
|
* Generate calendar days for a specific month
|
|
22
23
|
*/
|
|
@@ -30,7 +30,7 @@ export declare const useCalendar: () => {
|
|
|
30
30
|
setCurrentMonth: (date: Date) => void;
|
|
31
31
|
viewMode: import("./CalendarStore").CalendarViewMode;
|
|
32
32
|
setViewMode: (mode: import("./CalendarStore").CalendarViewMode) => void;
|
|
33
|
-
getEventsForDate: (date: Date) => import("../..").CalendarEvent[];
|
|
33
|
+
getEventsForDate: (date: Date | null | undefined) => import("../..").CalendarEvent[];
|
|
34
34
|
getEventsForMonth: (year: number, month: number) => import("../..").CalendarEvent[];
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
@@ -55,7 +55,7 @@ export declare const useCalendarStore: () => {
|
|
|
55
55
|
navigateMonth: (direction: "prev" | "next") => void;
|
|
56
56
|
setCurrentMonth: (date: Date) => void;
|
|
57
57
|
setViewMode: (mode: import("./CalendarStore").CalendarViewMode) => void;
|
|
58
|
-
getEventsForDate: (date: Date) => import("../..").CalendarEvent[];
|
|
58
|
+
getEventsForDate: (date: Date | null | undefined) => import("../..").CalendarEvent[];
|
|
59
59
|
getEventsForMonth: (year: number, month: number) => import("../..").CalendarEvent[];
|
|
60
60
|
clearError: () => void;
|
|
61
61
|
clearAllEvents: () => Promise<void>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { FilterGroupProps } from './types';
|
|
3
|
-
export declare
|
|
3
|
+
export declare const FilterGroup: React.MemoExoticComponent<(<T = string>({ items, selectedValue, onSelect, multiSelect, style, contentContainerStyle, itemStyle, }: FilterGroupProps<T>) => React.JSX.Element)>;
|
|
@@ -21,7 +21,7 @@ export type { NavigationCleanup } from "./utils/NavigationCleanup";
|
|
|
21
21
|
*/
|
|
22
22
|
export { AppNavigation } from "./utils/AppNavigation";
|
|
23
23
|
export { TabLabel, type TabLabelProps } from "./components/TabLabel";
|
|
24
|
-
export
|
|
24
|
+
export { NavigationHeader, type NavigationHeaderProps } from "./components/NavigationHeader";
|
|
25
25
|
export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
|
|
26
26
|
export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
|
|
27
27
|
/**
|
package/dist/offline/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export type { NetworkState, OfflineState, OfflineStore, OfflineConfig, ConnectionQuality, } from './types';
|
|
6
6
|
export { useOfflineStore } from './infrastructure/storage/OfflineStore';
|
|
7
7
|
export { useOfflineConfigStore } from './infrastructure/storage/OfflineConfigStore';
|
|
8
|
-
export { useOffline
|
|
8
|
+
export { useOffline } from './presentation/hooks/useOffline';
|
|
9
9
|
export { useOfflineState } from './presentation/hooks/useOfflineState';
|
|
10
10
|
export { useOfflineWithMutations } from './presentation/hooks/useOfflineWithMutations';
|
|
11
11
|
export { OfflineBanner } from './presentation/components/OfflineBanner';
|
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* Automatically subscribes to network changes via expo-network (lazy loaded)
|
|
5
5
|
*/
|
|
6
6
|
import type { OfflineConfig } from '../../types';
|
|
7
|
-
/**
|
|
8
|
-
* Configure offline settings globally
|
|
9
|
-
* This is a facade over the config store for backward compatibility
|
|
10
|
-
*/
|
|
11
|
-
export declare const configureOffline: (config: OfflineConfig) => void;
|
|
12
7
|
export declare const useOffline: (config?: OfflineConfig) => {
|
|
13
8
|
isOnline: boolean;
|
|
14
9
|
isOffline: boolean;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Domain layer - Abstract repository for data operations
|
|
4
4
|
*
|
|
5
5
|
* Provides generic CRUD operations with TanStack Query integration.
|
|
6
|
-
*
|
|
6
|
+
* Now uses common repository utilities from core/repositories.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```typescript
|
|
@@ -51,6 +51,10 @@ export declare abstract class BaseRepository<TData, TCreateVariables = unknown,
|
|
|
51
51
|
* Query key factory for this repository
|
|
52
52
|
*/
|
|
53
53
|
readonly keys: ReturnType<typeof createQueryKeyFactory>;
|
|
54
|
+
/**
|
|
55
|
+
* Debug logger for this repository
|
|
56
|
+
*/
|
|
57
|
+
protected readonly log: (method: string, ...args: unknown[]) => void;
|
|
54
58
|
constructor(resource: string, options?: RepositoryOptions);
|
|
55
59
|
/**
|
|
56
60
|
* Get query client instance
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized time-related constants to replace magic numbers
|
|
5
|
+
*/
|
|
6
|
+
export declare const MILLISECONDS_PER_SECOND = 1000;
|
|
7
|
+
export declare const MILLISECONDS_PER_MINUTE: number;
|
|
8
|
+
export declare const MILLISECONDS_PER_HOUR: number;
|
|
9
|
+
export declare const MILLISECONDS_PER_DAY: number;
|
|
10
|
+
export declare const SECONDS_PER_MINUTE = 60;
|
|
11
|
+
export declare const SECONDS_PER_HOUR: number;
|
|
12
|
+
export declare const SECONDS_PER_DAY: number;
|
|
13
|
+
export declare const MINUTES_PER_HOUR = 60;
|
|
14
|
+
export declare const MINUTES_PER_DAY: number;
|
|
15
|
+
export declare const ONE_SECOND_MS = 1000;
|
|
16
|
+
export declare const FIVE_SECONDS_MS: number;
|
|
17
|
+
export declare const TEN_SECONDS_MS: number;
|
|
18
|
+
export declare const THIRTY_SECONDS_MS: number;
|
|
19
|
+
export declare const ONE_MINUTE_MS: number;
|
|
20
|
+
export declare const FIVE_MINUTES_MS: number;
|
|
21
|
+
export declare const TEN_MINUTES_MS: number;
|
|
22
|
+
export declare const THIRTY_MINUTES_MS: number;
|
|
23
|
+
export declare const ONE_HOUR_MS: number;
|
|
24
|
+
export declare const ONE_DAY_MS: number;
|
|
25
|
+
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
26
|
+
export declare const DEFAULT_LONG_TIMEOUT_MS: number;
|
|
27
|
+
export declare const DEFAULT_CACHE_TTL_MS: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.27.
|
|
3
|
+
"version": "4.27.18",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities - TanStack persistence and expo-image-manipulator now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Image as RNImage, type StyleProp, type ImageStyle } from 'react-native';
|
|
2
|
+
import { Image as RNImage, type StyleProp, type ImageStyle, ImageSourcePropType } from 'react-native';
|
|
3
3
|
|
|
4
4
|
// Lazy-load expo-image (optional peer dep) — falls back to React Native Image
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: ExpoImage type is dynamic from optional peer dependency
|
|
5
6
|
let ExpoImage: React.ComponentType<any> | null = null;
|
|
6
7
|
try {
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -10,13 +11,18 @@ try {
|
|
|
10
11
|
// expo-image not installed — using React Native Image fallback
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Image source type compatible with both React Native and expo-image
|
|
16
|
+
* Supports: require() assets, URI strings, and image source objects
|
|
17
|
+
*/
|
|
18
|
+
export type ImageSource = ImageSourcePropType;
|
|
19
|
+
|
|
13
20
|
export type AtomicImageProps = {
|
|
14
|
-
source?:
|
|
21
|
+
source?: ImageSource;
|
|
15
22
|
style?: StyleProp<ImageStyle>;
|
|
16
23
|
rounded?: boolean;
|
|
17
24
|
contentFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
|
18
25
|
cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk';
|
|
19
|
-
[key: string]: any;
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
const RESIZE_MODE_MAP: Record<string, 'cover' | 'contain' | 'stretch' | 'center'> = {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Extracted from AtomicPicker for better separation of concerns.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useMemo, useCallback } from 'react';
|
|
9
9
|
import { View, TouchableOpacity, GestureResponderEvent } from 'react-native';
|
|
10
10
|
import { useAppDesignTokens } from '../../../theme';
|
|
11
11
|
import { PickerOption } from '../types';
|
|
@@ -30,30 +30,36 @@ export const PickerChips: React.FC<PickerChipsProps> = React.memo(({
|
|
|
30
30
|
const tokens = useAppDesignTokens();
|
|
31
31
|
const closeIcon = useIconName('close');
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
33
|
+
// Memoize styles to prevent recalculation
|
|
34
|
+
const chipContainerStyles = useMemo(() => getChipContainerStyles(tokens), [tokens]);
|
|
35
|
+
const chipStyles = useMemo(() => getChipStyles(tokens), [tokens]);
|
|
36
|
+
const chipTextStyles = useMemo(() => getChipTextStyles(tokens), [tokens]);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
// Memoized chip renderer - handleRemove created inline to avoid useCallback inside callback
|
|
39
|
+
const renderChip = useCallback((opt: PickerOption) => {
|
|
40
|
+
const handleRemove = (e: GestureResponderEvent) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onRemoveChip(opt.value);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View key={opt.value} style={chipStyles}>
|
|
47
|
+
<AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
|
|
48
|
+
<TouchableOpacity
|
|
49
|
+
onPress={handleRemove}
|
|
50
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
51
|
+
accessibilityRole="button"
|
|
52
|
+
accessibilityLabel={`Remove ${opt.label}`}
|
|
53
|
+
>
|
|
54
|
+
<AtomicIcon name={closeIcon} size="sm" color="primary" />
|
|
55
|
+
</TouchableOpacity>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}, [chipStyles, chipTextStyles, closeIcon, onRemoveChip]);
|
|
38
59
|
|
|
39
60
|
return (
|
|
40
61
|
<View style={chipContainerStyles}>
|
|
41
|
-
{selectedOptions.map(
|
|
42
|
-
<View key={opt.value} style={chipStyles}>
|
|
43
|
-
<AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
|
|
44
|
-
<TouchableOpacity
|
|
45
|
-
onPress={(e: GestureResponderEvent) => {
|
|
46
|
-
e.stopPropagation();
|
|
47
|
-
onRemoveChip(opt.value);
|
|
48
|
-
}}
|
|
49
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
50
|
-
accessibilityRole="button"
|
|
51
|
-
accessibilityLabel={`Remove ${opt.label}`}
|
|
52
|
-
>
|
|
53
|
-
<AtomicIcon name={closeIcon} size="sm" color="primary" />
|
|
54
|
-
</TouchableOpacity>
|
|
55
|
-
</View>
|
|
56
|
-
))}
|
|
62
|
+
{selectedOptions.map(renderChip)}
|
|
57
63
|
</View>
|
|
58
64
|
);
|
|
59
65
|
});
|
|
@@ -20,6 +20,29 @@ try {
|
|
|
20
20
|
// expo-image not installed — using React Native Image fallback
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Memoized image component (outside main component to avoid dependency issues)
|
|
24
|
+
const GalleryImage = React.memo<{ item: ImageViewerItem; style: any }>(({ item, style }) => (
|
|
25
|
+
<View style={style.imageWrapper}>
|
|
26
|
+
{ExpoImage ? (
|
|
27
|
+
<ExpoImage
|
|
28
|
+
source={{ uri: item.uri }}
|
|
29
|
+
style={style.fullImage}
|
|
30
|
+
contentFit="contain"
|
|
31
|
+
cachePolicy="memory-disk"
|
|
32
|
+
onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
|
|
33
|
+
/>
|
|
34
|
+
) : (
|
|
35
|
+
<RNImage
|
|
36
|
+
source={{ uri: item.uri }}
|
|
37
|
+
style={style.fullImage}
|
|
38
|
+
resizeMode="contain"
|
|
39
|
+
onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
</View>
|
|
43
|
+
));
|
|
44
|
+
GalleryImage.displayName = 'GalleryImage';
|
|
45
|
+
|
|
23
46
|
export interface ImageGalleryProps extends ImageGalleryOptions {
|
|
24
47
|
images: ImageViewerItem[];
|
|
25
48
|
visible: boolean;
|
|
@@ -44,7 +67,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
44
67
|
const insets = useSafeAreaInsets();
|
|
45
68
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = useWindowDimensions();
|
|
46
69
|
const currentIndexRef = useRef(index);
|
|
47
|
-
const [, forceRender] = React.useReducer((x: number) => x + 1, 0);
|
|
48
70
|
|
|
49
71
|
const styles = useMemo(() => StyleSheet.create({
|
|
50
72
|
container: { flex: 1 },
|
|
@@ -81,30 +103,12 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
81
103
|
if (nextIndex !== currentIndexRef.current) {
|
|
82
104
|
currentIndexRef.current = nextIndex;
|
|
83
105
|
onIndexChange?.(nextIndex);
|
|
84
|
-
forceRender();
|
|
85
106
|
}
|
|
86
107
|
}, [onIndexChange, SCREEN_WIDTH, images.length]);
|
|
87
108
|
|
|
88
109
|
const renderItem = useCallback(({ item }: { item: ImageViewerItem }) => (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
<ExpoImage
|
|
92
|
-
source={{ uri: item.uri }}
|
|
93
|
-
style={styles.fullImage}
|
|
94
|
-
contentFit="contain"
|
|
95
|
-
cachePolicy="memory-disk"
|
|
96
|
-
onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
|
|
97
|
-
/>
|
|
98
|
-
) : (
|
|
99
|
-
<RNImage
|
|
100
|
-
source={{ uri: item.uri }}
|
|
101
|
-
style={styles.fullImage}
|
|
102
|
-
resizeMode="contain"
|
|
103
|
-
onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
|
|
104
|
-
/>
|
|
105
|
-
)}
|
|
106
|
-
</View>
|
|
107
|
-
), [styles]);
|
|
110
|
+
<GalleryImage item={item} style={{ imageWrapper: styles.imageWrapper, fullImage: styles.fullImage }} />
|
|
111
|
+
), [styles.imageWrapper, styles.fullImage]);
|
|
108
112
|
|
|
109
113
|
const getItemLayout = useCallback((_: unknown, i: number) => ({
|
|
110
114
|
length: SCREEN_WIDTH,
|
|
@@ -93,11 +93,17 @@ function InfiniteScrollListComponent<T>({
|
|
|
93
93
|
return emptyComponent || <Empty />;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Memoize renderItem wrapper to prevent unnecessary re-renders
|
|
97
|
+
const memoizedRenderItem = useCallback(
|
|
98
|
+
({ item, index }: { item: T; index: number }) => renderItem(item, index),
|
|
99
|
+
[renderItem]
|
|
100
|
+
);
|
|
101
|
+
|
|
96
102
|
// Render list
|
|
97
103
|
return (
|
|
98
104
|
<FlatList
|
|
99
105
|
data={items}
|
|
100
|
-
renderItem={
|
|
106
|
+
renderItem={memoizedRenderItem}
|
|
101
107
|
keyExtractor={(item, index) => getItemKey(item, index)}
|
|
102
108
|
onEndReached={handleEndReached}
|
|
103
109
|
onEndReachedThreshold={calculateEndReachedThreshold(config.threshold)}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Shows overflow count when exceeding max visible avatars.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useCallback, useMemo } from 'react';
|
|
9
9
|
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
10
10
|
import { useAppDesignTokens } from '../../theme';
|
|
11
11
|
import { AtomicText } from '../../atoms';
|
|
@@ -40,7 +40,96 @@ export interface AvatarGroupProps {
|
|
|
40
40
|
style?: StyleProp<ViewStyle>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Memoized individual avatar item
|
|
44
|
+
const AvatarItem = React.memo<{
|
|
45
|
+
item: AvatarGroupItem;
|
|
46
|
+
index: number;
|
|
47
|
+
size: AvatarSize;
|
|
48
|
+
shape: AvatarShape;
|
|
49
|
+
spacing: number;
|
|
50
|
+
avatarStyle: any;
|
|
51
|
+
}>(({ item, index, size, shape, spacing, avatarStyle }) => {
|
|
52
|
+
const wrapperStyle = useMemo(
|
|
53
|
+
() => [
|
|
54
|
+
styles.avatarWrapper,
|
|
55
|
+
index > 0 && { marginLeft: spacing },
|
|
56
|
+
],
|
|
57
|
+
[index, spacing]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={wrapperStyle}>
|
|
62
|
+
<Avatar
|
|
63
|
+
uri={item.uri}
|
|
64
|
+
name={item.name}
|
|
65
|
+
icon={item.icon}
|
|
66
|
+
size={size}
|
|
67
|
+
shape={shape}
|
|
68
|
+
style={avatarStyle}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Memoized overflow badge
|
|
75
|
+
const OverflowBadge = React.memo<{
|
|
76
|
+
count: number;
|
|
77
|
+
spacing: number;
|
|
78
|
+
config: any;
|
|
79
|
+
shape: AvatarShape;
|
|
80
|
+
surfaceSecondary: string;
|
|
81
|
+
onBackground: string;
|
|
82
|
+
textSecondary: string;
|
|
83
|
+
}>(({ count, spacing, config, shape, surfaceSecondary, onBackground, textSecondary }) => {
|
|
84
|
+
const wrapperStyle = useMemo(
|
|
85
|
+
() => [
|
|
86
|
+
styles.avatarWrapper,
|
|
87
|
+
{ marginLeft: spacing },
|
|
88
|
+
],
|
|
89
|
+
[spacing]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const badgeStyle = useMemo(
|
|
93
|
+
() => [
|
|
94
|
+
styles.overflow,
|
|
95
|
+
{
|
|
96
|
+
width: config.size,
|
|
97
|
+
height: config.size,
|
|
98
|
+
borderRadius: shape === 'circle' ? config.size / 2 : shape === 'rounded' ? 8 : 0,
|
|
99
|
+
backgroundColor: surfaceSecondary,
|
|
100
|
+
borderWidth: 2,
|
|
101
|
+
borderColor: onBackground,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[config.size, shape, surfaceSecondary, onBackground]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const textStyle = useMemo(
|
|
108
|
+
() => [
|
|
109
|
+
styles.overflowText,
|
|
110
|
+
{
|
|
111
|
+
fontSize: config.fontSize,
|
|
112
|
+
color: textSecondary,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
[config.fontSize, textSecondary]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View style={wrapperStyle}>
|
|
120
|
+
<View style={badgeStyle}>
|
|
121
|
+
<AtomicText
|
|
122
|
+
type="bodySmall"
|
|
123
|
+
style={textStyle}
|
|
124
|
+
>
|
|
125
|
+
+{count}
|
|
126
|
+
</AtomicText>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const AvatarGroup: React.FC<AvatarGroupProps> = React.memo(({
|
|
44
133
|
items,
|
|
45
134
|
maxVisible = AVATAR_CONSTANTS.MAX_GROUP_VISIBLE,
|
|
46
135
|
size = AVATAR_CONSTANTS.DEFAULT_SIZE,
|
|
@@ -51,76 +140,62 @@ export const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|
|
51
140
|
const tokens = useAppDesignTokens();
|
|
52
141
|
const config = SIZE_CONFIGS[size];
|
|
53
142
|
|
|
54
|
-
//
|
|
55
|
-
const visibleItems =
|
|
56
|
-
|
|
57
|
-
|
|
143
|
+
// Memoize calculations to prevent recalculation on every render
|
|
144
|
+
const { visibleItems, overflowCount, hasOverflow } = useMemo(() => {
|
|
145
|
+
const visItems = items.slice(0, maxVisible);
|
|
146
|
+
const overflow = items.length - maxVisible;
|
|
147
|
+
return {
|
|
148
|
+
visibleItems: visItems,
|
|
149
|
+
overflowCount: overflow,
|
|
150
|
+
hasOverflow: overflow > 0,
|
|
151
|
+
};
|
|
152
|
+
}, [items, maxVisible]);
|
|
153
|
+
|
|
154
|
+
// Memoize avatar style
|
|
155
|
+
const avatarStyle = useMemo(
|
|
156
|
+
() => [
|
|
157
|
+
styles.avatar,
|
|
158
|
+
{
|
|
159
|
+
borderWidth: 2,
|
|
160
|
+
borderColor: tokens.colors.onBackground,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
[tokens.colors.onBackground]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Stable key extractor
|
|
167
|
+
const keyExtractor = useCallback((item: AvatarGroupItem, index: number) => {
|
|
168
|
+
return item.uri || item.name || item.icon || `avatar-${index}`;
|
|
169
|
+
}, []);
|
|
58
170
|
|
|
59
171
|
return (
|
|
60
172
|
<View style={[styles.container, style]}>
|
|
61
173
|
{visibleItems.map((item, index) => (
|
|
62
|
-
<
|
|
63
|
-
key={item
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
name={item.name}
|
|
72
|
-
icon={item.icon}
|
|
73
|
-
size={size}
|
|
74
|
-
shape={shape}
|
|
75
|
-
style={[
|
|
76
|
-
styles.avatar,
|
|
77
|
-
{
|
|
78
|
-
borderWidth: 2,
|
|
79
|
-
borderColor: tokens.colors.onBackground,
|
|
80
|
-
},
|
|
81
|
-
]}
|
|
82
|
-
/>
|
|
83
|
-
</View>
|
|
174
|
+
<AvatarItem
|
|
175
|
+
key={keyExtractor(item, index)}
|
|
176
|
+
item={item}
|
|
177
|
+
index={index}
|
|
178
|
+
size={size}
|
|
179
|
+
shape={shape}
|
|
180
|
+
spacing={spacing}
|
|
181
|
+
avatarStyle={avatarStyle}
|
|
182
|
+
/>
|
|
84
183
|
))}
|
|
85
184
|
|
|
86
185
|
{hasOverflow && (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
width: config.size,
|
|
98
|
-
height: config.size,
|
|
99
|
-
borderRadius: shape === 'circle' ? config.size / 2 : shape === 'rounded' ? 8 : 0,
|
|
100
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
101
|
-
borderWidth: 2,
|
|
102
|
-
borderColor: tokens.colors.onBackground,
|
|
103
|
-
},
|
|
104
|
-
]}
|
|
105
|
-
>
|
|
106
|
-
<AtomicText
|
|
107
|
-
type="bodySmall"
|
|
108
|
-
style={[
|
|
109
|
-
styles.overflowText,
|
|
110
|
-
{
|
|
111
|
-
fontSize: config.fontSize,
|
|
112
|
-
color: tokens.colors.textSecondary,
|
|
113
|
-
},
|
|
114
|
-
]}
|
|
115
|
-
>
|
|
116
|
-
+{overflowCount}
|
|
117
|
-
</AtomicText>
|
|
118
|
-
</View>
|
|
119
|
-
</View>
|
|
186
|
+
<OverflowBadge
|
|
187
|
+
count={overflowCount}
|
|
188
|
+
spacing={spacing}
|
|
189
|
+
config={config}
|
|
190
|
+
shape={shape}
|
|
191
|
+
surfaceSecondary={tokens.colors.surfaceSecondary}
|
|
192
|
+
onBackground={tokens.colors.onBackground}
|
|
193
|
+
textSecondary={tokens.colors.textSecondary}
|
|
194
|
+
/>
|
|
120
195
|
)}
|
|
121
196
|
</View>
|
|
122
197
|
);
|
|
123
|
-
};
|
|
198
|
+
});
|
|
124
199
|
|
|
125
200
|
const styles = StyleSheet.create({
|
|
126
201
|
container: {
|