├── .gitignore ├── DUMMY.env ├── README.md ├── app.json ├── app ├── (chat) │ └── [chatid].tsx ├── (modal) │ └── create.tsx ├── _layout.tsx └── index.tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── chatGroups.jsonl ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── greeting.ts ├── groups.ts ├── http.ts ├── messages.ts ├── schema.ts └── tsconfig.json ├── metro.config.js ├── package-lock.json ├── package.json ├── screenshots ├── 1.png ├── 2.png └── 3.png └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | # @end expo-cli 42 | .vscode/ -------------------------------------------------------------------------------- /DUMMY.env: -------------------------------------------------------------------------------- 1 | # Deployment used by `npx convex dev` 2 | CONVEX_DEPLOYMENT=dev:XXX 3 | EXPO_PUBLIC_CONVEX_URL="https://XXX.convex.cloud" 4 | EXPO_PUBLIC_CONVEX_SITE="https://XXX.convex.site" 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Best Realtime React Native Chat (Expo, Typescript, File Upload, Convex) 2 | 3 | This is a React Native project using [Expo](https://expo.dev/) and [Convex backend](https://www.convex.dev/). 4 | 5 | Features: 6 | 7 | - Convex database functions & schema 🚀 8 | - Realtime chat ⚡️ 9 | - File upload to Convex 📁 10 | - Expo file-based Router 🛣 11 | - React Native Dialog 📱 12 | - Expo Async Storage 📦 13 | - Typescript ❤️ 14 | 15 | ## Convex Setup 16 | 17 | All [Convex](https://www.convex.dev/) related code is in the `convex` folder: 18 | 19 | - `convex/groups.ts` - Convex mutations and queries for groups table 20 | - `convex/messages.ts` - Convex mutations and queries for messages table 21 | - `convex/schema.ts` - Convex schema for groups and messages tables 22 | - `convex/http.ts` - Custom endpoint for file upload 23 | - `convex/greetings.ts` - Convex actions example 24 | 25 | 26 | ## App Screenshots 27 | 28 |
29 | 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "convexChat", 4 | "slug": "convexChat", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/images/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | } 24 | }, 25 | "web": { 26 | "bundler": "metro", 27 | "output": "static", 28 | "favicon": "./assets/images/favicon.png" 29 | }, 30 | "plugins": [ 31 | "expo-router", 32 | [ 33 | "expo-image-picker", 34 | { 35 | "photosPermission": "The app accesses your photos to let you share them with your friends." 36 | } 37 | ] 38 | ], 39 | "experiments": { 40 | "tsconfigPaths": true, 41 | "typedRoutes": true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/(chat)/[chatid].tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, SafeAreaView, TouchableOpacity, TextInput, StyleSheet, FlatList, ListRenderItem, KeyboardAvoidingView, Platform, Image, Keyboard, ActivityIndicator } from 'react-native'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router'; 4 | import { useConvex, useMutation, useQuery } from 'convex/react'; 5 | import { api } from '@/convex/_generated/api'; 6 | import { Doc, Id } from '@/convex/_generated/dataModel'; 7 | import AsyncStorage from '@react-native-async-storage/async-storage'; 8 | import { Ionicons } from '@expo/vector-icons'; 9 | import * as ImagePicker from 'expo-image-picker'; 10 | 11 | const Page = () => { 12 | const { chatid } = useLocalSearchParams(); 13 | const [newMessage, setNewMessage] = useState(''); 14 | const addMessage = useMutation(api.messages.sendMessage); 15 | const messages = useQuery(api.messages.get, { chatId: chatid as Id<'groups'> }) || []; 16 | const [user, setUser] = useState(null); 17 | const listRef = useRef(null); 18 | const [selectedImage, setSelectedImage] = useState(null); 19 | const [uploading, setUploading] = useState(false); 20 | const convex = useConvex(); 21 | const navigation = useNavigation(); 22 | 23 | // Load group name and set header title 24 | useEffect(() => { 25 | const loadGroup = async () => { 26 | const groupInfo = await convex.query(api.groups.getGroup, { id: chatid as Id<'groups'> }); 27 | console.log(groupInfo); 28 | navigation.setOptions({ headerTitle: groupInfo!.name }); 29 | }; 30 | loadGroup(); 31 | }, [chatid]); 32 | 33 | // Load user from async storage 34 | useEffect(() => { 35 | const loadUser = async () => { 36 | const user = await AsyncStorage.getItem('user'); 37 | setUser(user); 38 | }; 39 | 40 | loadUser(); 41 | }, []); 42 | 43 | // Scroll to bottom when new message is added 44 | useEffect(() => { 45 | setTimeout(() => { 46 | listRef.current!.scrollToEnd({ animated: true }); 47 | }, 300); 48 | }, [messages]); 49 | 50 | // Send message to Convex 51 | // Optionally convert image from URI to blob and use special site endpoint 52 | const handleSendMessage = async () => { 53 | Keyboard.dismiss(); 54 | 55 | if (selectedImage) { 56 | // Use SITE instead of URL in here!!! 57 | const url = `${process.env.EXPO_PUBLIC_CONVEX_SITE}/sendImage?user=${encodeURIComponent(user!)}&group_id=${chatid}&content=${encodeURIComponent(newMessage)}`; 58 | setUploading(true); 59 | 60 | // Convert URI to blob 61 | const response = await fetch(selectedImage); 62 | const blob = await response.blob(); 63 | 64 | // Send blob to Convex 65 | fetch(url, { 66 | method: 'POST', 67 | headers: { 'Content-Type': blob!.type }, 68 | body: blob, 69 | }) 70 | .then(() => { 71 | setSelectedImage(null); 72 | setNewMessage(''); 73 | }) 74 | .catch((err) => console.log('ERROR: ', err)) 75 | .finally(() => setUploading(false)); 76 | } else { 77 | // Regular mutation to add a message 78 | await addMessage({ 79 | group_id: chatid as Id<'groups'>, 80 | content: newMessage, 81 | user: user || 'Anonymous', 82 | }); 83 | setNewMessage(''); 84 | } 85 | }; 86 | 87 | // Open image picker and set selected image 88 | const captureImage = async () => { 89 | const result = await ImagePicker.launchImageLibraryAsync({ 90 | mediaTypes: ImagePicker.MediaTypeOptions.Images, 91 | quality: 1, 92 | }); 93 | 94 | if (!result.canceled) { 95 | const uri = result.assets[0].uri; 96 | setSelectedImage(uri); 97 | } 98 | }; 99 | 100 | // Render a message 101 | // Use conditional styling and Convex data model 102 | const renderMessage: ListRenderItem> = ({ item }) => { 103 | const isUserMessage = item.user === user; 104 | 105 | return ( 106 | 107 | {item.content !== '' && {item.content}} 108 | {item.file && } 109 | 110 | {new Date(item._creationTime).toLocaleTimeString()} - {item.user} 111 | 112 | 113 | ); 114 | }; 115 | 116 | return ( 117 | 118 | 119 | {/* Render the messages */} 120 | item._id.toString()} ListFooterComponent={} /> 121 | 122 | {/* Bottom message input */} 123 | 124 | {selectedImage && } 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | {/* Cover screen while uploading image */} 139 | {uploading && ( 140 | 149 | 150 | 151 | )} 152 | 153 | ); 154 | }; 155 | 156 | const styles = StyleSheet.create({ 157 | container: { 158 | flex: 1, 159 | backgroundColor: '#F8F5EA', 160 | }, 161 | inputContainer: { 162 | padding: 10, 163 | backgroundColor: '#fff', 164 | alignItems: 'center', 165 | shadowColor: '#000', 166 | shadowOffset: { 167 | width: 0, 168 | height: -8, 169 | }, 170 | shadowOpacity: 0.1, 171 | shadowRadius: 5, 172 | 173 | elevation: 3, 174 | }, 175 | textInput: { 176 | flex: 1, 177 | borderWidth: 1, 178 | borderColor: 'gray', 179 | borderRadius: 5, 180 | paddingHorizontal: 10, 181 | minHeight: 40, 182 | backgroundColor: '#fff', 183 | paddingTop: 10, 184 | }, 185 | sendButton: { 186 | backgroundColor: '#EEA217', 187 | borderRadius: 5, 188 | padding: 10, 189 | marginLeft: 10, 190 | alignSelf: 'flex-end', 191 | }, 192 | sendButtonText: { 193 | color: 'white', 194 | textAlign: 'center', 195 | fontSize: 16, 196 | fontWeight: 'bold', 197 | }, 198 | messageContainer: { 199 | padding: 10, 200 | borderRadius: 10, 201 | marginTop: 10, 202 | marginHorizontal: 10, 203 | maxWidth: '80%', 204 | }, 205 | userMessageContainer: { 206 | backgroundColor: '#791363', 207 | alignSelf: 'flex-end', 208 | }, 209 | otherMessageContainer: { 210 | alignSelf: 'flex-start', 211 | backgroundColor: '#fff', 212 | }, 213 | messageText: { 214 | fontSize: 16, 215 | flexWrap: 'wrap', 216 | }, 217 | userMessageText: { 218 | color: '#fff', 219 | }, 220 | timestamp: { 221 | fontSize: 12, 222 | color: '#c7c7c7', 223 | }, 224 | }); 225 | 226 | export default Page; 227 | -------------------------------------------------------------------------------- /app/(modal)/create.tsx: -------------------------------------------------------------------------------- 1 | import { Text, KeyboardAvoidingView, Platform, StyleSheet, TextInput, TouchableOpacity } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import { useMutation } from 'convex/react'; 4 | import { api } from '@/convex/_generated/api'; 5 | import { useRouter } from 'expo-router'; 6 | 7 | const Page = () => { 8 | const [name, setName] = useState(''); 9 | const [desc, setDesc] = useState(''); 10 | const [icon, setIcon] = useState(''); 11 | const startGroup = useMutation(api.groups.create); 12 | const router = useRouter(); 13 | 14 | // Create a new group with Convex mutation 15 | const onCreateGroup = async () => { 16 | await startGroup({ 17 | name, 18 | description: desc, 19 | icon_url: icon, 20 | }); 21 | router.back(); 22 | }; 23 | 24 | return ( 25 | 26 | Name 27 | 28 | Description 29 | 30 | Icon URL 31 | 32 | 33 | Create 34 | 35 | 36 | ); 37 | }; 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | flex: 1, 42 | backgroundColor: '#F8F5EA', 43 | padding: 10, 44 | }, 45 | label: { 46 | marginVertical: 10, 47 | }, 48 | textInput: { 49 | borderWidth: 1, 50 | borderColor: 'gray', 51 | borderRadius: 5, 52 | paddingHorizontal: 10, 53 | minHeight: 40, 54 | backgroundColor: '#fff', 55 | }, 56 | button: { 57 | backgroundColor: '#EEA217', 58 | borderRadius: 5, 59 | padding: 10, 60 | marginVertical: 10, 61 | justifyContent: 'center', 62 | alignItems: 'center', 63 | }, 64 | buttonText: { 65 | color: 'white', 66 | textAlign: 'center', 67 | fontSize: 16, 68 | fontWeight: 'bold', 69 | }, 70 | }); 71 | 72 | export default Page; 73 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import { ConvexProvider, ConvexReactClient } from 'convex/react'; 3 | import { Link, Stack } from 'expo-router'; 4 | import { TouchableOpacity } from 'react-native'; 5 | 6 | const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, { 7 | unsavedChangesWarning: false, 8 | }); 9 | 10 | // Stack navigation with two screens and one modal 11 | export default function RootLayoutNav() { 12 | return ( 13 | 14 | 21 | ( 26 | 27 | 28 | 29 | 30 | 31 | ), 32 | }} 33 | /> 34 | 35 | ( 41 | 42 | 43 | 44 | 45 | 46 | ), 47 | }} 48 | /> 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, Image, ScrollView, TouchableOpacity } from 'react-native'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { useAction, useQuery } from 'convex/react'; 4 | import { api } from '../convex/_generated/api'; 5 | import { Link } from 'expo-router'; 6 | import AsyncStorage from '@react-native-async-storage/async-storage'; 7 | import Dialog from 'react-native-dialog'; 8 | 9 | const Page = () => { 10 | const groups = useQuery(api.groups.get) || []; 11 | const [name, setName] = useState(''); 12 | const [visible, setVisible] = useState(false); 13 | const performGetGreetingAction = useAction(api.greeting.getGreeting); 14 | const [greeting, setGreeting] = useState(''); 15 | 16 | // Check if the user has a name, otherwise show modal 17 | useEffect(() => { 18 | const loadUser = async () => { 19 | const user = await AsyncStorage.getItem('user'); 20 | if (!user) { 21 | setTimeout(() => { 22 | setVisible(true); 23 | }, 100); 24 | } else { 25 | setName(user); 26 | } 27 | }; 28 | loadUser(); 29 | }, []); 30 | 31 | // Safe the user name to async storage 32 | const setUser = async () => { 33 | let r = (Math.random() + 1).toString(36).substring(7); 34 | const userName = `${name}#${r}`; 35 | await AsyncStorage.setItem('user', userName); 36 | setName(userName); 37 | setVisible(false); 38 | }; 39 | 40 | // Load greeting using Convex action 41 | useEffect(() => { 42 | if (!name) return; 43 | const loadGreeting = async () => { 44 | const greeting = await performGetGreetingAction({ name }); 45 | setGreeting(greeting); 46 | }; 47 | loadGreeting(); 48 | }, [name]); 49 | 50 | return ( 51 | 52 | 53 | {groups.map((group) => ( 54 | 55 | 56 | 57 | 58 | {group.name} 59 | {group.description} 60 | 61 | 62 | 63 | ))} 64 | {greeting} 65 | 66 | 67 | Username required 68 | Please insert a name to start chatting. 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | const styles = StyleSheet.create({ 77 | container: { 78 | flex: 1, 79 | padding: 10, 80 | backgroundColor: '#F8F5EA', 81 | }, 82 | group: { 83 | flexDirection: 'row', 84 | gap: 10, 85 | alignItems: 'center', 86 | backgroundColor: '#fff', 87 | padding: 10, 88 | borderRadius: 10, 89 | marginBottom: 10, 90 | shadowColor: '#000', 91 | shadowOffset: { 92 | width: 0, 93 | height: 1, 94 | }, 95 | shadowOpacity: 0.22, 96 | shadowRadius: 2.22, 97 | elevation: 3, 98 | }, 99 | }); 100 | 101 | export default Page; 102 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/assets/images/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | // Required for expo-router 7 | 'expo-router/babel', 8 | ], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /chatGroups.jsonl: -------------------------------------------------------------------------------- 1 | {"name": "Convex Dev Chat", "description": "A place for Convex developers to chat", "icon_url": "https://www.convex.dev/favicon.ico"} 2 | {"name": "Galaxies PRO Chat", "description": "A place for Galaxies PRO developers to chat", "icon_url": "https://galaxies.dev/img/logos/logo--blue.png"} -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as greeting from "../greeting"; 18 | import type * as groups from "../groups"; 19 | import type * as http from "../http"; 20 | import type * as messages from "../messages"; 21 | 22 | /** 23 | * A utility for referencing Convex functions in your app's API. 24 | * 25 | * Usage: 26 | * ```js 27 | * const myFunctionReference = api.myModule.myFunction; 28 | * ``` 29 | */ 30 | declare const fullApi: ApiFromModules<{ 31 | greeting: typeof greeting; 32 | groups: typeof groups; 33 | http: typeof http; 34 | messages: typeof messages; 35 | }>; 36 | export declare const api: FilterApi< 37 | typeof fullApi, 38 | FunctionReference 39 | >; 40 | export declare const internal: FilterApi< 41 | typeof fullApi, 42 | FunctionReference 43 | >; 44 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /convex/greeting.ts: -------------------------------------------------------------------------------- 1 | import { v } from 'convex/values'; 2 | import { action } from './_generated/server'; 3 | 4 | export const getGreeting = action({ 5 | args: { name: v.string() }, 6 | handler: async (ctx, { name }) => { 7 | return `Welcome back, ${name}!`; 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /convex/groups.ts: -------------------------------------------------------------------------------- 1 | import { v } from 'convex/values'; 2 | import { mutation, query } from './_generated/server'; 3 | 4 | // This is a query that returns all the groups in the database 5 | export const get = query({ 6 | args: {}, 7 | handler: async (ctx) => { 8 | return await ctx.db.query('groups').collect(); 9 | }, 10 | }); 11 | 12 | // This is a query that returns a single group by ID 13 | export const getGroup = query({ 14 | args: { id: v.id('groups') }, 15 | handler: async (ctx, { id }) => { 16 | return await ctx.db 17 | .query('groups') 18 | .filter((q) => q.eq(q.field('_id'), id)) 19 | .unique(); 20 | }, 21 | }); 22 | 23 | // This is a mutation that creates a new group 24 | export const create = mutation({ 25 | args: { description: v.string(), name: v.string(), icon_url: v.string() }, 26 | handler: async ({ db }, args) => { 27 | await db.insert('groups', args); 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from 'convex/server'; 2 | import { httpAction } from './_generated/server'; 3 | import { api } from './_generated/api'; 4 | import { Id } from './_generated/dataModel'; 5 | 6 | const http = httpRouter(); 7 | 8 | // Special route for image upload to storage 9 | // Also runs the mutation to add the message to the database 10 | http.route({ 11 | path: '/sendImage', 12 | method: 'POST', 13 | handler: httpAction(async (ctx, request) => { 14 | // Store the file 15 | const blob = await request.blob(); 16 | const storageId = await ctx.storage.store(blob); 17 | 18 | // Save the storage ID to the database via a mutation 19 | const user = new URL(request.url).searchParams.get('user'); 20 | const group_id = new URL(request.url).searchParams.get('group_id'); 21 | const content = new URL(request.url).searchParams.get('content'); 22 | 23 | await ctx.runMutation(api.messages.sendMessage, { content: content!, file: storageId, user: user!, group_id: group_id as Id<'groups'> }); 24 | 25 | // Return a response 26 | return new Response(JSON.stringify({ success: true })); 27 | }), 28 | }); 29 | 30 | export default http; 31 | -------------------------------------------------------------------------------- /convex/messages.ts: -------------------------------------------------------------------------------- 1 | import { mutation, query } from './_generated/server'; 2 | import { v } from 'convex/values'; 3 | 4 | // This is a mutation that creates a new message linked to the group 5 | export const sendMessage = mutation({ 6 | args: { content: v.string(), group_id: v.id('groups'), user: v.string(), file: v.optional(v.string()) }, 7 | handler: async (ctx, args) => { 8 | await ctx.db.insert('messages', args); 9 | }, 10 | }); 11 | 12 | // This is a query that returns all messages in a specific group 13 | export const get = query({ 14 | args: { chatId: v.id('groups') }, 15 | handler: async ({ db, storage }, { chatId }) => { 16 | const messages = await db 17 | .query('messages') 18 | .filter((q) => q.eq(q.field('group_id'), chatId)) 19 | .collect(); 20 | 21 | // If the message has a file, get the URL from storage 22 | return Promise.all( 23 | messages.map(async (message) => { 24 | if (message.file) { 25 | const url = await storage.getUrl(message.file); 26 | if (url) { 27 | return { ...message, file: url }; 28 | } 29 | } 30 | return message; 31 | }) 32 | ); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from 'convex/server'; 2 | import { v } from 'convex/values'; 3 | 4 | // This is our schema for the database 5 | export default defineSchema({ 6 | groups: defineTable({ 7 | description: v.string(), 8 | icon_url: v.string(), 9 | name: v.string(), 10 | }), 11 | messages: defineTable({ 12 | content: v.string(), 13 | group_id: v.id('groups'), 14 | user: v.string(), 15 | file: v.optional(v.string()), 16 | }), 17 | }); 18 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | /** @type {import('expo/metro-config').MetroConfig} */ 5 | const config = getDefaultConfig(__dirname, { 6 | // [Web-only]: Enables CSS support in Metro. 7 | isCSSEnabled: true, 8 | }); 9 | 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convexchat", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "test": "jest --watchAll" 11 | }, 12 | "jest": { 13 | "preset": "jest-expo" 14 | }, 15 | "dependencies": { 16 | "@expo/vector-icons": "^13.0.0", 17 | "@react-native-async-storage/async-storage": "1.18.2", 18 | "@react-navigation/native": "^6.0.2", 19 | "convex": "^1.0.2", 20 | "expo": "~49.0.5", 21 | "expo-font": "~11.4.0", 22 | "expo-image-picker": "~14.3.2", 23 | "expo-linking": "~5.0.2", 24 | "expo-router": "2.0.0", 25 | "expo-splash-screen": "~0.20.4", 26 | "expo-status-bar": "~1.6.0", 27 | "expo-system-ui": "~2.4.0", 28 | "expo-web-browser": "~12.3.2", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-native": "0.72.3", 32 | "react-native-dialog": "^9.3.0", 33 | "react-native-gesture-handler": "~2.12.0", 34 | "react-native-safe-area-context": "4.6.3", 35 | "react-native-screens": "~3.22.0", 36 | "react-native-web": "~0.19.6" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.20.0", 40 | "@types/react": "~18.2.14", 41 | "jest": "^29.2.1", 42 | "jest-expo": "~49.0.0", 43 | "react-test-renderer": "18.2.0", 44 | "typescript": "^5.1.3" 45 | }, 46 | "overrides": { 47 | "react-refresh": "~0.14.0" 48 | }, 49 | "resolutions": { 50 | "react-refresh": "~0.14.0" 51 | }, 52 | "private": true 53 | } 54 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-chat-convex/55faaba476b2d0d0ffa93232c09c4855875c1d60/screenshots/3.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------