├── .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 |
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 |
--------------------------------------------------------------------------------