├── .env ├── .gitignore ├── Procfile ├── README.md ├── package.json ├── public └── index.html ├── server.js └── src ├── App.js ├── Chat.js ├── MessageList.js ├── OnlineList.js ├── SendMessageForm.js ├── UsernameForm.js ├── electron-react.js ├── electron-starter.js ├── index.css └── index.js /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | react: npm start 2 | electron: node src/electron-react -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chatkit Retirement Announcement 2 | We are sorry to say that as of April 23 2020, we will be fully retiring our Chatkit product. We understand that this will be disappointing to customers who have come to rely on the service, and are very sorry for the disruption that this will cause for them. Our sales and customer support teams are available at this time to handle enquiries and will support existing Chatkit customers as far as they can with transition. All Chatkit billing has now ceased , and customers will pay no more up to or beyond their usage for the remainder of the service. You can read more about our decision to retire Chatkit here: https://blog.pusher.com/narrowing-our-product-focus. If you are interested in learning about how you can build chat with Pusher Channels, check out our tutorials. 3 | 4 | # React Electron Desktop Chat 5 | 6 | A desktop chat app, built with React, [Electron](https://electronjs.org/), and [Pusher Chatkit](https://pusher.com/chatkit). 7 | 8 | ![](https://cdn-images-1.medium.com/max/1440/0*o0ILCOojCdCs4NB0.gif) 9 | 10 | Read the tutorial on [FreeCodeCamp](https://medium.freecodecamp.org/build-a-desktop-chat-app-with-react-electron-and-chatkit-744d168e6f2f) or dive into the code. 11 | 12 | ## Running the code 13 | Download the project with `git clone` (or [click here](https://github.com/pusher/electron-desktop-chat/archive/master.zip)): 14 | 15 | ``` 16 | git clone https://github.com/pusher/electron-desktop-chat 17 | cd electron-desktop-chat 18 | ``` 19 | 20 | Then, within the `electron-desktop-chat` app directory, run `npm install`. 21 | 22 | Once the dependencies have finished installing, run the server with `node server.js`. 23 | 24 | In another terminal, run the desktop app with `npm run development` 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "min-electron-react", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "homepage": "./", 6 | "main": "src/electron-starter.js", 7 | "devDependencies": { 8 | "electron": "^2.0.1", 9 | "foreman": "^2.0.0", 10 | "react-scripts": "^1.0.14" 11 | }, 12 | "dependencies": { 13 | "@pusher/chatkit": "^0.7.12", 14 | "body-parser": "^1.18.3", 15 | "cors": "^2.8.4", 16 | "express": "^4.16.3", 17 | "pusher-chatkit-server": "^0.12.0", 18 | "react": "^16.0.0", 19 | "react-desktop": "^0.3.5", 20 | "react-dom": "^16.0.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "electron": "electron .", 27 | "dev": "nf start -p 3000" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /server.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 | const config = require('./config') 6 | 7 | const chatkit = new Chatkit.default({ 8 | instanceLocator: 'YOUR INSTANCE LOCATOR', 9 | key: 'YOUR KEY' 10 | }) 11 | const app = express() 12 | 13 | app.use(bodyParser.urlencoded({ extended: false })) 14 | app.use(bodyParser.json()) 15 | app.use(cors()) 16 | 17 | app.post('/users', (req, res) => { 18 | const { username } = req.body 19 | const user = { name: username, id: username } 20 | chatkit 21 | .createUser(user) 22 | .then(() => { 23 | console.log('Created user ', user.name) 24 | res.status(201).json(user) 25 | }) 26 | .catch(error => { 27 | if (error.error === 'services/chatkit/user_already_exists') { 28 | console.log('User already exists ', user.name) 29 | res.status(201).json(user) 30 | } else { 31 | console.error(error) 32 | res.status(error.status).json(error) 33 | } 34 | }) 35 | }) 36 | 37 | app.listen(3001) 38 | console.log('Running on port 3001') 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import UsernameForm from './UsernameForm' 3 | import Chat from './Chat' 4 | 5 | class App extends Component { 6 | state = { 7 | currentUsername: null, 8 | currentId: null, 9 | currentScreen: 'usernameForm' 10 | } 11 | 12 | onUsernameSubmitted = username => { 13 | fetch('http://localhost:3001/users', { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | }, 18 | body: JSON.stringify({ username }) 19 | }) 20 | .then(response => response.json()) 21 | .then(data => { 22 | this.setState({ 23 | currentId: data.id, 24 | currentUsername: data.name, 25 | currentScreen: 'chat' 26 | }) 27 | }) 28 | .catch(error => { 29 | console.error('error', error) 30 | }) 31 | } 32 | 33 | render() { 34 | if (this.state.currentScreen === 'usernameForm') { 35 | return 36 | } 37 | 38 | if (this.state.currentScreen === 'chat') { 39 | return 40 | } 41 | } 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /src/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { ChatManager, TokenProvider } from '@pusher/chatkit' 3 | import MessageList from './MessageList' 4 | import SendMessageForm from './SendMessageForm' 5 | import OnlineList from './OnlineList' 6 | import config from '../configuration.js' 7 | 8 | class Chat extends React.Component { 9 | state = { 10 | currentUser: null, 11 | currentRoom: {}, 12 | messages: [] 13 | } 14 | 15 | componentDidMount() { 16 | const chatkit = new ChatManager({ 17 | instanceLocator: 'YOUR INSTANCE LOCATOR', 18 | userId: this.props.currentId, 19 | tokenProvider: new TokenProvider({ 20 | url: 'YOUR TOKEN PROVIDER URL' 21 | }) 22 | }) 23 | 24 | chatkit 25 | .connect() 26 | .then(currentUser => { 27 | this.setState({ currentUser }) 28 | 29 | return currentUser.subscribeToRoom({ 30 | roomId: 8434070, 31 | messageLimit: 100, 32 | hooks: { 33 | onNewMessage: message => { 34 | this.setState({ 35 | messages: [...this.state.messages, message] 36 | }) 37 | }, 38 | onUserCameOnline: () => this.forceUpdate(), 39 | onUserWentOffline: () => this.forceUpdate(), 40 | onUserJoined: () => this.forceUpdate() 41 | } 42 | }) 43 | }) 44 | .then(currentRoom => { 45 | console.log('currentRoom', currentRoom) 46 | this.setState({ currentRoom }) 47 | }) 48 | .catch(error => console.error('error', error)) 49 | } 50 | 51 | onSend = text => { 52 | this.state.currentUser.sendMessage({ 53 | text, 54 | roomId: this.state.currentRoom.id 55 | }) 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | ) 73 | } 74 | } 75 | 76 | export default Chat 77 | -------------------------------------------------------------------------------- /src/MessageList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | ListView, 4 | ListViewSection, 5 | ListViewSectionHeader, 6 | ListViewRow, 7 | Text 8 | } from 'react-desktop/macOs' 9 | 10 | class MessageList extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | {this.props.messages.map((message, index) => 16 | this.renderItem(message) 17 | )} 18 | 19 | 20 | ) 21 | } 22 | 23 | renderItem(message) { 24 | return ( 25 | 26 | 27 | {message.sender.name}: 28 | 29 | 30 | {message.text} 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | export default MessageList 38 | -------------------------------------------------------------------------------- /src/OnlineList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | ListView, 4 | ListViewSection, 5 | ListViewSectionHeader, 6 | ListViewRow, 7 | Text 8 | } from 'react-desktop/macOs' 9 | 10 | class OnlineList extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | {this.props.users && 16 | this.props.users.map((user, index) => { 17 | if (user.id === this.props.currentUser.id) { 18 | return this.renderItem( 19 | `${user.name} (You)`, 20 | user.id, 21 | user.presence.state 22 | ) 23 | } 24 | return this.renderItem(user.name, user.id, user.presence.state) 25 | })} 26 | 27 | 28 | ) 29 | } 30 | 31 | renderItem(name, id, status) { 32 | const itemStyle = {} 33 | return ( 34 | 35 |
41 | 42 | {name}{' '} 43 | {' '} 44 | 45 | ) 46 | } 47 | } 48 | 49 | export default OnlineList 50 | -------------------------------------------------------------------------------- /src/SendMessageForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button, TextInput } from 'react-desktop/macOs' 3 | 4 | class SendMessageForm extends Component { 5 | state = { 6 | text: '' 7 | } 8 | 9 | onSubmit = e => { 10 | e.preventDefault() 11 | this.props.onSend(this.state.text) 12 | this.setState({ text: '' }) 13 | } 14 | 15 | onChange = e => { 16 | this.setState({ text: e.target.value }) 17 | if (this.props.onChange) { 18 | this.props.onChange() 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |
26 | 32 | 35 | 36 |
37 | ) 38 | } 39 | } 40 | 41 | export default SendMessageForm 42 | -------------------------------------------------------------------------------- /src/UsernameForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { TextInput } from 'react-desktop/macOs' 3 | import { Button } from 'react-desktop/macOs' 4 | 5 | class UsernameForm extends Component { 6 | constructor() { 7 | super() 8 | this.state = { 9 | username: '' 10 | } 11 | } 12 | 13 | handleSubmit = e => { 14 | e.preventDefault() 15 | this.props.handleSubmit(this.state.username) 16 | } 17 | 18 | handleChange = e => { 19 | this.setState({ username: e.target.value }) 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |
26 |
27 | 33 |
34 |
35 | 38 |
39 |
40 |
41 | ) 42 | } 43 | } 44 | 45 | export default UsernameForm 46 | -------------------------------------------------------------------------------- /src/electron-react.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | const port = process.env.PORT ? process.env.PORT - 100 : 3000 3 | 4 | process.env.ELECTRON_START_URL = `http://localhost:${port}` 5 | 6 | const client = new net.Socket() 7 | 8 | let startedElectron = false 9 | const tryConnection = () => 10 | client.connect({ port: port }, () => { 11 | client.end() 12 | if (!startedElectron) { 13 | console.log('starting electron') 14 | startedElectron = true 15 | const exec = require('child_process').exec 16 | exec('npm run electron') 17 | } 18 | }) 19 | 20 | tryConnection() 21 | 22 | client.on('error', error => { 23 | setTimeout(tryConnection, 1000) 24 | }) 25 | -------------------------------------------------------------------------------- /src/electron-starter.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const app = electron.app 3 | const BrowserWindow = electron.BrowserWindow 4 | 5 | const path = require('path') 6 | const url = require('url') 7 | 8 | let mainWindow 9 | 10 | function createWindow() { 11 | mainWindow = new BrowserWindow({ width: 800, height: 600 }) 12 | 13 | const startUrl = 14 | process.env.ELECTRON_START_URL || 15 | url.format({ 16 | pathname: path.join(__dirname, '/../build/index.html'), 17 | protocol: 'file:', 18 | slashes: true 19 | }) 20 | mainWindow.loadURL(startUrl) 21 | 22 | mainWindow.on('closed', function() { 23 | mainWindow = null 24 | }) 25 | } 26 | 27 | app.on('ready', createWindow) 28 | 29 | app.on('window-all-closed', function() { 30 | if (process.platform !== 'darwin') { 31 | app.quit() 32 | } 33 | }) 34 | 35 | app.on('activate', function() { 36 | if (mainWindow === null) { 37 | createWindow() 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: BlinkMacSystemFont, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .username-form { 8 | padding: 10px; 9 | } 10 | 11 | .username-form button { 12 | margin-top: 10px; 13 | } 14 | 15 | .wrapper { 16 | height: 100vh; 17 | display: flex; 18 | 19 | } 20 | 21 | .online-list { 22 | /* box-shadow: 0 -8px 20px 2px #DEDEE3; */ 23 | background: #F9F9FB; 24 | } 25 | 26 | .chat { 27 | /* background: #F4FAFE; */ 28 | display: flex; 29 | flex-direction: column; 30 | flex: 1; 31 | } 32 | 33 | 34 | .send-message-form{ 35 | padding: 10px 15px 10px 15px; 36 | display: flex; 37 | } 38 | 39 | .send-message-form div { 40 | flex: 1; 41 | margin-right: 5px; 42 | } 43 | 44 | 45 | .online-list-item { 46 | width: 10px; 47 | height: 10px; 48 | border-radius: 50%; 49 | margin-right: 5px; 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.render(, document.getElementById('app')) 7 | --------------------------------------------------------------------------------