├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── index.svg └── manifest.json ├── src ├── chatkit.js ├── components │ ├── CreateMessageForm │ │ ├── index.js │ │ └── index.module.css │ ├── CreateRoomForm │ │ ├── index.js │ │ └── index.module.css │ ├── FileInput │ │ ├── index.js │ │ └── index.module.css │ ├── JoinRoomScreen │ │ ├── index.js │ │ └── index.module.css │ ├── Message │ │ ├── index.js │ │ └── index.module.css │ ├── MessageList │ │ ├── index.js │ │ └── index.module.css │ ├── RoomHeader │ │ ├── index.js │ │ └── index.module.css │ ├── RoomList │ │ ├── index.js │ │ └── index.module.css │ ├── TypingIndicator │ │ ├── index.js │ │ └── index.module.css │ ├── UserHeader │ │ ├── index.js │ │ └── index.module.css │ ├── UserList │ │ ├── index.js │ │ └── index.module.css │ └── WelcomeScreen │ │ ├── index.js │ │ └── index.module.css ├── index.css └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | client/static/index.js 4 | .DS_Store 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | 5 | script: yarn build 6 | 7 | deploy: 8 | local_dir: build 9 | provider: pages 10 | skip_cleanup: true 11 | github_token: $GITHUB_TOKEN 12 | on: 13 | branch: master 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Pusher, Ltd (https://pusher.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Slack Clone 2 | 3 | [![Star on GitHub][github-star-badge]][github-star] 4 | [![Tweet][twitter-badge]][twitter] 5 | [![Build Status][travis-badge]][travis] 6 | 7 | > Slack clone powered by [Chatkit](https://pusher.com/chatkit). See it in action here https://pusher.github.io/react-slack-clone 8 | 9 | ![demo](https://user-images.githubusercontent.com/1457604/35891289-687ad6ec-0b9b-11e8-99cc-ffbad31a017e.gif) 10 | 11 | This is a static, single page web app bootstrapped with [create-react-app](https://github.com/facebookincubator/create-react-app) for ease of setup, distribution and development. It is a thin UI wrapper around the [pusher-chatkit-client](https://github.com/pusher/chatkit-client-js) library to demonstrate how different features can work together to form a compelling real-time chat client with various potential product applications. 12 | 13 | ## Features 14 | 15 | The Chatkit SDK allows you to implement features you would expect from a chat client. These include: 16 | 17 | * 📝 Public and private chat rooms 18 | * 📡 Realtime sending and receiving of messages 19 | * 📦 Rich media attachments (drag and drop) 20 | * 💬 Typing and presence indicators 21 | * 📚 Read message cursors 22 | 23 | Want to get involved? We have a bunch of [beginner-friendly GitHub issues](https://github.com/pusher/react-slack-clone/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). 24 | 25 | ## Components 26 | 27 | The demo attempts to be feature complete according to documentation [here](https://docs.pusher.com/chatkit/reference/javascript). Feature requests should be made via issues or pull requests to this repository. 28 | 29 | * CreateMessageForm - to send a message with a textual body and trigger typing indicators. 30 | * CreateRoomForm - to create a new room and join it upon creation. 31 | * FileInput - to send a message with a rich media attachment. 32 | * Message - to render out a message that potentially includes an attachment. 33 | * MessageList - to render a list of messages from a key value store. 34 | * RoomHeader - to display useful information about a given room. 35 | * RoomList - to render a list of rooms which can be subscribed to by the current user. 36 | * TypingIndicator - to signify to the user that another user is typing in a given room. 37 | * UserHeader - to display useful information about a given user. 38 | 39 | ## Usage 40 | 41 | To run the application locally; clone the repo, install dependencies and run the app. 42 | 43 | ``` 44 | $ git clone https://github.com/pusher/react-slack-clone 45 | $ cd react-slack-clone 46 | $ yarn && yarn start 47 | ``` 48 | 49 | The app starts in development mode and opens a browser window on `http://localhost:3000`. The project rebuilds and the browser reloads automatically when source files are changed. Any build or runtime errors are propagated and displayed in the browser. 50 | 51 | The app depends on GitHub authentication and a user creation endpoint that is hosted at https://chatkit-demo-server.herokuapp.com. The endpoints are `/auth` and `/token`. 52 | 53 | [github-star-badge]: https://img.shields.io/github/stars/pusher/react-slack-clone.svg?style=social 54 | [github-star]: https://github.com/pusher/react-slack-clone/stargazers 55 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/react-testing-library.svg?style=social 56 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20this%20Slack%20clone%20using%20@pusher%20Chatkit%20%F0%9F%91%89https://github.com/pusher/react-slack-clone 57 | [travis-badge]: https://travis-ci.org/pusher/react-slack-clone.svg?branch=master 58 | [travis]: https://travis-ci.org/pusher/react-slack-clone 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatkit-demo", 3 | "version": "2.1.0", 4 | "homepage": "https://pusher.github.io/react-slack-clone", 5 | "dependencies": { 6 | "@pusher/chatkit-client": "^1.3.0", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-linkify": "^0.2.2", 10 | "react-scripts": "2.0.0", 11 | "vuid": "^1.0.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | }, 19 | "browserslist": { 20 | "development": [ 21 | "last 2 chrome versions", 22 | "last 2 firefox versions", 23 | "last 2 edge versions" 24 | ], 25 | "production": [ 26 | ">1%", 27 | "last 4 versions", 28 | "Firefox ESR", 29 | "not ie < 11" 30 | ] 31 | }, 32 | "prettier": { 33 | "trailingComma": "es5", 34 | "tabWidth": 2, 35 | "semi": false, 36 | "singleQuote": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukejacksonn/react-slack-clone/fd62288d063c811c6e609ac863dd4fd8ab321961/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 24 | Pusher | Chatkit Demo 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Pusher Chatkit Demo", 3 | "name": "Pusher Chatkit SDK Demo", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/chatkit.js: -------------------------------------------------------------------------------- 1 | import Chatkit from '@pusher/chatkit-client' 2 | 3 | const credentials = { 4 | url: (id, token) => 5 | `https://chatkit-demo-server.herokuapp.com/token?user=${id}&token=${token}`, 6 | instanceLocator: 'v1:us1:05f46048-3763-4482-9cfe-51ff327c3f29', 7 | } 8 | 9 | const { instanceLocator, url } = credentials 10 | export default ({ state, actions }, { id, token }) => 11 | new Chatkit.ChatManager({ 12 | tokenProvider: new Chatkit.TokenProvider({ url: url(id, token) }), 13 | instanceLocator, 14 | userId: id, 15 | }) 16 | .connect({ 17 | onUserStartedTyping: actions.isTyping, 18 | onUserStoppedTyping: actions.notTyping, 19 | onAddedToRoom: actions.subscribeToRoom, 20 | onRemovedFromRoom: actions.removeRoom, 21 | onPresenceChanged: actions.setUserPresence, 22 | }) 23 | .then(user => { 24 | // Subscribe to all rooms the user is a member of 25 | Promise.all( 26 | user.rooms.map(room => 27 | user.subscribeToRoom({ 28 | roomId: room.id, 29 | hooks: { onMessage: actions.addMessage }, 30 | }) 31 | ) 32 | ).then(rooms => { 33 | actions.setUser(user) 34 | // Join the first room in the users room list 35 | user.rooms.length > 0 && actions.joinRoom(user.rooms[0]) 36 | }) 37 | }) 38 | .catch(error => console.log('Error on connection', error)) 39 | -------------------------------------------------------------------------------- /src/components/CreateMessageForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './index.module.css' 3 | import { FileInput } from '../FileInput' 4 | 5 | export const CreateMessageForm = ({ 6 | state: { user = {}, room = {}, message = '' }, 7 | actions: { runCommand }, 8 | }) => 9 | room.id ? ( 10 |
{ 13 | e.preventDefault() 14 | 15 | const message = e.target[0].value.trim() 16 | 17 | if (message.length === 0) { 18 | return 19 | } 20 | 21 | e.target[0].value = '' 22 | 23 | message.startsWith('/') 24 | ? runCommand(message.slice(1)) 25 | : user.sendMessage({ 26 | text: message, 27 | roomId: room.id, 28 | }) 29 | }} 30 | > 31 | user.isTypingIn({ roomId: room.id })} 34 | /> 35 | 36 | 41 | 42 | ) : null 43 | -------------------------------------------------------------------------------- /src/components/CreateMessageForm/index.module.css: -------------------------------------------------------------------------------- 1 | .component { 2 | flex: none; 3 | position: relative; 4 | display: flex; 5 | align-items: center; 6 | border-top: 1px solid #e0e0e0; 7 | height: 3.6rem; 8 | display: flex; 9 | width: 100%; 10 | } 11 | 12 | .component input:first-child { 13 | flex: 1 1 100%; 14 | padding: 1rem; 15 | border: 0; 16 | font-size: 1rem; 17 | } 18 | 19 | .component button { 20 | position: relative; 21 | border: 0; 22 | background: #fff; 23 | padding: 0; 24 | cursor: pointer; 25 | } 26 | 27 | .component > * { 28 | margin-right: 0.62rem; 29 | } 30 | 31 | .component svg { 32 | width: 2rem; 33 | height: 2rem; 34 | fill: #006eff; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/CreateRoomForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './index.module.css' 3 | 4 | export const CreateRoomForm = ({ submit }) => ( 5 |
{ 8 | e.preventDefault() 9 | submit({ 10 | name: e.target[0].value, 11 | private: e.target.elements[2].checked, 12 | }) 13 | e.target[0].value = '' 14 | }} 15 | > 16 | 17 | 23 | 28 |
29 | ) 30 | -------------------------------------------------------------------------------- /src/components/CreateRoomForm/index.module.css: -------------------------------------------------------------------------------- 1 | .component { 2 | flex: none; 3 | position: relative; 4 | display: flex; 5 | align-items: center; 6 | border-top: 1px solid #e0e0e0; 7 | height: 3.6rem; 8 | display: flex; 9 | width: 100%; 10 | margin-top: auto; 11 | } 12 | 13 | .component > input { 14 | flex: 1 1 100%; 15 | padding: 1rem; 16 | border: 0; 17 | font-size: 1rem; 18 | } 19 | 20 | .component > button > input { 21 | flex: none; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | bottom: 0; 26 | right: 0; 27 | width: 100%; 28 | height: 100%; 29 | opacity: 0; 30 | z-index: 1; 31 | } 32 | 33 | .component > * { 34 | margin-right: 0.38rem; 35 | } 36 | 37 | .component button { 38 | position: relative; 39 | border: 0; 40 | padding: 0; 41 | background: transparent; 42 | cursor: pointer; 43 | } 44 | 45 | .component svg { 46 | width: 2rem; 47 | height: 2rem; 48 | fill: #006eff; 49 | } 50 | 51 | .component input { 52 | cursor: pointer; 53 | } 54 | 55 | .component input[type='checkbox']:not(:checked) + svg { 56 | opacity: 0.38; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/FileInput/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './index.module.css' 3 | 4 | export const FileInput = ({ state: { user, message, room } }) => 5 | room.id ? ( 6 |
7 | 8 | 9 | 10 | { 14 | const file = e.target.files[0] 15 | file && 16 | user.sendMessage({ 17 | text: message || file.name, 18 | roomId: room.id, 19 | attachment: { 20 | name: file.name.replace(/[^A-Za-z0-9._-]/g, '--'), 21 | file, 22 | }, 23 | }) 24 | }} 25 | /> 26 |
27 | ) : null 28 | -------------------------------------------------------------------------------- /src/components/FileInput/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | } 4 | 5 | .component { 6 | flex: none; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | bottom: 0; 11 | right: 0; 12 | width: 100%; 13 | height: 100%; 14 | opacity: 0; 15 | cursor: pointer; 16 | } 17 | 18 | .component::-webkit-file-upload-button { 19 | cursor: pointer; 20 | } -------------------------------------------------------------------------------- /src/components/JoinRoomScreen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './index.module.css' 3 | 4 | export const JoinRoomScreen = () => ( 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | Join a room, create a room or click on a user avatar and start a 18 | conversation. 19 |

20 |
21 |
22 | ) 23 | -------------------------------------------------------------------------------- /src/components/JoinRoomScreen/index.module.css: -------------------------------------------------------------------------------- 1 | .component { 2 | margin: auto; 3 | text-align: center; 4 | color: #b6b6b6; 5 | } 6 | 7 | .component > * + * { 8 | margin-top: 1rem; 9 | } 10 | 11 | .component > span { 12 | font-size: 3rem; 13 | } 14 | 15 | .component > p { 16 | font-size: 1rem; 17 | line-height: 150%; 18 | max-width: 40ex; 19 | } 20 | 21 | .component svg { 22 | width: 100%; 23 | height: 6rem; 24 | opacity: 0.1; 25 | animation: pulse 2s infinite; 26 | } 27 | 28 | @keyframes pulse { 29 | from { 30 | transform: scale(1); 31 | } 32 | 50% { 33 | transform: scale(0.9); 34 | } 35 | to { 36 | transform: scale(1); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Message/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './index.module.css' 3 | import Linkify from 'react-linkify' 4 | 5 | const time = string => { 6 | const date = new Date(string) 7 | const minutes = date.getMinutes() 8 | return `${date.getHours()}:${minutes < 10 ? '0' + minutes : minutes}` 9 | } 10 | 11 | class Attachment extends React.Component { 12 | componentDidMount() { 13 | if (this.props.link) this.setState({ src: this.props.link }); 14 | } 15 | render() { 16 | return this.state 17 | ? { 18 | image: ( 19 | {this.state.name} 20 | ), 21 | video: