├── assets ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── babel.config.js ├── App.js ├── .gitignore ├── src ├── chatkitty │ └── index.js ├── components │ ├── loading.js │ ├── formInput.js │ └── formButton.js ├── context │ ├── authStack.js │ ├── index.js │ ├── routes.js │ ├── authProvider.js │ ├── homeStack.js │ └── notificationProvider.js ├── firebase │ └── index.js └── screens │ ├── loginScreen.js │ ├── browseChannelsScreen.js │ ├── createChannelScreen.js │ ├── signupScreen.js │ ├── homeScreen.js │ └── chatScreen.js ├── app.json └── package.json /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatKitty/chatkitty-example-react-native/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatKitty/chatkitty-example-react-native/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatKitty/chatkitty-example-react-native/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatKitty/chatkitty-example-react-native/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Providers from './src/context'; 4 | 5 | export default function App() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /src/chatkitty/index.js: -------------------------------------------------------------------------------- 1 | import ChatKitty from '@chatkitty/core'; 2 | 3 | export const chatkitty = ChatKitty.getInstance('eb8472e5-db71-48fa-a92a-633bbbd3fcf8'); 4 | 5 | export function channelDisplayName(channel) { 6 | if (channel.type === 'DIRECT') { 7 | return channel.members.map((member) => member.displayName).join(', '); 8 | } else { 9 | return channel.name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, StyleSheet, View } from 'react-native'; 3 | 4 | export default function Loading() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | const styles = StyleSheet.create({ 13 | loadingContainer: { 14 | flex: 1, 15 | alignItems: 'center', 16 | justifyContent: 'center' 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/context/authStack.js: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import React from 'react'; 3 | 4 | import LoginScreen from '../screens/loginScreen'; 5 | import SignupScreen from '../screens/signupScreen'; 6 | 7 | const Stack = createStackNavigator(); 8 | 9 | export default function AuthStack() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/formInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimensions, StyleSheet } from 'react-native'; 3 | import { TextInput } from 'react-native-paper'; 4 | 5 | const { width, height } = Dimensions.get('screen'); 6 | 7 | export default function FormInput({ labelName, ...rest }) { 8 | return ( 9 | 15 | ); 16 | } 17 | 18 | const styles = StyleSheet.create({ 19 | input: { 20 | marginTop: 10, 21 | marginBottom: 10, 22 | width: width / 1.5, 23 | height: height / 15 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/formButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimensions, StyleSheet } from 'react-native'; 3 | import { Button } from 'react-native-paper'; 4 | 5 | const { width, height } = Dimensions.get('screen'); 6 | 7 | export default function FormButton({ title, modeValue, ...rest }) { 8 | return ( 9 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | button: { 22 | marginTop: 10 23 | }, 24 | buttonContainer: { 25 | width: width / 2, 26 | height: height / 15 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "chatkitty-example-react-native", 4 | "slug": "chatkitty-example-react-native", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper'; 3 | 4 | import { AuthProvider } from './authProvider'; 5 | import { NotificationProvider } from './notificationProvider'; 6 | import Routes from './routes'; 7 | 8 | export default function Providers() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const theme = { 21 | ...DefaultTheme, 22 | roundness: 2, 23 | colors: { 24 | ...DefaultTheme.colors, 25 | primary: '#5b3a70', 26 | accent: '#50c878', 27 | background: '#f7f9fb' 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/firebase/index.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { initializeApp } from 'firebase/app'; 3 | import { 4 | initializeAuth, 5 | getReactNativePersistence 6 | } from 'firebase/auth/react-native'; 7 | 8 | 9 | // Replace this with your Firebase SDK config snippet 10 | const firebaseConfig = { 11 | apiKey: "AIzaSyBt4Su-ZxyUFHCrBpBXzSaEzuisGKMar_w", 12 | authDomain: "chatkitty-expo-review.firebaseapp.com", 13 | projectId: "chatkitty-expo-review", 14 | storageBucket: "chatkitty-expo-review.appspot.com", 15 | messagingSenderId: "891463062190", 16 | appId: "1:891463062190:web:ef345c8bdeb8748bc3133f" 17 | }; 18 | 19 | // Initialize Firebase 20 | const app = initializeApp(firebaseConfig); 21 | 22 | // Initialize Auth 23 | const auth = initializeAuth(app, { 24 | persistence: getReactNativePersistence(AsyncStorage) 25 | }); 26 | 27 | export { auth }; 28 | -------------------------------------------------------------------------------- /src/context/routes.js: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native'; 2 | import React, { useContext, useState, useEffect } from 'react'; 3 | 4 | import { chatkitty } from '../chatkitty'; 5 | import Loading from '../components/loading'; 6 | 7 | import AuthStack from './authStack'; 8 | import HomeStack from './homeStack'; 9 | import { AuthContext } from './authProvider'; 10 | 11 | export default function Routes() { 12 | const { user, setUser } = useContext(AuthContext); 13 | const [loading, setLoading] = useState(true); 14 | const [initializing, setInitializing] = useState(true); 15 | 16 | useEffect(() => { 17 | return chatkitty.onCurrentUserChanged((currentUser) => { 18 | setUser(currentUser); 19 | 20 | if (initializing) { 21 | setInitializing(false); 22 | } 23 | 24 | setLoading(false); 25 | }); 26 | }, [initializing, setUser]); 27 | 28 | if (loading) { 29 | return ; 30 | } 31 | 32 | return ( 33 | 34 | {user ? : } 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatkitty-example-react-native", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@chatkitty/core": "^1.2.13", 13 | "@react-native-async-storage/async-storage": "1.17.11", 14 | "@react-native-community/masked-view": "^0.1.11", 15 | "@react-navigation/native": "^6.1.6", 16 | "@react-navigation/stack": "^6.3.16", 17 | "expo": "~48.0.9", 18 | "expo-status-bar": "~1.4.4", 19 | "firebase": "^9.18.0", 20 | "react": "18.2.0", 21 | "react-native": "0.71.4", 22 | "react-native-gesture-handler": "~2.9.0", 23 | "react-native-gifted-chat": "^2.0.1", 24 | "react-native-paper": "^5.5.1", 25 | "react-native-reanimated": "~2.14.4", 26 | "react-native-safe-area-context": "4.5.0", 27 | "react-native-screens": "~3.20.0", 28 | "react-native-vector-icons": "^9.2.0", 29 | "expo-device": "~5.2.1", 30 | "expo-notifications": "~0.18.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.20.0" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /src/screens/loginScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Title } from 'react-native-paper'; 4 | 5 | import FormButton from '../components/formButton.js'; 6 | import FormInput from '../components/formInput.js'; 7 | import Loading from '../components/loading.js'; 8 | import { AuthContext } from '../context/authProvider.js'; 9 | 10 | export default function LoginScreen({ navigation }) { 11 | const [email, setEmail] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | 14 | const { login, loading } = useContext(AuthContext); 15 | 16 | if (loading) { 17 | return ; 18 | } 19 | 20 | return ( 21 | 22 | Welcome! 23 | setEmail(userEmail)} 28 | /> 29 | setPassword(userPassword)} 34 | /> 35 | { 40 | login(email, password); 41 | }} 42 | /> 43 | navigation.navigate('Signup')} 49 | /> 50 | 51 | ); 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | backgroundColor: '#f5f5f5', 57 | flex: 1, 58 | justifyContent: 'center', 59 | alignItems: 'center' 60 | }, 61 | titleText: { 62 | fontSize: 24, 63 | marginBottom: 10 64 | }, 65 | loginButtonLabel: { 66 | fontSize: 22 67 | }, 68 | navButtonText: { 69 | fontSize: 16 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /src/screens/browseChannelsScreen.js: -------------------------------------------------------------------------------- 1 | import { useIsFocused } from '@react-navigation/native'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { FlatList, StyleSheet, View } from 'react-native'; 4 | import { Divider, List } from 'react-native-paper'; 5 | 6 | import { chatkitty, channelDisplayName } from '../chatkitty'; 7 | import Loading from '../components/loading'; 8 | 9 | export default function BrowseChannelsScreen({ navigation }) { 10 | const [channels, setChannels] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | 13 | const isFocused = useIsFocused(); 14 | 15 | useEffect(() => { 16 | chatkitty.listChannels({ filter: { joined: false } }).then((result) => { 17 | setChannels(result.paginator.items); 18 | 19 | if (loading) { 20 | setLoading(false); 21 | } 22 | }); 23 | }, [isFocused, loading]); 24 | 25 | async function handleJoinChannel(channel) { 26 | const result = await chatkitty.joinChannel({ channel: channel }); 27 | 28 | navigation.navigate('Chat', { channel: result.channel }); 29 | } 30 | 31 | if (loading) { 32 | return ; 33 | } 34 | 35 | return ( 36 | 37 | item.id.toString()} 40 | ItemSeparatorComponent={() => } 41 | renderItem={({ item }) => ( 42 | handleJoinChannel(item)} 50 | /> 51 | )} 52 | /> 53 | 54 | ); 55 | } 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | backgroundColor: '#f5f5f5', 60 | flex: 1 61 | }, 62 | listTitle: { 63 | fontSize: 22 64 | }, 65 | listDescription: { 66 | fontSize: 16 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /src/screens/createChannelScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { IconButton, Title } from 'react-native-paper'; 4 | 5 | import { chatkitty } from '../chatkitty'; 6 | import FormButton from '../components/formButton'; 7 | import FormInput from '../components/formInput'; 8 | 9 | export default function CreateChannelScreen({ navigation }) { 10 | const [channelName, setChannelName] = useState(''); 11 | 12 | function handleButtonPress() { 13 | if (channelName.length > 0) { 14 | chatkitty 15 | .createChannel({ 16 | type: 'PUBLIC', 17 | name: channelName 18 | }) 19 | .then(() => navigation.navigate('Home')); 20 | } 21 | } 22 | 23 | return ( 24 | 25 | 26 | navigation.goBack()} 31 | /> 32 | 33 | 34 | Create a new channel 35 | setChannelName(text)} 39 | clearButtonMode='while-editing' 40 | /> 41 | handleButtonPress()} 46 | disabled={channelName.length === 0} 47 | /> 48 | 49 | 50 | ); 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | rootContainer: { 55 | flex: 1 56 | }, 57 | closeButtonContainer: { 58 | position: 'absolute', 59 | top: 30, 60 | right: 0, 61 | zIndex: 1 62 | }, 63 | innerContainer: { 64 | flex: 1, 65 | justifyContent: 'center', 66 | alignItems: 'center' 67 | }, 68 | title: { 69 | fontSize: 24, 70 | marginBottom: 10 71 | }, 72 | buttonLabel: { 73 | fontSize: 22 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/screens/signupScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { IconButton, Title } from 'react-native-paper'; 4 | 5 | import FormButton from '../components/formButton'; 6 | import FormInput from '../components/formInput'; 7 | import Loading from '../components/loading'; 8 | import { AuthContext } from '../context/authProvider'; 9 | 10 | export default function SignupScreen({ navigation }) { 11 | const [displayName, setDisplayName] = useState(''); 12 | const [email, setEmail] = useState(''); 13 | const [password, setPassword] = useState(''); 14 | 15 | const { register, loading } = useContext(AuthContext); 16 | 17 | if (loading) { 18 | return ; 19 | } 20 | 21 | return ( 22 | 23 | Let's get started! 24 | setDisplayName(userDisplayName)} 29 | /> 30 | setEmail(userEmail)} 35 | /> 36 | setPassword(userPassword)} 41 | /> 42 | register(displayName, email, password)} 47 | /> 48 | navigation.goBack()} 54 | /> 55 | 56 | ); 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | backgroundColor: '#f5f5f5', 62 | flex: 1, 63 | justifyContent: 'center', 64 | alignItems: 'center' 65 | }, 66 | titleText: { 67 | fontSize: 24, 68 | marginBottom: 10 69 | }, 70 | loginButtonLabel: { 71 | fontSize: 22 72 | }, 73 | navButtonText: { 74 | fontSize: 18 75 | }, 76 | navButton: { 77 | marginTop: 10 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /src/context/authProvider.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | import { 3 | createUserWithEmailAndPassword, 4 | updateProfile, 5 | signInWithEmailAndPassword 6 | } from 'firebase/auth/react-native'; 7 | import { auth } from '../firebase'; 8 | import { chatkitty } from '../chatkitty'; 9 | 10 | export const AuthContext = createContext({}); 11 | 12 | export const AuthProvider = ({ children }) => { 13 | const [user, setUser] = useState(null); 14 | const [loading, setLoading] = useState(false); 15 | 16 | return ( 17 | { 24 | setLoading(true); 25 | 26 | try { 27 | const userCredential = await signInWithEmailAndPassword(auth, 28 | email, password); 29 | 30 | // Signed-in Firebase user 31 | const currentUser = userCredential.user; 32 | 33 | const result = await chatkitty.startSession({ 34 | username: currentUser.uid, 35 | authParams: { 36 | idToken: await currentUser.getIdToken() 37 | } 38 | }); 39 | 40 | if (result.failed) { 41 | console.log('could not login'); 42 | } 43 | } catch (error) { 44 | console.error(error); 45 | } finally { 46 | setLoading(false); 47 | } 48 | }, 49 | 50 | register: async (displayName, email, password) => { 51 | setLoading(true); 52 | 53 | try { 54 | const userCredential = await createUserWithEmailAndPassword( 55 | auth, email, password); 56 | 57 | await updateProfile(auth.currentUser, { 58 | displayName: displayName 59 | }); 60 | 61 | // Signed-in Firebase user 62 | const currentUser = userCredential.user; 63 | 64 | const startSessionResult = await chatkitty.startSession({ 65 | username: currentUser.uid, 66 | authParams: { 67 | idToken: await currentUser.getIdToken() 68 | } 69 | }); 70 | 71 | if (startSessionResult.failed) { 72 | console.log('Could not sign up'); 73 | } 74 | } catch (error) { 75 | console.error(error); 76 | } finally { 77 | setLoading(false); 78 | } 79 | }, 80 | logout: async () => { 81 | try { 82 | await chatkitty.endSession(); 83 | } catch (error) { 84 | console.error(error); 85 | } 86 | } 87 | }} 88 | > 89 | {children} 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/context/homeStack.js: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import React, { useContext, useEffect } from 'react'; 3 | import { IconButton } from 'react-native-paper'; 4 | 5 | import { chatkitty, channelDisplayName } from '../chatkitty'; 6 | 7 | import { NotificationContext } from './notificationProvider'; 8 | 9 | import BrowseChannelsScreen from '../screens/browseChannelsScreen'; 10 | import ChatScreen from '../screens/chatScreen'; 11 | import CreateChannelScreen from '../screens/createChannelScreen'; 12 | import HomeScreen from '../screens/homeScreen'; 13 | 14 | const ChatStack = createStackNavigator(); 15 | const ModalStack = createStackNavigator(); 16 | 17 | export default function HomeStack() { 18 | const { registerForPushNotifications, sendNotification } = useContext(NotificationContext); 19 | 20 | useEffect(() => { 21 | registerForPushNotifications(); 22 | 23 | chatkitty.onNotificationReceived(async (notification) => { 24 | await sendNotification({ 25 | title: notification.title, 26 | body: notification.body 27 | }); 28 | }); 29 | }, []); 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | function ChatComponent() { 40 | return ( 41 | 52 | ({ 56 | headerRight: () => ( 57 | navigation.navigate('BrowseChannels')} 62 | /> 63 | ) 64 | })} 65 | /> 66 | ({ 70 | headerRight: () => ( 71 | navigation.navigate('CreateChannel')} 76 | /> 77 | ) 78 | })} 79 | /> 80 | ({ 84 | title: channelDisplayName(route.params.channel) 85 | })} 86 | /> 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/screens/homeScreen.js: -------------------------------------------------------------------------------- 1 | import { useIsFocused } from '@react-navigation/native'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { FlatList, StyleSheet, View } from 'react-native'; 4 | import { Button, Dialog, Divider, List, Portal } from 'react-native-paper'; 5 | 6 | import { chatkitty, channelDisplayName } from '../chatkitty'; 7 | import Loading from '../components/loading'; 8 | 9 | export default function HomeScreen({ navigation }) { 10 | const [channels, setChannels] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | const [leaveChannel, setLeaveChannel] = useState(null); 13 | 14 | const isFocused = useIsFocused(); 15 | 16 | useEffect(() => { 17 | let isCancelled = false; 18 | 19 | chatkitty.listChannels({ filter: { joined: true } }).then((result) => { 20 | if (!isCancelled) { 21 | setChannels(result.paginator.items); 22 | 23 | if (loading) { 24 | setLoading(false); 25 | } 26 | } 27 | }); 28 | 29 | return () => { 30 | isCancelled = true; 31 | }; 32 | }, [isFocused, loading]); 33 | 34 | if (loading) { 35 | return ; 36 | } 37 | 38 | function handleLeaveChannel() { 39 | chatkitty.leaveChannel({ channel: leaveChannel }).then(() => { 40 | setLeaveChannel(null); 41 | 42 | chatkitty.listChannels({ filter: { joined: true } }).then((result) => { 43 | setChannels(result.paginator.items); 44 | }); 45 | }); 46 | } 47 | 48 | function handleDismissLeaveChannel() { 49 | setLeaveChannel(null); 50 | } 51 | 52 | return ( 53 | 54 | item.id.toString()} 57 | ItemSeparatorComponent={() => } 58 | renderItem={({ item }) => ( 59 | navigation.navigate('Chat', { channel: item })} 67 | onLongPress={() => { 68 | setLeaveChannel(item); 69 | }} 70 | /> 71 | )} 72 | /> 73 | 74 | 75 | Leave channel? 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | } 85 | 86 | const styles = StyleSheet.create({ 87 | container: { 88 | backgroundColor: '#f5f5f5', 89 | flex: 1 90 | }, 91 | listTitle: { 92 | fontSize: 22 93 | }, 94 | listDescription: { 95 | fontSize: 16 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /src/context/notificationProvider.js: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useRef, useState } from 'react'; 2 | import { Platform } from 'react-native'; 3 | 4 | import * as Device from 'expo-device'; 5 | import * as Notifications from 'expo-notifications'; 6 | import { chatkitty } from '../chatkitty'; 7 | 8 | Notifications.setNotificationHandler({ 9 | handleNotification: async () => ({ 10 | shouldShowAlert: true, 11 | shouldPlaySound: false, 12 | shouldSetBadge: false 13 | }) 14 | }); 15 | 16 | export const NotificationContext = createContext({}); 17 | 18 | export const NotificationProvider = ({ children }) => { 19 | const [notification, setNotification] = useState(null); 20 | const notificationListener = useRef(); 21 | const responseListener = useRef(); 22 | 23 | useEffect(() => { 24 | notificationListener.current = 25 | Notifications.addNotificationReceivedListener((notification) => { 26 | setNotification(notification); 27 | }); 28 | 29 | responseListener.current = 30 | Notifications.addNotificationResponseReceivedListener((response) => { 31 | console.log(response); 32 | }); 33 | 34 | return () => { 35 | Notifications.removeNotificationSubscription( 36 | notificationListener.current 37 | ); 38 | Notifications.removeNotificationSubscription(responseListener.current); 39 | }; 40 | }, []); 41 | 42 | return ( 43 | { 47 | await Notifications.scheduleNotificationAsync({ 48 | content, 49 | trigger: null 50 | }); 51 | }, 52 | registerForPushNotifications: async () => { 53 | let token; 54 | if (Device.isDevice) { 55 | const { status: existingStatus } = await Notifications.getPermissionsAsync(); 56 | let finalStatus = existingStatus; 57 | if (existingStatus !== 'granted') { 58 | const { status } = await Notifications.requestPermissionsAsync(); 59 | finalStatus = status; 60 | } 61 | if (finalStatus !== 'granted') { 62 | alert('Failed to get push token for push notification!'); 63 | return; 64 | } 65 | token = (await Notifications.getExpoPushTokenAsync()).data; 66 | console.log(token); 67 | } else { 68 | alert('Must use physical device for Push Notifications'); 69 | } 70 | 71 | if (Platform.OS === 'android') { 72 | await Notifications.setNotificationChannelAsync('default', { 73 | name: 'default', 74 | importance: Notifications.AndroidImportance.MAX, 75 | vibrationPattern: [0, 250, 250, 250], 76 | lightColor: '#FF231F7C' 77 | }); 78 | } 79 | 80 | await chatkitty.updateCurrentUser((user) => { 81 | user.properties = { 82 | ...user.properties, 83 | 'expo-push-token': token 84 | }; 85 | return user; 86 | }); 87 | } 88 | }} 89 | > 90 | {children} 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/screens/chatScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat'; 4 | import { Text } from 'react-native-paper'; 5 | 6 | import { chatkitty } from '../chatkitty'; 7 | import Loading from '../components/loading'; 8 | import { AuthContext } from '../context/authProvider'; 9 | import { NotificationContext } from '../context/notificationProvider'; 10 | 11 | export default function ChatScreen({ route, navigation }) { 12 | const { user } = useContext(AuthContext); 13 | const { sendNotification } = useContext(NotificationContext); 14 | const { channel } = route.params; 15 | 16 | const [messages, setMessages] = useState([]); 17 | const [loading, setLoading] = useState(true); 18 | const [loadEarlier, setLoadEarlier] = useState(false); 19 | const [isLoadingEarlier, setIsLoadingEarlier] = useState(false); 20 | const [messagePaginator, setMessagePaginator] = useState(null); 21 | const [typing, setTyping] = useState(null); 22 | 23 | useEffect(() => { 24 | const startChatSessionResult = chatkitty.startChatSession({ 25 | channel: channel, 26 | onMessageReceived: (message) => { 27 | setMessages((currentMessages) => 28 | GiftedChat.append(currentMessages, [mapMessage(message)]) 29 | ); 30 | }, 31 | onTypingStarted: (typingUser) => { 32 | if (typingUser.id !== user.id) { 33 | setTyping(typingUser); 34 | } 35 | }, 36 | onTypingStopped: (typingUser) => { 37 | if (typingUser.id !== user.id) { 38 | setTyping(null); 39 | } 40 | }, 41 | onParticipantEnteredChat: (participant) => { 42 | sendNotification({ 43 | title: `${participant.displayName} entered the chat` 44 | }); 45 | }, 46 | onParticipantLeftChat: (participant) => { 47 | sendNotification({ 48 | title: `${participant.displayName} left the chat` 49 | }); 50 | } 51 | }); 52 | 53 | chatkitty 54 | .listMessages({ 55 | channel: channel 56 | }) 57 | .then((result) => { 58 | setMessages(result.paginator.items.map(mapMessage)); 59 | 60 | setMessagePaginator(result.paginator); 61 | setLoadEarlier(result.paginator.hasNextPage); 62 | 63 | setLoading(false); 64 | }); 65 | 66 | return startChatSessionResult.session.end; 67 | }, [user, channel]); 68 | 69 | async function handleSend(pendingMessages) { 70 | await chatkitty.sendMessage({ 71 | channel: channel, 72 | body: pendingMessages[0].text 73 | }); 74 | } 75 | 76 | async function handleLoadEarlier() { 77 | if (!messagePaginator.hasNextPage) { 78 | setLoadEarlier(false); 79 | 80 | return; 81 | } 82 | 83 | setIsLoadingEarlier(true); 84 | 85 | const nextPaginator = await messagePaginator.nextPage(); 86 | 87 | setMessagePaginator(nextPaginator); 88 | 89 | setMessages((currentMessages) => 90 | GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage)) 91 | ); 92 | 93 | setIsLoadingEarlier(false); 94 | } 95 | 96 | function handleInputTextChanged(text) { 97 | chatkitty.sendKeystrokes({ 98 | channel: channel, 99 | keys: text 100 | }); 101 | } 102 | 103 | function renderBubble(props) { 104 | return ( 105 | 113 | ); 114 | } 115 | 116 | function renderAvatar(props) { 117 | return ( 118 | { 121 | chatkitty 122 | .createChannel({ 123 | type: 'DIRECT', 124 | members: [{ id: avatarUser._id }] 125 | }) 126 | .then((result) => { 127 | navigation.navigate('Chat', { channel: result.channel }); 128 | }); 129 | }} 130 | /> 131 | ); 132 | } 133 | 134 | function renderFooter() { 135 | if (typing) { 136 | return ( 137 | 138 | {typing.displayName} is typing 139 | 140 | ); 141 | } 142 | 143 | return null; 144 | } 145 | 146 | if (loading) { 147 | return ; 148 | } 149 | 150 | return ( 151 | 163 | ); 164 | } 165 | 166 | function mapMessage(message) { 167 | return { 168 | _id: message.id, 169 | text: message.body, 170 | createdAt: new Date(message.createdTime), 171 | user: mapUser(message.user) 172 | }; 173 | } 174 | 175 | function mapUser(user) { 176 | return { 177 | _id: user.id, 178 | name: user.displayName, 179 | avatar: user.displayPictureUrl 180 | }; 181 | } 182 | 183 | const styles = StyleSheet.create({ 184 | footer: { 185 | paddingRight: 10, 186 | paddingLeft: 10, 187 | paddingBottom: 5 188 | } 189 | }); 190 | --------------------------------------------------------------------------------