DEV Community

Cover image for How to Build a Social Network in 1 Day: Part 5 - Creating Posts
Tsabary
Tsabary

Posted on • Edited on • Originally published at replyke.com

How to Build a Social Network in 1 Day: Part 5 - Creating Posts

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the Code

  1. State Variables:
  • permission: Tracks the camera permissions.
  • cameraPhoto and galleryPhoto: Store the selected or captured photo.
  • step: Determines the current view (capture, preview, or finalize).
  1. resetCreateScreen Function: Resets the state to start the process from the Capture Photo view.

  2. Conditional Rendering:

    • Displays RequestPermission if camera permissions are not granted.
    • Shows PhotoCapture, PhotoPreview, or FinalizePost components based on the step state and photo data.

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;
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. 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.
  2. Props:

    • The requestPermission prop is a function that prompts the user to grant camera access. This function is passed down from the parent CreateScreen component.
  3. 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 the requestPermission function.

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;
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. 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.
  2. Props:

    • setCameraPhoto and setGalleryPhoto: Functions to update the state for the captured or selected photo.
    • setStep: Updates the current step in the Create Post workflow.
  3. 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.
  4. 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.

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;
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. 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.
  2. Props:

    • cameraPhoto and galleryPhoto: 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.
  3. 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.

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);
      }
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. Purpose:

    • This utility ensures images are resized to a maximum dimension of 800px for better performance and upload efficiency.
  2. 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.
  3. 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;
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. Purpose:

    • Allows users to add a caption and submit their post.
    • Ensures the image is resized for optimal performance.
  2. Key Functions:

    • resizeIfNeeded: Resizes the image if its dimensions exceed 800px.
    • handleUpload: Uploads the image and creates the post entity.
  3. 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)