React Native Chat Composer with a Hold Photo Menu

Description:

expo-composer-hold-menu is a React Native and Expo chat composer with a press-and-hold recent-photo picker.

Holding the attachment button opens the photo menu, and selected images appear as animated cards in a shelf above the text input.

The component has controlled text, callbacks for attachment and voice actions, and theme-aware glass styling.

Preview

React Native Chat Composer

Run the Example

Use a native Expo build for the included modules.

git clone https://github.com/rit3zh/expo-composer-hold-menu
cd expo-composer-hold-menu
bun install
bunx expo prebuild
bun ios

For Android, use the project’s Android script:

bun android

Integrate the Composer

Copy src/components/composer.tsx with its imported components, hooks, icons, constants, types, and utilities. An existing Expo app needs the matching packages:

bunx expo install react-native-reanimated react-native-worklets react-native-gesture-handler react-native-keyboard-controller react-native-safe-area-context react-native-svg expo-media-library expo-image expo-glass-effect expo-linear-gradient expo-backdrop

Wrap the app once in Gesture Handler and Keyboard Controller. In an Expo Router app, use src/app/_layout.tsx:

import { Stack } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <KeyboardProvider enabled>
        <Stack screenOptions={{ headerShown: false }} />
      </KeyboardProvider>
    </GestureHandlerRootView>
  );
}

Basic Usage

The composer calls its recent-photo hook on mount and requests photo permission. Passing recentPhotos overrides the displayed array but does not disable that hook. Modify the hook or split the library-photo path out of the component if a custom-data screen must avoid that permission request.

import { useState } from "react";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { Composer } from "@/components/composer";
export function ChatInput() {
  const [message, setMessage] = useState("");
  return (
    <KeyboardAvoidingView
      behavior="padding"
      style={{ flex: 1, justifyContent: "flex-end" }}
    >
      <Composer
        value={message}
        onChangeText={setMessage}
        placeholder="Write a message"
        onPhotoSelect={(photo) => console.log("Selected", photo.uri)}
        onAttachPress={() => console.log("Open attachment options")}
        onDictatePress={() => console.log("Start dictation")}
        onVoicePress={() => console.log("Start voice mode")}
      />
    </KeyboardAvoidingView>
  );
}

Composer Props

PropType or behavior
value, onChangeTextControlled text and text-change callback
placeholderInput placeholder; default Ask ChatGPT
recentPhotosReadonly array of { id: string, uri: string } photo objects
onAttachPressCallback for a normal attachment-button tap
onPhotoSelectReceives the selected photo object from the hold menu
onDictatePressCallback for the microphone button
onVoicePressCallback for the voice-mode button
styleReact Native style for the outer container

Alternatives and Related Resources

Tags:

Add Comment