In this part of the tutorial, we’ll implement the Create Post screen—an essential feature of any social networking app. Users will be able to upload an image, add a caption, and create their very first post. This screen is comprised of four views, each serving a distinct purpose in the post creation process:
1. Asking for Permission
Before using the user’s camera, we need to ask for permission. When navigating to the Create Post screen for the first time, users will see a prompt asking them to grant camera access. This view will only appear once, during the app’s initial setup. After permission is granted, users won’t see this screen again.
2. Capture Photo View
The second view is where users interact with their camera. This is the default view every time they access the Create Post screen after the first usage. Here, users can:
- Capture a photo.
- Choose an image from their gallery.
- Flip the camera between front and back.
Once the user selects or captures a photo, they are automatically directed to the next screen: the Preview Screen.
3. Preview Screen
The Preview Screen allows users to view their selected photo in full. From here, they can:
- Discard the photo and return to the Capture Photo View.
- Proceed to the next stage by clicking Next, which takes them to the Finalize Post View.
4. Finalize Post View
In the Finalize Post View, users can:
- View a thumbnail of their selected photo.
- Enter a caption for their post using a text input field.
- Submit their post by clicking the Submit button.
Upon submission, the app will first upload the file to Replyke. After a successful upload, we’ll create a new entity in Replyke using the post details and file data.
Key Considerations for Readers
1. Leveraging Replyke to Add Unique Features
While this tutorial focuses on building the foundational features of a social network, Replyke offers a wide range of tools to customize your app and make it unique. For example:
-
Additional Fields: When creating entities, we’ll use the basic "title" and "media" fields. However, Replyke also supports fields like:
- Keywords: Add tags for better feed filtration.
- Content: Include both a title and detailed content for posts.
- Location: Enable sorting posts by geolocation.
- Metadata: Store extra information using a flexible JSON object.
These additional features can help developers create a unique value proposition for their app and enable advanced feed filtering options.
2. Keeping UI Design Minimal
Our primary focus in this tutorial is functionality, not design. We will place elements in an intuitive way, but they won’t have cutting-edge styling. It’s up to you as a developer to elevate your app’s design and make it stand out.
To assist with design inspiration, consider joining the Discord server where boilerplates for social network apps—including more advanced styles—are shared.
With this solid overview in place, let’s move on to the technical implementation of the Create Post screen. By the end of this part, you’ll have a fully functional workflow for users to create and share posts in your app. Let’s get started!
Accumulated reading time so far: 18 minutes.
Part 5 completed repo on GitHub for reference
Installing Required Packages
To get started, install the following packages in your project:
npx expo install expo-camera expo-image-picker expo-image-manipulator
These libraries will handle camera access, image selection, and basic image manipulation.
Setting Up the Folder Structure
Create a components
folder in the root of your project. Inside it, create another folder named create-post
to store the components for this feature. Add the following files to the create-post
folder:
components
- create-post
- FinalizePost.tsx
- PhotoCapture.tsx
- PhotoPreview.tsx
- RequestPermission.tsx
We will populate these components in the following sections of this article.
Adding the Create Post Screen
Add the following code to your Create Post screen file:
import { useState } from "react";
import { View } from "react-native";
import { useCameraPermissions, CameraCapturedPicture } from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import { useUser } from "replyke-expo";
import { Redirect } from "expo-router";
import PhotoCapture from "../../components/create-post/PhotoCapture";
import PhotoPreview from "../../components/create-post/PhotoPreview";
import RequestPermission from "../../components/create-post/RequestPermission";
import FinalizePost from "../../components/create-post/FinalizePost";
export default function CreateScreen() {
const { user } = useUser();
const [permission, requestPermission] = useCameraPermissions();
const [cameraPhoto, setCameraPhoto] = useState<CameraCapturedPicture | null>(
null
);
const [galleryPhoto, setGalleryPhoto] =
useState<ImagePicker.ImagePickerAsset | null>(null);
const [step, setStep] = useState<"capture" | "preview" | "finalize">(
"capture"
);
const resetCreateScreen = () => {
setCameraPhoto(null);
setGalleryPhoto(null);
setStep("capture");
};
if (!user) return <Redirect href="/sign-in" />;
if (!permission) {
// Camera permissions are still loading.
return <View />;
}
if (!permission.granted) {
// Camera permissions are not granted yet.
return <RequestPermission requestPermission={requestPermission} />;
}
if (step === "finalize" && (cameraPhoto || galleryPhoto)) {
return (
<FinalizePost
setStep={setStep}
cameraPhoto={cameraPhoto}
galleryPhoto={galleryPhoto}
resetCreateScreen={resetCreateScreen}
/>
);
}
if (step === "preview" && (cameraPhoto || galleryPhoto)) {
return (
<PhotoPreview
cameraPhoto={cameraPhoto}
galleryPhoto={galleryPhoto}
setStep={setStep}
resetCreateScreen={resetCreateScreen}
/>
);
}
return (
<PhotoCapture
setCameraPhoto={setCameraPhoto}
setGalleryPhoto={setGalleryPhoto}
setStep={setStep}
/>
);
}
Explanation of the Code
- State Variables:
-
permission
: Tracks the camera permissions. -
cameraPhoto
andgalleryPhoto
: Store the selected or captured photo. -
step
: Determines the current view (capture, preview, or finalize).
resetCreateScreen
Function: Resets the state to start the process from the Capture Photo view.-
Conditional Rendering:
- Displays
RequestPermission
if camera permissions are not granted. - Shows
PhotoCapture
,PhotoPreview
, orFinalizePost
components based on thestep
state and photo data.
- Displays
In the next sections, we’ll dive into the implementation of each component individually.
In this section, we will implement the individual components, starting with the RequestPermission
component. This is a simple yet critical part of the Create Post feature. It handles asking the user for camera permissions when they navigate to the Create Post screen for the first time.
Code for RequestPermission Component
Below is the code for the RequestPermission
component:
import { View, Text } from "react-native";
import React from "react";
import { Button } from "react-native";
import * as ImagePicker from "expo-image-picker";
// The RequestPermission component is responsible for requesting camera permissions
// when the user navigates to the Create Post screen for the first time.
const RequestPermission = ({
requestPermission, // Function passed as a prop to trigger the permission request.
}: {
requestPermission: () => Promise<ImagePicker.PermissionResponse>; // Defines the type of the prop to ensure it's a function that returns a Promise.
}) => {
return (
<View className="flex-1">
{/* Full-screen container with content vertically centered */}
<View className="flex-1 justify-center">
{/* Text to explain why camera permission is needed */}
<Text className="pb-2.5 text-center">
We need your permission to show the camera
</Text>
{/* Button to trigger the requestPermission function */}
<Button onPress={requestPermission} title="grant permission" />
</View>
</View>
);
};
export default RequestPermission;
Explanation
-
Purpose:
- The
RequestPermission
component handles the UI and logic for requesting camera permissions. - It is displayed only if the app does not yet have the required permissions.
- The
-
Props:
- The
requestPermission
prop is a function that prompts the user to grant camera access. This function is passed down from the parentCreateScreen
component.
- The
-
Structure:
- The component renders a
View
container to take up the full screen. - Inside the container, a
Text
component displays a message explaining why the app requires camera permissions. - A
Button
component is provided for the user to grant permission, triggering therequestPermission
function.
- The component renders a
This component is simple and straightforward, focusing on user interaction for permission handling. Once the user grants permission, the app will navigate to the next step in the workflow.
Code for PhotoCapture Component
Now, let’s implement the PhotoCapture
component. This component allows users to capture a photo, choose one from the gallery, or switch between the front and back cameras.
import React, { useRef, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { CameraView, CameraType, CameraCapturedPicture } from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
import FontAwesome from "@expo/vector-icons/FontAwesome";
function PhotoCapture({
setCameraPhoto,
setGalleryPhoto,
setStep,
}: {
setCameraPhoto: React.Dispatch<
React.SetStateAction<CameraCapturedPicture | null>
>;
setGalleryPhoto: React.Dispatch<
React.SetStateAction<ImagePicker.ImagePickerAsset | null>
>;
setStep: React.Dispatch<
React.SetStateAction<"capture" | "preview" | "finalize">
>;
}) {
const [facing, setFacing] = useState<CameraType>("back");
const cameraRef = useRef<CameraView>(null);
const pickFromGallery = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
quality: 1,
aspect: [9, 16],
});
if (!result.canceled) {
setGalleryPhoto(result.assets[0]);
setCameraPhoto(null); // Clear the camera photo if you switch sources
setStep("preview");
}
};
const capturePhoto = async () => {
if (cameraRef.current) {
const options = { quality: 1, base64: true };
const capturedPhoto = await cameraRef.current.takePictureAsync(options);
setCameraPhoto(capturedPhoto ?? null);
setGalleryPhoto(null); // Clear the gallery photo if you switch sources
setStep("preview");
}
};
function toggleCameraFacing() {
setFacing((current) => (current === "back" ? "front" : "back"));
}
return (
<View className="flex-1 justify-center">
<CameraView style={{ flex: 1 }} facing={facing} ref={cameraRef}>
<View className="absolute bottom-12 left-0 right-0 flex-row items-center w-full bg-transparent justify-around">
<TouchableOpacity
className="p-4 bg-black/10 rounded-full"
onPress={pickFromGallery}
>
<FontAwesome name="picture-o" size={24} color="white" />
</TouchableOpacity>
<TouchableOpacity
onPress={capturePhoto}
className="border-4 border-white rounded-full"
>
<View className="bg-white rounded-full size-16 border-4" />
</TouchableOpacity>
<TouchableOpacity
className="p-4 bg-black/10 rounded-full"
onPress={toggleCameraFacing}
>
<FontAwesome6 name="rotate" size={24} color="white" />
</TouchableOpacity>
</View>
</CameraView>
</View>
);
}
export default PhotoCapture;
Explanation
-
Purpose:
- The
PhotoCapture
component provides the main interface for users to capture a photo or select one from their gallery. - Users can also toggle between the front and back cameras.
- The
-
Props:
-
setCameraPhoto
andsetGalleryPhoto
: Functions to update the state for the captured or selected photo. -
setStep
: Updates the current step in the Create Post workflow.
-
-
Functions:
-
pickFromGallery
: Opens the device's image library and allows the user to select a photo. -
capturePhoto
: Uses the camera to capture a photo and saves it to the state. -
toggleCameraFacing
: Switches the camera between front and back.
-
-
Structure:
- A
CameraView
component displays the live camera feed. - Three buttons allow the user to:
- Open the gallery.
- Capture a photo.
- Toggle the camera facing.
- A
This component plays a vital role in capturing or selecting the image that will be part of the post. In the next section, we’ll explore the implementation of the PhotoPreview
component.
Code for PhotoPreview Component
Now, let’s implement the PhotoPreview
component. This component allows users to review the photo they’ve captured or selected and decide whether to proceed or discard it.
import { Image, Pressable, Text, View } from "react-native";
import { CameraCapturedPicture } from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import AntDesign from "@expo/vector-icons/AntDesign";
function PhotoPreview({
cameraPhoto,
galleryPhoto,
setStep,
resetCreateScreen,
}: {
cameraPhoto: CameraCapturedPicture | null;
galleryPhoto: ImagePicker.ImagePickerAsset | null;
setStep: React.Dispatch<
React.SetStateAction<"capture" | "preview" | "finalize">
>;
resetCreateScreen: () => void;
}) {
return (
<View className="flex-1 bg-black">
<Image
source={{ uri: (cameraPhoto ?? galleryPhoto)?.uri }}
className="flex-1"
resizeMode="cover"
/>
<View className="flex-1 absolute top-0 left-0 right-0 z-50 flex-row justify-between p-3">
<Pressable
onPress={() => {
resetCreateScreen();
}}
className="px-3 py-2 bg-blue-500 flex-row items-center gap-2 rounded-xl"
>
<AntDesign name="arrowleft" size={16} color="white" />
<Text className="text-lg tracking-wide text-white">Preview</Text>
</Pressable>
<Pressable
onPress={() => setStep("finalize")}
className="px-3 bg-blue-500 flex-row items-center gap-2 rounded-xl"
>
<Text className="text-lg tracking-wide text-white">Next</Text>
</Pressable>
</View>
</View>
);
}
export default PhotoPreview;
Explanation
-
Purpose:
- The
PhotoPreview
component lets users review the selected or captured image before finalizing their post. - It provides options to go back and retake/select another photo or proceed to add a caption.
- The
-
Props:
-
cameraPhoto
andgalleryPhoto
: Represent the image captured by the camera or selected from the gallery. -
setStep
: Controls navigation to the next step in the post creation process. -
resetCreateScreen
: Resets the entire flow, returning the user to the initial camera view.
-
-
Structure:
- Displays the photo in full-screen mode using the
Image
component. - Two
Pressable
buttons are provided:- One for returning to the photo capture step.
- One for proceeding to the finalization step.
- Displays the photo in full-screen mode using the
This component is key for offering a seamless and user-friendly post creation experience. In the next section, we will implement the FinalizePost
component.
Code for FinalizePost Component
In this final step, users can review their selected image, add a caption, and submit their post. We’ll also include a utility function to resize images if necessary.
Creating the resizeIfNeeded
Utility
First, create a new file named resizeIfNeeded.ts
in the utils
folder and add the following code:
import { Image } from "react-native";
import { manipulateAsync, SaveFormat } from "expo-image-manipulator";
export async function resizeIfNeeded(uri: string): Promise<{ uri: string; extension: string }> {
return new Promise((resolve, reject) => {
Image.getSize(
uri,
async (originalWidth, originalHeight) => {
try {
const maxDim = 800;
// Extract the file extension from the URI (if available)
const originalExtensionMatch = uri.match(/\.(\w+)$/);
const originalExtension = originalExtensionMatch ? originalExtensionMatch[1].toLowerCase() : "jpg";
// If the image is small enough, keep the original extension
if (originalWidth <= maxDim && originalHeight <= maxDim) {
return resolve({ uri, extension: originalExtension });
}
// Resize logic
let newWidth: number;
let newHeight: number;
if (originalWidth > originalHeight) {
// Landscape image -> limit width to 800, scale height
const scaleFactor = maxDim / originalWidth;
newWidth = Math.floor(originalWidth * scaleFactor);
newHeight = Math.floor(originalHeight * scaleFactor);
} else {
// Portrait or square -> limit height to 800, scale width
const scaleFactor = maxDim / originalHeight;
newWidth = Math.floor(originalWidth * scaleFactor);
newHeight = Math.floor(originalHeight * scaleFactor);
}
// Manipulate the image
const { uri: resizedUri } = await manipulateAsync(
uri,
[{ resize: { width: newWidth, height: newHeight } }],
{ compress: 0.7, format: SaveFormat.JPEG }
);
resolve({ uri: resizedUri, extension: "jpg" });
} catch (err) {
reject(err);
}
},
(error) => {
reject(error);
}
);
});
}
Explanation
-
Purpose:
- This utility ensures images are resized to a maximum dimension of 800px for better performance and upload efficiency.
-
Logic:
- If the image dimensions exceed 800px, the function calculates the new dimensions while preserving the aspect ratio.
- The
manipulateAsync
function resizes and compresses the image before returning the new URI.
-
Usage:
- The resized image is returned as a URI, ready for uploading.
Code for FinalizePost
Component
import {
ActivityIndicator,
Image,
Pressable,
Text,
TextInput,
View,
TouchableOpacity,
} from "react-native";
import React, { useState } from "react";
import { useCreateEntity, useUploadFile, useUser } from "replyke-expo";
import { useRouter } from "expo-router";
import { CameraCapturedPicture } from "expo-camera";
import * as ImagePicker from "expo-image-picker";
import AntDesign from "@expo/vector-icons/AntDesign";
import { resizeIfNeeded } from "../../utils/resizeIfNeeded";
const FinalizePost = ({
cameraPhoto,
galleryPhoto,
setStep,
resetCreateScreen,
}: {
cameraPhoto: CameraCapturedPicture | null;
galleryPhoto: ImagePicker.ImagePickerAsset | null;
setStep: React.Dispatch<
React.SetStateAction<"capture" | "preview" | "finalize">
>;
resetCreateScreen: () => void;
}) => {
const router = useRouter();
const { user } = useUser();
const uploadFile = useUploadFile();
const createEntity = useCreateEntity();
const [uploading, setUploading] = useState(false);
const [caption, setCaption] = useState("");
const handleUpload = async (imageUri: string) => {
if (!imageUri || !user) return;
setUploading(true);
try {
// Resize to ensure neither side exceeds 800
const { uri, extension } = await resizeIfNeeded(imageUri);
// Prepare the file for upload (in React Native style)
const rnFile = {
uri: uri,
type: `image/${extension}`,
name: `${Date.now()}.${extension}`,
};
// Upload the resized file to your storage:
const pathParts = ["posts", user.id];
const uploadResponse = await uploadFile(rnFile, pathParts);
if (uploadResponse) {
await createEntity({
title: caption,
media: [{ ...uploadResponse, extension }],
});
}
// Navigate away and reset
router.navigate("/");
resetCreateScreen();
} catch (error) {
console.error("Error uploading file:", error);
} finally {
setUploading(false);
}
};
return (
<View className="flex-1 bg-white">
<Pressable
onPress={() => setStep("preview")}
className="m-3 px-3 py-2 flex-row items-center gap-2 rounded-xl"
>
<AntDesign name="arrowleft" size={16} color="black" />
<Text className="text-lg tracking-wide">New Post</Text>
</Pressable>
<View className="flex-1">
<View className="h-64">
<Image
source={{ uri: (cameraPhoto ?? galleryPhoto)?.uri }}
className="flex-1"
resizeMode="cover"
/>
</View>
<TextInput
value={caption}
onChangeText={(value) => {
if (value.length > 120) return;
setCaption(value);
}}
className="m-3 text-left border-b border-gray-300"
placeholder="Add caption.."
placeholderTextColor="#9ca3af"
autoCapitalize="none"
/>
</View>
<TouchableOpacity
onPress={() => handleUpload((cameraPhoto ?? galleryPhoto)!.uri)}
disabled={uploading}
className="px-3 py-4 flex-row items-center justify-center gap-2 rounded-xl bg-black m-3 "
>
{uploading && <ActivityIndicator />}
<Text className="text-lg tracking-wide text-white text-center">
Publish
</Text>
</TouchableOpacity>
</View>
);
};
export default FinalizePost;
Explanation
-
Purpose:
- Allows users to add a caption and submit their post.
- Ensures the image is resized for optimal performance.
-
Key Functions:
-
resizeIfNeeded
: Resizes the image if its dimensions exceed 800px. -
handleUpload
: Uploads the image and creates the post entity.
-
-
User Flow:
- Displays the selected image.
- Captures user input for the caption.
- Handles uploading and navigation after submission.
Wrapping Up
In this part of our tutorial, we built the Create Post feature of our social networking app. We implemented components to request camera permissions, capture or select an image, preview the photo, and finalize the post with a caption. Along the way, we also introduced a utility function to resize images for better performance.
With the Create Post functionality completed, we are now ready to move to the next exciting part of this series: building the feed! In the upcoming chapter, we will create a dynamic feed to display posts, allowing users to browse and interact with content shared on the app.
If you're looking to take your app even further, explore the documentation to learn how you can customize the entity with additional data and functionality beyond what we've covered here. Stay tuned!
Stay Updated
Don’t forget to join the Discord server where I post free boilerplate code repos for different types of social networks. Updates about the next article and additional resources will also be shared there. Lastly, follow me for updates here and on X/Twitter & BlueSky.
Top comments (0)