├── .watchmanconfig
├── .gitattributes
├── app.json
├── babel.config.js
├── .env
├── server
├── .env
├── package.json
└── index.js
├── .buckconfig
├── index.js
├── .editorconfig
├── metro.config.js
├── App.js
├── Root.js
├── .gitignore
├── src
├── components
│ ├── ChatBubble.js
│ ├── AudioPlayer.js
│ └── VideoPlayer.js
└── screens
│ ├── Login.js
│ ├── Groups.js
│ └── Chat.js
├── package.json
├── .flowconfig
└── README.md
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNChatkitDemo",
3 | "displayName": "RNChatkitDemo"
4 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
2 | CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
2 | CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import {AppRegistry} from 'react-native';
6 | import App from './App';
7 | import {name as appName} from './app.json';
8 |
9 | AppRegistry.registerComponent(appName, () => App);
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = false
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | quote_type = double
9 |
10 |
11 | [*.js]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.mustache]
16 | indent_style = space
17 | indent_size = 2
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | module.exports = {
9 | transformer: {
10 | getTransformOptions: async () => ({
11 | transform: {
12 | experimentalImportSupport: false,
13 | inlineRequires: false,
14 | },
15 | }),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { View } from "react-native";
3 |
4 | import Root from "./Root";
5 |
6 | export default class App extends Component {
7 |
8 | render() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | const styles = {
18 | container: {
19 | flex: 1
20 | }
21 | };
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatkit-demo-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@pusher/chatkit-server": "^1.1.0",
14 | "body-parser": "^1.18.3",
15 | "cors": "^2.8.5",
16 | "dotenv": "^7.0.0",
17 | "express": "^4.16.4"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Root.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { YellowBox } from 'react-native';
3 | import { createStackNavigator, createAppContainer } from 'react-navigation';
4 | import Login from './src/screens/Login';
5 | import Groups from './src/screens/Groups';
6 | import Chat from './src/screens/Chat';
7 |
8 | YellowBox.ignoreWarnings(["Setting a timer"]);
9 |
10 | const RootStack = createStackNavigator(
11 | {
12 | Login,
13 | Groups,
14 | Chat
15 | },
16 | {
17 | initialRouteName: "Login"
18 | }
19 | );
20 |
21 | const AppContainer = createAppContainer(RootStack);
22 |
23 | class Router extends Component {
24 | render() {
25 | return ;
26 | }
27 | }
28 |
29 | export default Router;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 |
33 | # node.js
34 | #
35 | node_modules/
36 | npm-debug.log
37 | yarn-error.log
38 |
39 | # BUCK
40 | buck-out/
41 | \.buckd/
42 | *.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
--------------------------------------------------------------------------------
/src/components/ChatBubble.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { View } from "react-native";
3 | import { MessageText, Time } from "react-native-gifted-chat";
4 |
5 | const ChatBubble = (props) => {
6 | const { position, children, currentMessage, uri } = props;
7 | return (
8 |
9 |
10 |
11 | {children}
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = {
19 | left: {
20 | container: {
21 | flex: 1,
22 | alignItems: 'flex-start',
23 | },
24 | wrapper: {
25 | borderRadius: 15,
26 | backgroundColor: '#f0f0f0',
27 | marginRight: 60,
28 | minHeight: 20,
29 | justifyContent: 'flex-end',
30 | }
31 | },
32 | right: {
33 | container: {
34 | flex: 1,
35 | alignItems: 'flex-end',
36 | },
37 | wrapper: {
38 | borderRadius: 15,
39 | backgroundColor: '#0084ff',
40 | marginLeft: 60,
41 | minHeight: 20,
42 | justifyContent: 'flex-end',
43 | }
44 | }
45 | }
46 |
47 | export default ChatBubble;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNChatkitDemo",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node node_modules/react-native/local-cli/cli.js start",
7 | "test": "jest"
8 | },
9 | "dependencies": {
10 | "@pusher/chatkit-client": "^1.4.1",
11 | "axios": "^0.18.0",
12 | "react": "16.8.3",
13 | "react-native": "0.59.1",
14 | "react-native-audio-toolkit": "^1.0.6",
15 | "react-native-config": "^0.11.7",
16 | "react-native-document-picker": "^2.3.0",
17 | "react-native-fs": "^2.13.3",
18 | "react-native-gesture-handler": "^1.1.0",
19 | "react-native-gifted-chat": "^0.7.2",
20 | "react-native-mime-types": "^2.2.1",
21 | "react-native-modal": "^9.0.0",
22 | "react-native-vector-icons": "^6.4.2",
23 | "react-native-video": "^4.4.0",
24 | "react-navigation": "^3.5.1",
25 | "rn-fetch-blob": "^0.10.15"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.4.0",
29 | "@babel/runtime": "^7.4.0",
30 | "babel-jest": "^24.5.0",
31 | "jest": "^24.5.0",
32 | "metro-react-native-babel-preset": "^0.53.1",
33 | "react-test-renderer": "16.8.3"
34 | },
35 | "jest": {
36 | "preset": "react-native"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore "BUCK" generated dirs
6 | /\.buckd/
7 |
8 | ; Ignore unexpected extra "@providesModule"
9 | .*/node_modules/.*/node_modules/fbjs/.*
10 |
11 | ; Ignore duplicate module providers
12 | ; For RN Apps installed via npm, "Libraries" folder is inside
13 | ; "node_modules/react-native" but in the source repo it is in the root
14 | .*/Libraries/react-native/React.js
15 |
16 | ; Ignore polyfills
17 | .*/Libraries/polyfills/.*
18 |
19 | ; Ignore metro
20 | .*/node_modules/metro/.*
21 |
22 | [include]
23 |
24 | [libs]
25 | node_modules/react-native/Libraries/react-native/react-native-interface.js
26 | node_modules/react-native/flow/
27 |
28 | [options]
29 | emoji=true
30 |
31 | esproposal.optional_chaining=enable
32 | esproposal.nullish_coalescing=enable
33 |
34 | module.system=haste
35 | module.system.haste.use_name_reducers=true
36 | # get basename
37 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
38 | # strip .js or .js.flow suffix
39 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
40 | # strip .ios suffix
41 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
42 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
43 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
44 | module.system.haste.paths.blacklist=.*/__tests__/.*
45 | module.system.haste.paths.blacklist=.*/__mocks__/.*
46 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.*
47 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.*
48 |
49 | munge_underscores=true
50 |
51 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
52 |
53 | module.file_ext=.js
54 | module.file_ext=.jsx
55 | module.file_ext=.json
56 | module.file_ext=.native.js
57 |
58 | suppress_type=$FlowIssue
59 | suppress_type=$FlowFixMe
60 | suppress_type=$FlowFixMeProps
61 | suppress_type=$FlowFixMeState
62 |
63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
64 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
65 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
66 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
67 |
68 | [version]
69 | ^0.92.0
70 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const bodyParser = require("body-parser");
3 | const cors = require("cors");
4 | const Chatkit = require("@pusher/chatkit-server");
5 |
6 | require("dotenv").config();
7 |
8 | const app = express();
9 | const INSTANCE_LOCATOR_ID = process.env.CHATKIT_INSTANCE_LOCATOR_ID;
10 | const CHATKIT_SECRET = process.env.CHATKIT_SECRET_KEY;
11 |
12 | const chatkit = new Chatkit.default({
13 | instanceLocator: `v1:us1:${INSTANCE_LOCATOR_ID}`,
14 | key: CHATKIT_SECRET
15 | });
16 |
17 | app.use(bodyParser.urlencoded({ extended: false }));
18 | app.use(bodyParser.json());
19 | app.use(cors());
20 |
21 | app.post("/auth", (req, res) => {
22 | const { user_id } = req.query;
23 | const authData = chatkit.authenticate({
24 | userId: user_id
25 | });
26 |
27 | res.status(authData.status)
28 | .send(authData.body);
29 | });
30 |
31 | let users = [];
32 | app.get("/users", async (req, res) => {
33 | try {
34 | users = await chatkit.getUsers();
35 | res.send({ users });
36 | } catch (get_users_err) {
37 | console.log("error getting users: ", get_users_err);
38 | }
39 | });
40 |
41 | app.post("/user", async (req, res) => {
42 | // note: don't forget to access http://localhost:5000/users on your browser first
43 | // because this route depends on the users variable to be filled
44 | const { username } = req.body;
45 | try {
46 | const user = users.find((usr) => usr.name == username);
47 | res.send({ user });
48 | } catch (get_user_err) {
49 | console.log("error getting user: ", get_user_err);
50 | }
51 | });
52 |
53 |
54 | app.post("/user/permissions", async(req, res) => {
55 | const { room_id, user_id } = req.body;
56 | try {
57 | const roles = await chatkit.getUserRoles({ userId: user_id });
58 | const role = roles.find(role => role.room_id == room_id);
59 | const permissions = (role) ? role.permissions : [];
60 |
61 | res.send({ permissions });
62 | } catch (user_permissions_err) {
63 | console.log("error getting user permissions: ", user_permissions_err);
64 | }
65 | });
66 |
67 |
68 | app.post("/rooms", async (req, res) => {
69 | const { user_id } = req.body;
70 | try {
71 | const rooms = await chatkit.getUserRooms({
72 | userId: user_id
73 | });
74 | res.send({ rooms });
75 | } catch (get_rooms_err) {
76 | console.log("error getting rooms: ", get_rooms_err);
77 | }
78 | });
79 |
80 | const PORT = 5000;
81 | app.listen(PORT, (err) => {
82 | if (err) {
83 | console.error(err);
84 | } else {
85 | console.log(`Running on ports ${PORT}`);
86 | }
87 | });
--------------------------------------------------------------------------------
/src/screens/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { View, Text, TextInput, Button } from "react-native";
3 | import axios from "axios";
4 |
5 | const CHAT_SERVER = "YOUR NGROK HTTPS URL";
6 |
7 | class Login extends Component {
8 | static navigationOptions = {
9 | title: "Login"
10 | };
11 |
12 |
13 | state = {
14 | username: "",
15 | is_loading: false
16 | };
17 |
18 | //
19 |
20 | render() {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | Enter your username
28 | this.setState({ username })}
31 | value={this.state.username}
32 | />
33 |
34 |
35 | {!this.state.is_loading && (
36 |
37 | )}
38 |
39 | {this.state.is_loading && (
40 | Loading...
41 | )}
42 |
43 |
44 |
45 | );
46 | }
47 |
48 |
49 | login = async () => {
50 | const username = this.state.username;
51 | this.setState({
52 | is_loading: true
53 | });
54 |
55 | if (username) {
56 | try {
57 | const response = await axios.post(`${CHAT_SERVER}/user`, { username });
58 | const { user } = response.data;
59 |
60 | this.props.navigation.navigate("Groups", {
61 | ...user
62 | });
63 |
64 | } catch (login_err) {
65 | console.log("error logging in user: ", login_err);
66 | }
67 | }
68 |
69 | await this.setState({
70 | is_loading: false,
71 | username: ""
72 | });
73 |
74 | };
75 | }
76 |
77 | export default Login;
78 |
79 | const styles = {
80 | wrapper: {
81 | flex: 1
82 | },
83 | container: {
84 | flex: 1,
85 | alignItems: "center",
86 | justifyContent: "center",
87 | padding: 20,
88 | backgroundColor: "#FFF"
89 | },
90 | fieldContainer: {
91 | marginTop: 20
92 | },
93 | label: {
94 | fontSize: 16
95 | },
96 | textInput: {
97 | height: 40,
98 | marginTop: 5,
99 | marginBottom: 10,
100 | borderColor: "#ccc",
101 | borderWidth: 1,
102 | backgroundColor: "#eaeaea",
103 | padding: 5
104 | },
105 | loadingText: {
106 | alignSelf: "center"
107 | }
108 | };
--------------------------------------------------------------------------------
/src/components/AudioPlayer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { View, Text, TouchableOpacity, Animated } from "react-native";
3 | import Icon from "react-native-vector-icons/FontAwesome";
4 | import { Player } from "react-native-audio-toolkit";
5 |
6 | class AudioPlayer extends Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.progress = new Animated.Value(0);
11 | this.state = {
12 | progress: 0,
13 | icon: 'play'
14 | };
15 |
16 | this.player = null;
17 | }
18 |
19 |
20 | componentDidMount() {
21 | this.audio_url = this.props.url;
22 |
23 | this.player = new Player(this.audio_url);
24 | this.player.prepare((err) => {
25 | if (this.player.isPrepared) {
26 | this.player_duration = this.player.duration;
27 | }
28 | });
29 |
30 | this.player.on('ended', () => {
31 | this.player = null;
32 | this.setState({
33 | icon: 'play'
34 | });
35 | });
36 | }
37 |
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | //
56 |
57 | getProgressStyles = () => {
58 | const animated_width = this.progress.interpolate({
59 | inputRange: [0, 50, 100],
60 | outputRange: [0, 50, 100]
61 | });
62 |
63 | return {
64 | width: animated_width,
65 | backgroundColor: '#f1a91b',
66 | height: 5
67 | }
68 | }
69 |
70 | //
71 |
72 | toggleAudioPlayer = () => {
73 | if (this.player.isPlaying) {
74 | this.setState({
75 | icon: 'play'
76 | });
77 | this.player.pause();
78 | this.progress.stopAnimation((value) => {
79 | this.player_duration = this.player.duration - (this.player.duration * (Math.ceil(value) / 100));
80 | });
81 | } else {
82 | this.setState({
83 | icon: 'pause'
84 | });
85 | this.player.play();
86 |
87 | Animated.timing(this.progress, {
88 | duration: this.player_duration,
89 | toValue: 100
90 | }).start();
91 |
92 | }
93 | }
94 | }
95 |
96 | const styles = {
97 | container: {
98 | flexDirection: 'row',
99 | alignItems: 'center',
100 | margin: 10
101 | },
102 | rail: {
103 | width: 100,
104 | height: 5,
105 | marginLeft: 5,
106 | backgroundColor: '#2C2C2C'
107 | }
108 | }
109 |
110 | export default AudioPlayer;
--------------------------------------------------------------------------------
/src/screens/Groups.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { View, Text, FlatList, Button } from "react-native";
3 | import axios from "axios";
4 |
5 | const CHAT_SERVER = "YOUR NGROK HTTPS URL";
6 |
7 | class Groups extends Component {
8 | static navigationOptions = {
9 | title: "Groups"
10 | };
11 |
12 | state = {
13 | rooms: []
14 | };
15 |
16 |
17 | constructor(props) {
18 | super(props);
19 | const { navigation } = this.props;
20 | this.user_id = navigation.getParam("id");
21 | }
22 |
23 | //
24 |
25 | async componentDidMount() {
26 | try {
27 | const response = await axios.post(`${CHAT_SERVER}/rooms`, { user_id: this.user_id });
28 | const { rooms } = response.data;
29 |
30 | this.setState({
31 | rooms
32 | });
33 | } catch (get_rooms_err) {
34 | console.log("error getting rooms: ", get_rooms_err);
35 | }
36 | }
37 |
38 |
39 | render() {
40 | const { rooms } = this.state;
41 |
42 | return (
43 |
44 | {
45 | rooms &&
46 | item.id.toString()}
48 | data={rooms}
49 | renderItem={this.renderRoom}
50 | />
51 | }
52 |
53 |
54 | );
55 | }
56 |
57 | //
58 |
59 | renderRoom = ({ item }) => {
60 | return (
61 |
62 | {item.name}
63 |
67 | );
68 | }
69 |
70 | //
71 |
72 | enterChat = async (room) => {
73 | try {
74 | const response = await axios.post(`${CHAT_SERVER}/user/permissions`, { room_id: room.id, user_id: this.user_id });
75 | const { permissions } = response.data;
76 | const is_room_admin = (permissions.indexOf('room:members:add') !== -1);
77 |
78 | this.props.navigation.navigate("Chat", {
79 | user_id: this.user_id,
80 | room_id: room.id,
81 | room_name: room.name,
82 | is_room_admin
83 | });
84 |
85 | } catch (get_permissions_err) {
86 | console.log("error getting permissions: ", get_permissions_err);
87 | }
88 | }
89 | }
90 |
91 | export default Groups;
92 |
93 | const styles = {
94 | container: {
95 | flex: 1,
96 | backgroundColor: "#FFF"
97 | },
98 | list_item: {
99 | flex: 1,
100 | flexDirection: 'row',
101 | justifyContent: 'space-between',
102 | padding: 20,
103 | borderBottomWidth: 1,
104 | borderBottomColor: '#ccc',
105 | },
106 | list_item_text: {
107 | marginLeft: 10,
108 | fontSize: 20,
109 | }
110 | };
--------------------------------------------------------------------------------
/src/components/VideoPlayer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import {
4 | TouchableOpacity,
5 | View,
6 | } from "react-native";
7 |
8 | import Video from "react-native-video";
9 | import Icon from "react-native-vector-icons/FontAwesome";
10 |
11 | class VideoPlayer extends Component {
12 |
13 | state = {
14 | rate: 1,
15 | volume: 1,
16 | muted: false,
17 | resizeMode: 'contain',
18 | duration: 0.0,
19 | currentTime: 0.0,
20 | paused: true,
21 | };
22 |
23 | onLoad = (data) => {
24 | this.setState({ duration: data.duration });
25 | }
26 |
27 | onProgress = (data) => {
28 | this.setState({ currentTime: data.currentTime });
29 | }
30 |
31 | onEnd = () => {
32 | this.setState({ paused: true });
33 | this.video.seek(0);
34 | }
35 |
36 |
37 | getCurrentTimePercentage() {
38 | if (this.state.currentTime > 0) {
39 | return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
40 | }
41 | return 0;
42 | };
43 |
44 |
45 | render() {
46 | const flexCompleted = this.getCurrentTimePercentage() * 100;
47 | const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
48 | const icon = (this.state.paused) ? 'play' : 'pause';
49 |
50 | const { uri } = this.props;
51 |
52 | return (
53 |
54 | this.setState({ paused: !this.state.paused })}
57 | >
58 | {
59 | uri &&
60 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 |
87 | const styles = {
88 | container: {
89 | flex: 1,
90 | justifyContent: 'center',
91 | alignItems: 'center',
92 | backgroundColor: '#000'
93 | },
94 | fullScreen: {
95 | position: 'absolute',
96 | top: 0,
97 | left: 0,
98 | bottom: 0,
99 | right: 0,
100 | },
101 | controls: {
102 | flexDirection: 'row',
103 | backgroundColor: 'transparent',
104 | borderRadius: 5,
105 | position: 'absolute',
106 | bottom: 20,
107 | left: 20,
108 | right: 20,
109 | alignItems: 'center'
110 | },
111 | progress: {
112 | flex: 1,
113 | flexDirection: 'row',
114 | borderRadius: 3,
115 | overflow: 'hidden',
116 | marginLeft: 10
117 | },
118 | innerProgressCompleted: {
119 | height: 10,
120 | backgroundColor: '#f1a91b',
121 | },
122 | innerProgressRemaining: {
123 | height: 10,
124 | backgroundColor: '#2C2C2C',
125 | }
126 | }
127 |
128 | export default VideoPlayer;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RNChatkitDemo
2 | A demo chat app built with React Native and Chatkit which has the following features:
3 |
4 | - Public and private chat rooms
5 | - Roles and permissions
6 | - Typing indicators
7 | - Read receipt
8 | - File uploads
9 | - Show online and offline users
10 |
11 | You can read the full tutorial at: [Build a demo chat app with React Native and Chatkit](https://pusher.com/tutorials/chatroom-app-react-native)
12 |
13 | ### Prerequisites
14 |
15 | - React Native development environment
16 | - [Node.js](https://nodejs.org/en/)
17 | - [Yarn](https://yarnpkg.com/en/)
18 | - [Chatkit app instance](https://pusher.com/chatkit)
19 | - [ngrok account](https://ngrok.com/)
20 |
21 | ## Getting Started
22 |
23 | 1. Clone the repo:
24 |
25 | ```
26 | git clone https://github.com/anchetaWern/RNChatkitDemo.git
27 | cd RNChatkitDemo
28 | ```
29 |
30 | 2. Install the app dependencies:
31 |
32 | ```
33 | yarn
34 | ```
35 |
36 | 3. Eject the project (re-creates the `ios` and `android` folders):
37 |
38 | ```
39 | react-native eject
40 | ```
41 |
42 | 4. Link the packages:
43 |
44 | ```
45 | react-native link react-native-gesture-handler
46 | react-native link react-native-permissions
47 | react-native link react-native-document-picker
48 | react-native link react-native-fs
49 | react-native link react-native-config
50 | react-native link react-native-vector-icons
51 | react-native link rn-fetch-blob
52 | ```
53 |
54 | 5. Update `android/app/build.gradle` file:
55 |
56 | ```
57 | apply from: "../../node_modules/react-native/react.gradle"
58 |
59 | // add these:
60 | apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
61 | apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
62 | ```
63 |
64 | 6. Update `android/app/src/main/AndroidManifest.xml` to add permission to read from external storage:
65 |
66 | ```
67 |
69 |
70 |
71 | ...
72 |
73 | ```
74 |
75 | 7. Update `.env` file with your Chatkit credentials.
76 |
77 | 8. Set up the server:
78 |
79 | ```
80 | cd server
81 | yarn
82 | ```
83 |
84 | 9. Update the `server/.env` file with your Chatkit credentials.
85 |
86 | 10. Run the server:
87 |
88 | ```
89 | yarn start
90 | ```
91 |
92 | 11. Run ngrok:
93 |
94 | ```
95 | ./ngrok http 5000
96 | ```
97 |
98 | 12. Update the `src/screens/Login.js`, `src/screens/Group.js`, and `src/screens/Chat.js` file with your ngrok https URL.
99 |
100 | 13. Run the app:
101 |
102 | ```
103 | react-native run-android
104 | react-native run-ios
105 | ```
106 |
107 | 14. Log in to the app on two separate devices (or emulator).
108 |
109 | ## Built With
110 |
111 | - [React Native](http://facebook.github.io/react-native/)
112 | - [Chatkit](https://pusher.com/chatkit)
113 |
114 | ## Donation
115 |
116 | If this project helped you reduce time to develop, please consider buying me a cup of coffee :)
117 |
118 |
119 |
--------------------------------------------------------------------------------
/src/screens/Chat.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { View, Text, ActivityIndicator, FlatList, TouchableOpacity, Alert } from "react-native";
3 | import { GiftedChat, Send, Message } from "react-native-gifted-chat";
4 | import { ChatManager, TokenProvider } from "@pusher/chatkit-client";
5 | import axios from "axios";
6 | import Config from "react-native-config";
7 | import Icon from "react-native-vector-icons/FontAwesome";
8 | import { DocumentPicker, DocumentPickerUtil } from "react-native-document-picker";
9 | import * as mime from "react-native-mime-types";
10 | import Modal from "react-native-modal";
11 | import RNFetchBlob from "rn-fetch-blob";
12 |
13 | const Blob = RNFetchBlob.polyfill.Blob;
14 | const fs = RNFetchBlob.fs;
15 | window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
16 | window.Blob = Blob;
17 |
18 | import RNFS from "react-native-fs";
19 |
20 | import ChatBubble from "../components/ChatBubble";
21 | import AudioPlayer from "../components/AudioPlayer";
22 | import VideoPlayer from "../components/VideoPlayer";
23 |
24 | const CHATKIT_INSTANCE_LOCATOR_ID = `v1:us1:${Config.CHATKIT_INSTANCE_LOCATOR_ID}`;
25 | const CHATKIT_SECRET_KEY = Config.CHATKIT_SECRET_KEY;
26 |
27 | const CHAT_SERVER = "YOUR NGROK HTTPS URL";
28 | const CHATKIT_TOKEN_PROVIDER_ENDPOINT = `${CHAT_SERVER}/auth`;
29 |
30 | class Chat extends Component {
31 |
32 | static navigationOptions = ({ navigation }) => {
33 | const { params } = navigation.state;
34 | return {
35 | headerTitle: params.room_name,
36 | headerRight: (
37 |
38 |
39 |
40 | Users
41 |
42 |
43 |
44 | ),
45 | headerStyle: {
46 | backgroundColor: "#333"
47 | },
48 | headerTitleStyle: {
49 | color: "#FFF"
50 | }
51 | };
52 | };
53 |
54 |
55 | state = {
56 | company_users: null,
57 | room_users: null,
58 | messages: [],
59 | is_initialized: false,
60 | is_picking_file: false,
61 | show_load_earlier: false,
62 |
63 | is_video_modal_visible: false,
64 | is_last_viewed_message_modal_visible: false,
65 | is_users_modal_visible: false,
66 |
67 | is_typing: false,
68 | typing_user: null,
69 |
70 | viewed_user: null,
71 | viewed_message: null
72 | };
73 |
74 |
75 | constructor(props) {
76 | super(props);
77 | const { navigation } = this.props;
78 |
79 | this.user_id = navigation.getParam("user_id");
80 | this.room_id = navigation.getParam("room_id");
81 | this.is_room_admin = navigation.getParam("is_room_admin");
82 |
83 | this.modal_types = {
84 | video: 'is_video_modal_visible',
85 | last_viewed_message: 'is_last_viewed_message_modal_visible',
86 | users: 'is_users_modal_visible'
87 | }
88 | }
89 |
90 |
91 | async componentDidMount() {
92 |
93 | this.props.navigation.setParams({
94 | showUsersModal: this.showUsersModal
95 | });
96 |
97 | try {
98 | const chatManager = new ChatManager({
99 | instanceLocator: CHATKIT_INSTANCE_LOCATOR_ID,
100 | userId: this.user_id,
101 | tokenProvider: new TokenProvider({ url: CHATKIT_TOKEN_PROVIDER_ENDPOINT })
102 | });
103 |
104 | let currentUser = await chatManager.connect();
105 | this.currentUser = currentUser;
106 |
107 | await this.currentUser.subscribeToRoomMultipart({
108 | roomId: this.room_id,
109 | hooks: {
110 | onMessage: this.onReceive,
111 | onUserStartedTyping: this.startTyping,
112 | onUserStoppedTyping: this.stopTyping
113 | }
114 | });
115 |
116 | await this.setState({
117 | is_initialized: true,
118 | room_users: this.currentUser.users
119 | });
120 |
121 | } catch (chat_mgr_err) {
122 | console.log("error with chat manager: ", chat_mgr_err);
123 | }
124 | }
125 |
126 | //
127 |
128 | startTyping = (user) => {
129 | this.setState({
130 | is_typing: true,
131 | typing_user: user.name
132 | });
133 | }
134 |
135 |
136 | stopTyping = (user) => {
137 | this.setState({
138 | is_typing: false,
139 | typing_user: null
140 | });
141 | }
142 |
143 |
144 | onReceive = async (data) => {
145 | this.last_message_id = data.id;
146 | const { message } = await this.getMessage(data);
147 | await this.setState((previousState) => ({
148 | messages: GiftedChat.append(previousState.messages, message)
149 | }));
150 |
151 | if (this.state.messages.length > 9) {
152 | this.setState({
153 | show_load_earlier: true
154 | });
155 | }
156 | }
157 |
158 |
159 | onSend = async ([message]) => {
160 | let message_parts = [
161 | { type: "text/plain", content: message.text }
162 | ];
163 |
164 | if (this.attachment) {
165 | const { file_blob, file_name, file_type } = this.attachment;
166 | message_parts.push({
167 | file: file_blob,
168 | name: file_name,
169 | type: file_type
170 | });
171 | }
172 |
173 | this.setState({
174 | is_sending: true
175 | });
176 |
177 | try {
178 | if (this.last_message_id) {
179 | const set_cursor_response = await this.currentUser.setReadCursor({
180 | roomId: this.room_id,
181 | position: this.last_message_id
182 | });
183 | }
184 |
185 | await this.currentUser.sendMultipartMessage({
186 | roomId: this.room_id,
187 | parts: message_parts
188 | });
189 |
190 | this.attachment = null;
191 | await this.setState({
192 | is_sending: false
193 | });
194 | } catch (send_msg_err) {
195 | console.log("error sending message: ", send_msg_err);
196 | }
197 | }
198 |
199 |
200 | renderSend = props => {
201 | if (this.state.is_sending) {
202 | return (
203 |
208 | );
209 | }
210 |
211 | return ;
212 | }
213 |
214 |
215 | getMessage = async ({ id, sender, parts, createdAt }) => {
216 |
217 | const text = parts.find(part => part.partType === 'inline').payload.content;
218 | const attachment = parts.find(part => part.partType === 'attachment');
219 |
220 | const attachment_url = (attachment) ? await attachment.payload.url() : null;
221 | const attachment_type = (attachment) ? attachment.payload.type : null;
222 |
223 | const msg_data = {
224 | _id: id,
225 | text: text,
226 | createdAt: new Date(createdAt),
227 | user: {
228 | _id: sender.id,
229 | name: sender.name,
230 | avatar: `https://ui-avatars.com/api/?background=d88413&color=FFF&name=${sender.name}`
231 | }
232 | };
233 |
234 | if (attachment) {
235 | Object.assign(msg_data, { attachment: { url: attachment_url, type: attachment_type } });
236 | }
237 |
238 | if (attachment && attachment_type.indexOf('video') !== -1) {
239 | Object.assign(msg_data, { video: attachment_url });
240 | }
241 |
242 | if (attachment && attachment_type.indexOf('image') !== -1) {
243 | Object.assign(msg_data, { image: attachment_url });
244 | }
245 |
246 | return {
247 | message: msg_data
248 | };
249 | }
250 |
251 |
252 | asyncForEach = async (array, callback) => {
253 | for (let index = 0; index < array.length; index++) {
254 | await callback(array[index], index, array);
255 | }
256 | };
257 |
258 |
259 | renderMessage = (msg) => {
260 | const { attachment } = msg.currentMessage;
261 | const renderBubble = (attachment && attachment.type.indexOf('audio') !== -1) ? this.renderPreview.bind(this, attachment.url) : null;
262 | const onLongPress = (attachment && attachment.type.indexOf('video') !== -1) ? this.onLongPressMessageBubble.bind(this, attachment.url) : null;
263 |
264 | const modified_msg = {
265 | ...msg,
266 | renderBubble,
267 | onLongPress,
268 | videoProps: {
269 | paused: true
270 | }
271 | }
272 |
273 | return
274 | }
275 |
276 | //
277 |
278 | onLongPressMessageBubble = (link) => {
279 | this.setState({
280 | is_video_modal_visible: true,
281 | video_uri: link
282 | });
283 | }
284 |
285 |
286 | renderPreview = (uri, bubbleProps) => {
287 | const text_color = (bubbleProps.position == 'right') ? '#FFF' : '#000';
288 | const modified_bubbleProps = {
289 | ...bubbleProps
290 | };
291 |
292 | return (
293 |
294 |
295 |
296 | );
297 | }
298 |
299 | //
300 |
301 | render() {
302 | const {
303 | is_initialized,
304 | room_users,
305 | messages,
306 | video_uri,
307 | is_video_modal_visible,
308 | is_last_viewed_message_modal_visible,
309 | viewed_user,
310 | viewed_message,
311 | is_users_modal_visible,
312 | is_add_user_modal_visible,
313 | show_load_earlier,
314 | typing_user
315 | } = this.state;
316 |
317 | return (
318 |
319 | {(!is_initialized) && (
320 |
325 | )}
326 |
327 | {is_initialized && (
328 | this.onSend(messages)}
331 | user={{
332 | _id: this.user_id
333 | }}
334 | renderActions={this.renderCustomActions}
335 | renderSend={this.renderSend}
336 | renderMessage={this.renderMessage}
337 | onInputTextChanged={this.onTyping}
338 | renderFooter={this.renderFooter}
339 | extraData={{ typing_user }}
340 | onPressAvatar={this.viewLastReadMessage}
341 |
342 | loadEarlier={show_load_earlier}
343 | onLoadEarlier={this.loadEarlierMessages}
344 | />
345 | )}
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 | {
357 | viewed_user && viewed_message &&
358 |
359 |
360 |
361 | Last viewed msg: {viewed_user}
362 |
363 |
364 |
365 |
366 |
367 |
368 | Message: {viewed_message}
369 |
370 |
371 |
372 | }
373 |
374 | {
375 | room_users &&
376 |
377 |
378 |
379 | Users
380 |
381 |
382 |
383 |
384 |
385 |
386 | item.id.toString()}
388 | data={room_users}
389 | renderItem={this.renderUser}
390 | />
391 |
392 |
393 |
394 | }
395 |
396 | );
397 | }
398 |
399 | //
400 |
401 | viewLastReadMessage = async (data) => {
402 | try {
403 | const cursor = await this.currentUser.readCursor({
404 | userId: data.userId,
405 | roomId: this.room_id
406 | });
407 |
408 | const viewed_message = this.state.messages.find(msg => msg._id == cursor.position);
409 |
410 | await this.setState({
411 | viewed_user: data.name,
412 | is_last_viewed_message_modal_visible: true,
413 | viewed_message: viewed_message.text ? viewed_message.text : ''
414 | });
415 | } catch (view_last_msg_err) {
416 | console.log("error viewing last message: ", view_last_msg_err);
417 | }
418 | }
419 |
420 |
421 | showUsersModal = () => {
422 | this.setState({
423 | is_users_modal_visible: true
424 | });
425 | }
426 |
427 | //
428 |
429 | renderUser = ({ item }) => {
430 | const online_status = item.presenceStore[item.id];
431 |
432 | return (
433 |
434 |
435 |
436 | {item.name}
437 |
438 |
439 | );
440 | }
441 |
442 | //
443 |
444 | hideModal = (type) => {
445 | const modal = this.modal_types[type];
446 | this.setState({
447 | [modal]: false
448 | });
449 | }
450 |
451 |
452 | renderCustomActions = () => {
453 | if (!this.state.is_picking_file) {
454 | const icon_color = this.attachment ? "#0064e1" : "#808080";
455 |
456 | return (
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 | );
465 | }
466 |
467 | return (
468 |
469 | );
470 | }
471 |
472 | //
473 |
474 | openFilePicker = async () => {
475 | await this.setState({
476 | is_picking_file: true
477 | });
478 |
479 | DocumentPicker.show({
480 | filetype: [DocumentPickerUtil.allFiles()],
481 | }, async (err, file) => {
482 | if (!err) {
483 |
484 | try {
485 | const file_type = mime.contentType(file.fileName);
486 | const base64 = await RNFS.readFile(file.uri, "base64");
487 |
488 | const file_blob = await Blob.build(base64, { type: `${file_type};BASE64` });
489 |
490 | this.attachment = {
491 | file_blob: file_blob,
492 | file_name: file.fileName,
493 | file_type: file_type
494 | };
495 |
496 | Alert.alert("Success", "File attached!");
497 |
498 | } catch (attach_err) {
499 | console.log("error attaching file: ", attach_err);
500 | }
501 | }
502 |
503 | this.setState({
504 | is_picking_file: false
505 | });
506 | });
507 | }
508 |
509 |
510 | onTyping = async () => {
511 | try {
512 | await this.currentUser.isTypingIn({ roomId: this.room_id });
513 | } catch (typing_err) {
514 | console.log("error setting is typing: ", typing_err);
515 | }
516 | }
517 |
518 |
519 | renderFooter = () => {
520 | const { is_typing, typing_user } = this.state;
521 | if (is_typing) {
522 | return (
523 |
524 |
525 | {typing_user} is typing...
526 |
527 |
528 | );
529 | }
530 | return null;
531 | }
532 |
533 |
534 | loadEarlierMessages = async () => {
535 | this.setState({
536 | is_loading: true
537 | });
538 |
539 | const earliest_message_id = Math.min(
540 | ...this.state.messages.map(m => parseInt(m._id))
541 | );
542 |
543 | try {
544 | let messages = await this.currentUser.fetchMultipartMessages({
545 | roomId: this.room_id,
546 | initialId: earliest_message_id,
547 | direction: "older",
548 | limit: 10
549 | });
550 |
551 | if (!messages.length) {
552 | this.setState({
553 | show_load_earlier: false
554 | });
555 | }
556 |
557 | let earlier_messages = [];
558 | await this.asyncForEach(messages, async (msg) => {
559 | let { message } = await this.getMessage(msg);
560 | earlier_messages.push(message);
561 | });
562 |
563 | await this.setState(previousState => ({
564 | messages: previousState.messages.concat(earlier_messages)
565 | }));
566 | } catch (err) {
567 | console.log("error occured while trying to load older messages", err);
568 | }
569 |
570 | await this.setState({
571 | is_loading: false
572 | });
573 | }
574 |
575 | }
576 |
577 |
578 | const styles = {
579 | container: {
580 | flex: 1
581 | },
582 | loader: {
583 | paddingTop: 20
584 | },
585 |
586 | header_right: {
587 | flex: 1,
588 | flexDirection: "row",
589 | justifyContent: "space-around"
590 | },
591 |
592 | header_button_container: {
593 | marginRight: 10
594 | },
595 | header_button: {
596 |
597 | },
598 | header_button_text: {
599 | color: '#FFF'
600 | },
601 |
602 | sendLoader: {
603 | marginRight: 10,
604 | marginBottom: 10
605 | },
606 | customActionsContainer: {
607 | flexDirection: "row",
608 | justifyContent: "space-between"
609 | },
610 | buttonContainer: {
611 | padding: 10
612 | },
613 | modal: {
614 | flex: 1,
615 | backgroundColor: '#FFF'
616 | },
617 | close: {
618 | alignSelf: 'flex-end',
619 | marginBottom: 10
620 | },
621 | modal_header: {
622 | flexDirection: 'row',
623 | justifyContent: 'space-between',
624 | padding: 10
625 | },
626 | modal_header_text: {
627 | fontSize: 20,
628 | fontWeight: 'bold'
629 | },
630 | modal_body: {
631 | marginTop: 20,
632 | padding: 20
633 | },
634 | centered: {
635 | alignItems: 'center'
636 | },
637 | list_item_body: {
638 | flex: 1,
639 | padding: 10,
640 | flexDirection: "row",
641 | justifyContent: "space-between"
642 | },
643 | list_item: {
644 | flexDirection: 'row',
645 | justifyContent: 'space-between'
646 | },
647 | list_item_text: {
648 | marginLeft: 10,
649 | fontSize: 20,
650 | },
651 | status_indicator: {
652 | width: 10,
653 | height: 10,
654 | borderRadius: 10,
655 | },
656 | online: {
657 | backgroundColor: '#5bb90b'
658 | },
659 | offline: {
660 | backgroundColor: '#606060'
661 | },
662 |
663 | footerContainer: {
664 | marginTop: 5,
665 | marginLeft: 10,
666 | marginRight: 10,
667 | marginBottom: 10,
668 | },
669 | footerText: {
670 | fontSize: 14,
671 | color: '#aaa',
672 | }
673 | }
674 |
675 | export default Chat;
--------------------------------------------------------------------------------