Skip to content

Commit

Permalink
Optimize media library handling and photo grid rendering in Expo app
Browse files Browse the repository at this point in the history
* ♻️ optimize photo grid performance with memoization and image caching

The commit improves performance by:
- Adding memoization for grid items and layout calculations
- Implementing FlatList performance optimizations
- Enhancing image caching and transition settings
- Adding photo prefetching

* ♻️ refactor(media): improve media library hook with better error handling and DRY

* ♻️ refactor(PhotoGrid): improve grid layout with consistent spacing and gaps

* ✨ feat: enhance photo grid and capture button UI components

The commit improves the UI by fixing the capture button position, adjusting its
styling, and optimizing the photo grid layout with proper bottom padding and
TypeScript types
  • Loading branch information
jaronheard authored Jan 14, 2025
1 parent 78b83c6 commit 9f281ee
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 134 deletions.
4 changes: 3 additions & 1 deletion apps/expo/src/app/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PhotoGrid } from "~/components/PhotoGrid";
import { useCreateEvent } from "~/hooks/useCreateEvent";
import { useInitializeInput } from "~/hooks/useInitializeInput";
import { useKeyboardHeight } from "~/hooks/useKeyboardHeight";
import { useMediaLibrary } from "~/hooks/useMediaLibrary";
import { useNotification } from "~/providers/NotificationProvider";
import { useAppStore } from "~/store";
import { cn } from "~/utils/cn";
Expand All @@ -24,6 +25,7 @@ export default function NewEventModal() {
const { expoPushToken, hasNotificationPermission } = useNotification();
const { user } = useUser();
const { createEvent } = useCreateEvent();
useMediaLibrary();

const {
input,
Expand Down Expand Up @@ -281,7 +283,7 @@ export default function NewEventModal() {
</View>

<Animated.View
className={cn("px-4")}
className={cn("absolute bottom-0 left-0 right-0 px-4")}
style={{ marginBottom: marginBottomAnim }}
>
<CaptureEventButton
Expand Down
32 changes: 15 additions & 17 deletions apps/expo/src/components/CaptureEventButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,24 @@ export function CaptureEventButton({

return (
<View className={containerClassName}>
<View className="overflow-hidden rounded-full bg-white/10 p-[1px] shadow-lg">
<Pressable
onPress={handleCreateEvent}
disabled={isDisabled}
<Pressable
onPress={handleCreateEvent}
disabled={isDisabled}
className={cn(
"w-full flex-row items-center justify-center rounded-full px-3 py-3.5 shadow-lg",
isDisabled ? "bg-neutral-3" : "bg-white",
)}
>
<Sparkles size={20} color={isDisabled ? "#627496" : "#5A32FB"} />
<Text
className={cn(
"w-full flex-row items-center justify-center rounded-full px-3 py-3.5",
isDisabled ? "bg-neutral-3" : "bg-white",
"ml-2 text-2xl font-bold",
isDisabled ? "text-neutral-2" : "text-[#5A32FB]",
)}
>
<Sparkles size={16} color={isDisabled ? "#627496" : "#5A32FB"} />
<Text
className={cn(
"ml-2 text-xl font-bold",
isDisabled ? "text-neutral-2" : "text-[#5A32FB]",
)}
>
Capture event
</Text>
</Pressable>
</View>
Capture event
</Text>
</Pressable>
</View>
);
}
7 changes: 5 additions & 2 deletions apps/expo/src/components/EventPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ export function EventPreview({
style={{ width: "100%", height: "100%" }}
contentFit="contain"
contentPosition="center"
transition={100}
cachePolicy="disk"
transition={200}
cachePolicy="memory-disk"
placeholder={null}
placeholderContentFit="contain"
className="bg-muted/10"
/>
<Pressable
onPress={clearPreview}
Expand Down
223 changes: 144 additions & 79 deletions apps/expo/src/components/PhotoGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import {
ActionSheetIOS,
Dimensions,
Expand Down Expand Up @@ -26,6 +26,90 @@ interface PhotoGridProps {
containerClassName?: string;
}

// Memoize the grid item component
const GridItem = React.memo(
({
item,
imageSize,
onPhotoSelect,
selectedUri,
hasMediaPermission,
hasFullPhotoAccess,
onMorePhotos,
}: {
item: RecentPhoto & { id: string };
imageSize: number;
onPhotoSelect: (uri: string) => void;
selectedUri: string | null;
hasMediaPermission: boolean;
hasFullPhotoAccess: boolean;
onMorePhotos: () => void;
}) => {
if (item.id === "plus-button") {
return (
<Pressable
onPress={() => {
if (hasMediaPermission && !hasFullPhotoAccess) {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ["Select More Photos", "Change Settings", "Cancel"],
cancelButtonIndex: 2,
},
(buttonIndex) => {
if (buttonIndex === 0) {
void MediaLibrary.presentPermissionsPickerAsync();
} else if (buttonIndex === 1) {
void Linking.openSettings();
}
},
);
} else {
onMorePhotos();
}
}}
style={{
width: imageSize,
height: imageSize,
}}
className="items-center justify-center bg-interactive-3"
>
<ImagePlus size={36} color="#5A32FB" />
</Pressable>
);
}

return (
<Pressable
onPress={() => onPhotoSelect(item.uri)}
className={cn(
"aspect-square bg-muted/20",
selectedUri === item.uri && "opacity-50",
)}
style={{
width: imageSize,
height: imageSize,
}}
>
<ExpoImage
source={{ uri: item.uri }}
style={{
width: "100%",
height: "100%",
}}
contentFit="cover"
contentPosition="center"
transition={200}
cachePolicy="memory-disk"
recyclingKey={item.id}
placeholder={null}
placeholderContentFit="cover"
className="bg-muted/10"
/>
</Pressable>
);
},
);

export const PhotoGrid = React.memo(
({
hasMediaPermission,
Expand All @@ -38,18 +122,62 @@ export const PhotoGrid = React.memo(
containerClassName,
}: PhotoGridProps) => {
const windowWidth = Dimensions.get("window").width;
const spacing = 1;
const spacing = 2;
const columns = 4;
const imageSize = (windowWidth - (columns - 1) * spacing) / columns;
const containerPadding = 0;
const availableWidth =
windowWidth - spacing * (columns - 1) - containerPadding * 2;
const imageSize = availableWidth / columns;

const handleManagePress = () => {
void Linking.openSettings();
};

// Only show the plus button if we have media permission
const gridData = hasMediaPermission
? [...recentPhotos, { id: "plus-button", uri: "" }]
: [];
const gridData = useMemo(
() =>
hasMediaPermission
? [...recentPhotos, { id: "plus-button", uri: "" }]
: [],
[hasMediaPermission, recentPhotos],
);

// Memoize the getItemLayout function
const getItemLayout = useMemo(
() =>
(
_data: ArrayLike<RecentPhoto & { id: string }> | null | undefined,
index: number,
) => ({
length: imageSize,
offset: imageSize * Math.floor(index / columns),
index,
}),
[imageSize, columns],
);

const renderItem = useMemo(
() =>
({ item }: { item: RecentPhoto & { id: string } }) => (
<GridItem
item={item}
imageSize={imageSize}
onPhotoSelect={onPhotoSelect}
selectedUri={selectedUri}
hasMediaPermission={hasMediaPermission}
hasFullPhotoAccess={hasFullPhotoAccess}
onMorePhotos={onMorePhotos}
/>
),
[
imageSize,
onPhotoSelect,
selectedUri,
hasMediaPermission,
hasFullPhotoAccess,
onMorePhotos,
],
);

return (
<View className={cn("flex-1", containerClassName)}>
Expand Down Expand Up @@ -89,85 +217,22 @@ export const PhotoGrid = React.memo(
</View>
)}

<View className="flex-1">
<View className="-mx-4 flex-1">
<FlatList
data={gridData}
renderItem={({ item }) => {
if (item.id === "plus-button") {
return (
<Pressable
onPress={() => {
if (hasMediaPermission && !hasFullPhotoAccess) {
// Show action sheet for partial access
ActionSheetIOS.showActionSheetWithOptions(
{
options: [
"Select More Photos",
"Change Settings",
"Cancel",
],
cancelButtonIndex: 2,
},
(buttonIndex) => {
if (buttonIndex === 0) {
void MediaLibrary.presentPermissionsPickerAsync();
} else if (buttonIndex === 1) {
void Linking.openSettings();
}
},
);
} else {
onMorePhotos();
}
}}
style={{
width: imageSize,
height: imageSize,
marginVertical: spacing / 2,
marginHorizontal: spacing / 2,
}}
className="items-center justify-center bg-interactive-3"
>
<ImagePlus size={36} color="#5A32FB" />
</Pressable>
);
}

return (
<Pressable
onPress={() => onPhotoSelect(item.uri)}
className={cn(
"aspect-square bg-muted/20",
selectedUri === item.uri && "opacity-50",
)}
style={{
width: imageSize,
height: imageSize,
marginVertical: spacing / 2,
marginHorizontal: spacing / 2,
}}
>
<ExpoImage
source={{ uri: item.uri }}
style={{
width: "100%",
height: "100%",
}}
contentFit="cover"
contentPosition="center"
transition={100}
cachePolicy="disk"
/>
</Pressable>
);
}}
renderItem={renderItem}
numColumns={4}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: 100,
}}
keyExtractor={(item) => item.id}
horizontal={false}
windowSize={5}
maxToRenderPerBatch={16}
updateCellsBatchingPeriod={50}
initialNumToRender={12}
removeClippedSubviews={true}
getItemLayout={getItemLayout}
contentContainerStyle={{ paddingBottom: 140, gap: spacing }}
columnWrapperStyle={{ gap: spacing }}
/>
</View>
</View>
Expand Down
Loading

0 comments on commit 9f281ee

Please sign in to comment.