├── .firebaserc ├── public ├── favicon.ico ├── manifest.json └── index.html ├── database.rules.json ├── storage.rules ├── firebase.json ├── src ├── components │ ├── UI │ │ ├── Spinner.js │ │ └── ProgressBar.js │ ├── Messages │ │ ├── Typing.js │ │ ├── Skeleton.js │ │ ├── Message.js │ │ ├── MessagesHeader.js │ │ ├── FileModal.js │ │ ├── MessagesForm.js │ │ └── Messages.js │ ├── SidePanel │ │ ├── SidePanel.js │ │ ├── Starred.js │ │ ├── DirectMessages.js │ │ ├── UserPanel.js │ │ └── Channels.js │ ├── App.js │ ├── MetaPanel │ │ └── MetaPanel.js │ ├── Auth │ │ ├── Login.js │ │ └── Register.js │ ├── App.css │ └── ColorPanel │ │ └── ColorPanel.js ├── App.test.js ├── store │ ├── actions │ │ ├── actionTypes.js │ │ └── index.js │ ├── store.js │ └── reducers │ │ ├── colorsReducer.js │ │ ├── userReducer.js │ │ └── channelReducer.js ├── firebaseSetup.js ├── index.js ├── logo.svg └── serviceWorker.js ├── package.json ├── .gitignore ├── README.md └── .firebase └── hosting.YnVpbGQ.cache /.firebaserc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatheoDodi/chit-chat/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": "auth != null" 5 | } 6 | } -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "./build" 4 | }, 5 | "database": { 6 | "rules": "database.rules.json" 7 | }, 8 | "storage": { 9 | "rules": "storage.rules" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/UI/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loader, Dimmer } from 'semantic-ui-react'; 3 | 4 | const Spinner = () => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default Spinner; 11 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Messages/Typing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Typing = () => ( 4 |
5 |
6 |
7 |
8 |
9 | ); 10 | 11 | export default Typing; 12 | -------------------------------------------------------------------------------- /src/components/Messages/Skeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Skeleton = () => ( 4 |
5 |
6 |
7 |
8 |
9 | ); 10 | 11 | export default Skeleton; 12 | -------------------------------------------------------------------------------- /src/store/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const SET_USER = 'SET_USER'; 2 | export const CLEAR_USER = 'CLEAR_USER'; 3 | export const SET_CURRENT_CHANNEL = 'SET_CURRENT_CHANNEL'; 4 | export const SET_PRIVATE_CHANNEL = 'SET_PRIVATE_CHANNEL'; 5 | export const SET_USER_POSTS = 'SET_USER_POSTS'; 6 | export const SET_COLORS = 'SET_COLORS'; 7 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { userReducer } from './reducers/userReducer'; 3 | import { channelReducer } from './reducers/channelReducer'; 4 | import { colorsReducer } from './reducers/colorsReducer'; 5 | 6 | const rootReducer = combineReducers({ 7 | user: userReducer, 8 | channel: channelReducer, 9 | colors: colorsReducer 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /src/components/UI/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Progress } from 'semantic-ui-react'; 3 | 4 | const ProgressBar = ({ uploadState, percentUploaded }) => 5 | uploadState === 'uploading' && ( 6 | 14 | ); 15 | 16 | export default ProgressBar; 17 | -------------------------------------------------------------------------------- /src/firebaseSetup.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import 'firebase/database'; 4 | import 'firebase/storage'; 5 | 6 | import { API_KEY } from './util/API'; 7 | 8 | var config = { 9 | apiKey: API_KEY, 10 | authDomain: 'chit-chat-ee570.firebaseapp.com', 11 | databaseURL: 'https://chit-chat-ee570.firebaseio.com', 12 | projectId: 'chit-chat-ee570', 13 | storageBucket: 'chit-chat-ee570.appspot.com', 14 | messagingSenderId: '660885860948' 15 | }; 16 | firebase.initializeApp(config); 17 | 18 | export default firebase; 19 | -------------------------------------------------------------------------------- /src/store/reducers/colorsReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | primaryColor: '#3F0F3F', 5 | secondaryColor: '#eee' 6 | }; 7 | 8 | export const colorsReducer = (state = initialState, action) => { 9 | console.log('reducer'); 10 | switch (action.type) { 11 | case actionTypes.SET_COLORS: 12 | return { 13 | ...state, 14 | primaryColor: action.payload.primaryColor, 15 | secondaryColor: action.payload.secondaryColor 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/store/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | currentUser: null, 5 | isLoading: true 6 | }; 7 | 8 | export const userReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case actionTypes.SET_USER: 11 | return { 12 | ...state, 13 | isLoading: false, 14 | currentUser: action.payload.currentUser 15 | }; 16 | case actionTypes.CLEAR_USER: 17 | return { 18 | ...state, 19 | currentUser: null, 20 | isLoading: false 21 | }; 22 | 23 | default: 24 | return state; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/store/reducers/channelReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | currentChannel: null, 5 | isPrivateChannel: false, 6 | userPosts: null 7 | }; 8 | 9 | export const channelReducer = (state = initialState, action) => { 10 | switch (action.type) { 11 | case actionTypes.SET_CURRENT_CHANNEL: 12 | return { 13 | ...state, 14 | currentChannel: action.payload.channel 15 | }; 16 | case actionTypes.SET_PRIVATE_CHANNEL: 17 | return { 18 | ...state, 19 | isPrivateChannel: action.payload.isPrivateChannel 20 | }; 21 | case actionTypes.SET_USER_POSTS: 22 | return { 23 | ...state, 24 | userPosts: action.payload.userPosts 25 | }; 26 | default: 27 | return state; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes'; 2 | 3 | export const setUser = user => ({ 4 | type: actionTypes.SET_USER, 5 | payload: { 6 | currentUser: user 7 | } 8 | }); 9 | 10 | export const clearUser = () => ({ 11 | type: actionTypes.CLEAR_USER 12 | }); 13 | 14 | export const setCurrentChannel = channel => ({ 15 | type: actionTypes.SET_CURRENT_CHANNEL, 16 | payload: { 17 | channel 18 | } 19 | }); 20 | 21 | export const setPrivateChannel = isPrivateChannel => ({ 22 | type: actionTypes.SET_PRIVATE_CHANNEL, 23 | payload: { 24 | isPrivateChannel 25 | } 26 | }); 27 | 28 | export const setUserPosts = userPosts => ({ 29 | type: actionTypes.SET_USER_POSTS, 30 | payload: { 31 | userPosts 32 | } 33 | }); 34 | 35 | export const setColors = (primaryColor, secondaryColor) => ({ 36 | type: actionTypes.SET_COLORS, 37 | payload: { 38 | primaryColor, 39 | secondaryColor 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import UserPanel from './UserPanel'; 4 | import DirectMessages from './DirectMessages'; 5 | import Channels from './Channels'; 6 | import Starred from './Starred'; 7 | 8 | class SidePanel extends Component { 9 | render() { 10 | const { currentUser, primaryColor } = this.props; 11 | return ( 12 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default SidePanel; 33 | -------------------------------------------------------------------------------- /src/components/Messages/Message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { Comment, Image } from 'semantic-ui-react'; 4 | 5 | const isOwnMessage = (message, user) => { 6 | return message.user.id === user.uid ? 'message__self' : ''; 7 | }; 8 | 9 | const timeFromNow = timestamp => moment(timestamp).fromNow(); 10 | 11 | const isImage = message => { 12 | return message.hasOwnProperty('image') && !message.hasOwnProperty('content'); 13 | }; 14 | 15 | const Message = ({ message, user }) => ( 16 | 17 | 18 | 19 | {message.user.name} 20 | {timeFromNow(message.timestamp)} 21 | 22 | {isImage(message) ? ( 23 | 24 | ) : ( 25 | {message.content} 26 | )} 27 | 28 | 29 | ); 30 | 31 | export default Message; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chit-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "emoji-chart": "^1.3.0", 7 | "emoji-mart": "^2.9.2", 8 | "firebase": "^5.8.3", 9 | "md5": "^2.2.1", 10 | "mime-types": "^2.1.22", 11 | "moment": "^2.24.0", 12 | "react": "^16.8.2", 13 | "react-avatar-editor": "^11.0.6", 14 | "react-color": "^2.17.0", 15 | "react-dom": "^16.8.2", 16 | "react-redux": "^6.0.1", 17 | "react-router": "^4.3.1", 18 | "react-router-dom": "^4.3.1", 19 | "react-scripts": "2.1.5", 20 | "redux": "^4.0.1", 21 | "redux-devtools-extension": "^2.13.8", 22 | "semantic-ui-css": "^2.4.1", 23 | "semantic-ui-react": "^0.85.0", 24 | "uuid": "^3.3.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Messages/MessagesHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Header, Segment, Input, Icon } from 'semantic-ui-react'; 3 | 4 | class MessagesHeader extends Component { 5 | render() { 6 | const { 7 | channelName, 8 | numUniqueUsers, 9 | handleSearchChange, 10 | searchLoading, 11 | isPrivateChannel, 12 | handleStar, 13 | isChannelStarred 14 | } = this.props; 15 | return ( 16 | 17 |
18 | 19 | {channelName} 20 | {!isPrivateChannel && ( 21 | 26 | )} 27 | 28 | {numUniqueUsers} 29 |
30 |
31 | 39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | export default MessagesHeader; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 63 | 64 | # dependencies 65 | /node_modules 66 | /.pnp 67 | .pnp.js 68 | 69 | # testing 70 | /coverage 71 | 72 | # production 73 | /build 74 | 75 | # misc 76 | .DS_Store 77 | .env.local 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | 82 | npm-debug.log* 83 | yarn-debug.log* 84 | yarn-error.log* 85 | 86 | src/util/API.js -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | chitChat 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/Messages/FileModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import mime from 'mime-types'; 3 | import { Modal, Button, Input, Icon } from 'semantic-ui-react'; 4 | 5 | class FileModal extends Component { 6 | state = { 7 | file: null, 8 | authorized: ['image/png', 'image/jpeg'] 9 | }; 10 | 11 | addFile = event => { 12 | const file = event.target.files[0]; 13 | if (file) { 14 | this.setState({ file }); 15 | } 16 | }; 17 | 18 | sendFile = () => { 19 | const { file } = this.state; 20 | 21 | if (file) { 22 | if (this.isAuthorized(file.name)) { 23 | const metadata = { contentType: mime.lookup(file.name) }; 24 | this.props.uploadFile(file, metadata); 25 | this.props.closeModal(); 26 | this.setState({ file: null }); 27 | } 28 | } 29 | }; 30 | 31 | isAuthorized = fileName => 32 | this.state.authorized.includes(mime.lookup(fileName)); 33 | 34 | render() { 35 | const { modal, closeModal } = this.props; 36 | 37 | return ( 38 | 39 | Select an Image File 40 | 41 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default FileModal; 63 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { Grid } from 'semantic-ui-react'; 4 | import ColorPanel from './ColorPanel/ColorPanel'; 5 | import SidePanel from './SidePanel/SidePanel'; 6 | import Messages from './Messages/Messages'; 7 | import MetaPanel from './MetaPanel/MetaPanel'; 8 | import { connect } from 'react-redux'; 9 | 10 | const App = ({ 11 | currentUser, 12 | currentChannel, 13 | isPrivateChannel, 14 | userPosts, 15 | primaryColor, 16 | secondaryColor 17 | }) => ( 18 | 27 | 31 | {currentUser ? ( 32 | 37 | ) : ( 38 | 'Loading' 39 | )} 40 | 41 | 47 | 48 | 49 | 55 | 56 | 57 | ); 58 | 59 | const MapStateToProps = state => ({ 60 | currentUser: state.user.currentUser, 61 | currentChannel: state.channel.currentChannel, 62 | isPrivateChannel: state.channel.isPrivateChannel, 63 | userPosts: state.channel.userPosts, 64 | primaryColor: state.colors.primaryColor, 65 | secondaryColor: state.colors.secondaryColor 66 | }); 67 | 68 | export default connect(MapStateToProps)(App); 69 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Provider, connect } from 'react-redux'; 4 | import { setUser, clearUser } from './store/actions'; 5 | import { composeWithDevTools } from 'redux-devtools-extension'; 6 | import ReactDOM from 'react-dom'; 7 | import { 8 | BrowserRouter as Router, 9 | Switch, 10 | Route, 11 | withRouter 12 | } from 'react-router-dom'; 13 | import rootReducer from './store/store'; 14 | import 'semantic-ui-css/semantic.min.css'; 15 | import firebase from './firebaseSetup'; 16 | import App from './components/App'; 17 | import Login from './components/Auth/Login'; 18 | import Register from './components/Auth/Register'; 19 | import Spinner from './components/UI/Spinner'; 20 | import * as serviceWorker from './serviceWorker'; 21 | 22 | const store = createStore(rootReducer, composeWithDevTools()); 23 | 24 | class Root extends Component { 25 | componentDidMount() { 26 | firebase.auth().onAuthStateChanged(user => { 27 | if (user) { 28 | this.props.setUser(user); 29 | this.props.history.push('/'); 30 | } else { 31 | this.props.history.push('/login'); 32 | this.props.clearUser(); 33 | } 34 | }); 35 | } 36 | 37 | render() { 38 | return this.props.isLoading ? ( 39 | 40 | ) : ( 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | const mapStateToProps = state => ({ 51 | isLoading: state.user.isLoading 52 | }); 53 | 54 | const RootWithAuth = withRouter( 55 | connect( 56 | mapStateToProps, 57 | { setUser, clearUser } 58 | )(Root) 59 | ); 60 | 61 | ReactDOM.render( 62 | 63 | 64 | 65 | 66 | , 67 | document.getElementById('root') 68 | ); 69 | 70 | // If you want your app to work offline and load faster, you can change 71 | // unregister() to register() below. Note this comes with some pitfalls. 72 | // Learn more about service workers: http://bit.ly/CRA-PWA 73 | serviceWorker.unregister(); 74 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/SidePanel/Starred.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import firebase from '../../firebaseSetup'; 4 | import { setCurrentChannel, setPrivateChannel } from '../../store/actions'; 5 | import { Menu, Icon } from 'semantic-ui-react'; 6 | 7 | class Starred extends Component { 8 | state = { 9 | activeChannel: '', 10 | starredChannels: [], 11 | usersRef: firebase.database().ref('users') 12 | }; 13 | 14 | componentDidMount() { 15 | if (this.props.currentUser) { 16 | this.addListeners(this.props.currentUser.uid); 17 | } 18 | } 19 | 20 | componentWillUnmount() { 21 | this.removeListener(); 22 | } 23 | 24 | removeListener = () => { 25 | this.state.usersRef.child(`${this.props.currentUser.uid}/starred`).off(); 26 | }; 27 | 28 | addListeners = userId => { 29 | this.state.usersRef 30 | .child(userId) 31 | .child('starred') 32 | .on('child_added', snap => { 33 | const starredChannel = { id: snap.key, ...snap.val() }; 34 | this.setState({ 35 | starredChannels: [...this.state.starredChannels, starredChannel] 36 | }); 37 | }); 38 | 39 | this.state.usersRef 40 | .child(userId) 41 | .child('starred') 42 | .on('child_removed', snap => { 43 | const channelToRemove = { id: snap.key, ...snap.val() }; 44 | const filteredChannels = this.state.starredChannels.filter(channel => { 45 | return channel.id !== channelToRemove.id; 46 | }); 47 | this.setState({ starredChannels: filteredChannels }); 48 | }); 49 | }; 50 | 51 | setActiveChannel = channel => { 52 | this.setState({ activeChannel: channel.id }); 53 | }; 54 | 55 | changeChannel = channel => { 56 | this.setActiveChannel(channel); 57 | this.props.setCurrentChannel(channel); 58 | this.props.setPrivateChannel(false); 59 | }; 60 | 61 | displayChannels = starredChannels => 62 | starredChannels.length > 0 && 63 | starredChannels.map(channel => ( 64 | this.changeChannel(channel)} 67 | name={channel.name} 68 | style={{ opacity: 0.7 }} 69 | active={channel.id === this.state.activeChannel} 70 | > 71 | # {channel.name} 72 | 73 | )); 74 | 75 | render() { 76 | const { starredChannels } = this.state; 77 | 78 | return ( 79 | 80 | 81 | 82 | STARRED 83 | {' '} 84 | ({starredChannels.length}) 85 | 86 | {this.displayChannels(starredChannels)} 87 | 88 | ); 89 | } 90 | } 91 | 92 | export default connect( 93 | null, 94 | { setCurrentChannel, setPrivateChannel } 95 | )(Starred); 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![LinkedIn][linkedin-shield]][linkedin-url] 2 | 3 | 4 |
5 |

6 | React Logo 7 | Redux Logo 8 | Firebase Logo 9 | 10 |

chitChat

11 |

12 | 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [About the Repo](#about-the-project) 18 | - [Built With](#built-with) 19 | - [Getting Started](#getting-started) 20 | - [Installation](#installation) 21 | - [Contributing](#contributing) 22 | - [What Did I Learn](#what-did-i-learn) 23 | - [Contact](#contact) 24 | 25 | 26 | 27 | ## About The Project 28 | 29 | Building a Chat web application which looks a lot like slack. Like, a lot. Okay, well, yeah...it's a clone. 30 | 31 | ### Built With 32 | 33 | - [React](https://reactjs.org/) 34 | - [Router](https://reacttraining.com/react-router/) 35 | - [Redux](https://redux.js.org/) 36 | - [Firebase](https://firebase.google.com/docs/) 37 | - [Semantic-UI-React](https://react.semantic-ui.com/) 38 | 39 | 40 | 41 | ## Getting Started 42 | 43 | To get a local copy up and running follow these simple steps. 44 | 45 | ### Installation 46 | 47 | 1. Clone the repo 48 | 49 | ```sh 50 | git clone https://github.com/MatthewDodi/chit-chat.git 51 | ``` 52 | 53 | 2. Install NPM packages 54 | 55 | ```sh 56 | npm install 57 | ``` 58 | 59 | 3. Run the Development Server 60 | 61 | ```sh 62 | npm start 63 | ``` 64 | 65 | 66 | 67 | ## Contributing 68 | 69 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 70 | 71 | 1. Fork the Project 72 | 2. Create your Feature Branch (`git checkout -b feature/AwesomeFeature`) 73 | 3. Commit your Changes (`git commit -m 'Add some AwesomeFeature`) 74 | 4. Push to the Branch (`git push origin feature/AwesomeFeature`) 75 | 5. Open a Pull Request 76 | 77 | ## What Did I Learn 78 | 79 | - Semantic UI React component library 80 | - Firebase Functions 81 | - Destructuring mapStateToProps properties inside the react-redux 'connect' method intead of setting up an external variable 82 | 83 | 84 | 85 | ## Contact 86 | 87 | Matthew Dodi - [in/MatthewDodi](https://linkedin.com/in/MatthewDodi) - matthew.dodi@gmail.com 88 | 89 | Project Link: [https://chit-chat-ee570.firebaseapp.com/](https://chit-chat-ee570.firebaseapp.com/) 90 | 91 | 92 | 93 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555 94 | [linkedin-url]: https://linkedin.com/in/MatthewDodi 95 | -------------------------------------------------------------------------------- /src/components/MetaPanel/MetaPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Segment, 4 | Header, 5 | Accordion, 6 | Icon, 7 | Image, 8 | List 9 | } from 'semantic-ui-react'; 10 | 11 | class MetaPanel extends Component { 12 | state = { 13 | currentChannel: this.props.currentChannel, 14 | isPrivateChannel: this.props.isPrivateChannel, 15 | activeIndex: 0 16 | }; 17 | 18 | setActiveIndex = (event, titleProps) => { 19 | const { index } = titleProps; 20 | const { activeIndex } = this.state; 21 | const newIndex = activeIndex === index ? -1 : index; 22 | this.setState({ activeIndex: newIndex }); 23 | }; 24 | 25 | formatCount = postsNum => (postsNum > 1 ? 'posts' : 'post'); 26 | 27 | displayTopPosters = userPosts => { 28 | return Object.entries(userPosts) 29 | .sort((a, b) => b[1].count - a[1].count) 30 | .map(([key, val], i) => { 31 | return ( 32 | 33 | 34 | 35 | {key} 36 | 37 | {val.count} {this.formatCount(val.count)} 38 | 39 | 40 | 41 | ); 42 | }) 43 | .slice(0, 3); 44 | }; 45 | 46 | render() { 47 | const { activeIndex, currentChannel, isPrivateChannel } = this.state; 48 | const { userPosts } = this.props; 49 | 50 | if (isPrivateChannel) return null; 51 | 52 | return ( 53 | 54 |
55 | About # {currentChannel && currentChannel.name} 56 |
57 | 58 | 63 | 64 | 65 | Channel Details 66 | 67 | 68 | {currentChannel && currentChannel.details} 69 | 70 | 75 | 76 | 77 | Top Posters 78 | 79 | 80 | {userPosts && this.displayTopPosters(userPosts)} 81 | 82 | 87 | 88 | 89 | Created By 90 | 91 | 92 |
93 | 97 | {currentChannel && currentChannel.createdBy.name} 98 |
99 |
100 |
101 |
102 | ); 103 | } 104 | } 105 | 106 | export default MetaPanel; 107 | -------------------------------------------------------------------------------- /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1551422349349,60382616c19e015b6f592466c3ba94fb16fbf9063c95150013b45331bea85f43 2 | index.html,1551422349349,375d7b511f7e7099c139dc1bac6289bfaad65de59047b073bc752e9160c6c93b 3 | favicon.ico,499162500000,eae62e993eb980ec8a25058c39d5a51feab118bd2100c4deebb2a9c158ec11f9 4 | manifest.json,499162500000,a40a4294484385ec155814f7d72caf5967a19f5efcbedf7a62b2cdff07e42711 5 | precache-manifest.762123211ead629462fa840ed7f45df2.js,1551422349349,456e3f6b7e10ed6b8bf3bad65001c13e43d8de0978c5a227a0b8b9cbd8664a00 6 | service-worker.js,1551422349349,8d60ea29df1cd92b973fb8ef0fb853f679723418d372c62fcf6abaeb7feaae22 7 | static/css/main.4c5ac5a4.chunk.css,1551422349380,74c11d4937167f14c5d653f78f3af7b5810fd9a814e374cac9897f9e2d571d28 8 | static/css/main.4c5ac5a4.chunk.css.map,1551422349381,8359dc3803b97a364bfe76f0025b00f7f1d9ac024b22ff124caeaa46d8b74de1 9 | static/js/main.9602d5b9.chunk.js,1551422349380,82dad96219ed8a4fa3ef887ddbee303b08215cf295761ae7ed034b43345bf6fa 10 | static/js/runtime~main.fdfcfda2.js,1551422349381,72e3757cb82c478f9c9665c7799e28ff1bfa7ca4cd9e6bd13cf75949929cb09b 11 | static/js/runtime~main.fdfcfda2.js.map,1551422349382,de6d0e5ab860fce49e55482af60c5811b913a08226f87573a3c348b5642425d8 12 | static/media/outline-icons.701ae6ab.eot,1551422349380,145a45b76068d4888605c06dfa228697fd81dc70421dd8ab69788e76b33b139a 13 | static/media/outline-icons.cd6c777f.woff2,1551422349352,2d89a0faf29706d4b6638ed18f599eef886b74bc0220f1515da12bc156ed7282 14 | static/media/outline-icons.ef60a4f6.woff,1551422349381,ab55aa410cd37e4558b5f08ed994df507363e040bcebaa508f701a7423b0e001 15 | static/media/outline-icons.ad97afd3.ttf,1551422349381,32e73c77d4d2493e85eba07b5d78f5d9dce40c3963f058a85d64889e298a2006 16 | static/media/flags.9c74e172.png,1551422349380,cac524e90c64beae88adf680a1f95222e8c6b06f58287e154f07b15cef6e86e9 17 | static/media/icons.0ab54153.woff2,1551422349381,6160eb4d1b1a5d3b82b3d72eb36e6452386bf3b9b793c104c8e002d9aca791c9 18 | static/js/main.9602d5b9.chunk.js.map,1551422349382,dc41a3a66d6a8bd83ca624cb9e00a1245109459eef62aafe019a648f7e3895a7 19 | static/media/brand-icons.a046592b.woff,1551422349381,bc54b585b11b205c8ed78d35af421d3125806b197e33a48a311851e6d3375c7e 20 | static/media/brand-icons.e8c322de.woff2,1551422349381,2ebbe7de3783c3949604a06fb9beb1d27c9d7cf7e0aaadc0d9f72e57247b33b9 21 | static/media/icons.faff9214.woff,1551422349381,cf858e669dd53b1392bedff9f8bb6d56622872a2ca2a1dc87bc0a3bd03458e5e 22 | static/media/outline-icons.82f60bd0.svg,1551422349381,bc4b3fad701e9901f04a655687ac130024a87230db6c5dac6809fd8416bb2cba 23 | static/media/brand-icons.13db00b7.eot,1551422349381,48d842cdbab9d537d4d90d6e1cd9271f8a2abb420d2456d03443b6a62ab0c39c 24 | static/media/brand-icons.c5ebe0b3.ttf,1551422349381,a0d070cd406c91f7683eaf22448700e0b3cd64bb2d25c3150ae20d6262a92968 25 | static/media/icons.8e3c7f55.eot,1551422349381,c0054eb39461c278e6d5cc139366fe4a3719c16399a2f8093ecb1a8e7ddb33e9 26 | static/media/icons.b87b9ba5.ttf,1551422349381,28e24b8ace4febd9f94823317035c32dc567c64ff8ac7e746af45e1c21dc3eff 27 | static/media/icons.962a1bf3.svg,1551422349381,639b544f1f822b2b8dd3a2c6ec6c4bab8831e18321fc17195597e303b806d146 28 | static/css/2.21e08f2c.chunk.css,1551422349382,fea3e8cd7ba8e66860203561752543e3919166573b9359699893718b0925cf7d 29 | static/media/brand-icons.a1a749e8.svg,1551422349381,cb9ac3cd861d77606ea1e569ec141086c60fe1b05720496f2c7f0eae28465876 30 | static/css/2.21e08f2c.chunk.css.map,1551422349382,f1453c32bbd6273d8f24e358da0b492e2006b30b91633b51282ce814f4bf1005 31 | static/js/2.0008bedd.chunk.js,1551422349382,8011678ae6206f4b57fb85a63a174ef196f06e35a7a5781590c91c2f1d423132 32 | static/js/2.0008bedd.chunk.js.map,1551422349383,7eebd41454f827f7632665605e198c7ceb027c3333b89ad4e73969927ebd9ece 33 | -------------------------------------------------------------------------------- /src/components/Auth/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import firebase from '../../firebaseSetup'; 3 | 4 | import { 5 | Grid, 6 | Form, 7 | Segment, 8 | Button, 9 | Header, 10 | Message, 11 | Icon 12 | } from 'semantic-ui-react'; 13 | import { Link } from 'react-router-dom'; 14 | 15 | class Login extends React.Component { 16 | state = { 17 | email: '', 18 | password: '', 19 | errors: [], 20 | loading: false 21 | }; 22 | 23 | displayErrors = errors => 24 | errors.map((error, i) =>

{error.message}

); 25 | 26 | handleInputError = (errors, inputName) => { 27 | return errors.some(error => error.message.toLowerCase().includes(inputName)) 28 | ? 'error' 29 | : ''; 30 | }; 31 | 32 | handleChange = event => { 33 | this.setState({ [event.target.name]: event.target.value }); 34 | }; 35 | 36 | handleSubmit = event => { 37 | event.preventDefault(); 38 | if (this.isFormValid(this.state)) { 39 | this.setState({ errors: [], loading: true }); 40 | firebase 41 | .auth() 42 | .signInWithEmailAndPassword(this.state.email, this.state.password) 43 | .then(signedInUser => {}) 44 | .catch(err => { 45 | console.error(err); 46 | this.setState({ 47 | errors: this.state.errors.concat(err), 48 | loading: false 49 | }); 50 | }); 51 | } 52 | }; 53 | 54 | isFormValid = ({ email, password }) => email && password; 55 | 56 | render() { 57 | const { email, password, errors, loading } = this.state; 58 | return ( 59 |
60 | 61 | 62 |
63 | 64 | Login to chitChat - Hello Everyone 65 |
66 |
67 | 68 | 79 | 90 | 91 | 100 | 101 |
102 | {errors.length > 0 && ( 103 | 104 |

Error

105 | {this.displayErrors(errors)} 106 |
107 | )} 108 | 109 | Don't have an account? Register 110 | 111 |
112 |
113 |
114 | ); 115 | } 116 | } 117 | 118 | export default Login; 119 | -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100vh; 5 | background: #eee; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | height: 100vh; 11 | } 12 | 13 | #root { 14 | height: 100vh; 15 | } 16 | 17 | #messages-container { 18 | height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .auth-container__center { 24 | height: 90%; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .messages-container { 30 | margin-left: 350px; 31 | } 32 | 33 | #messages-segment { 34 | height: 100%; 35 | width: 100%; 36 | overflow-y: scroll; 37 | } 38 | 39 | #messages-group { 40 | max-width: 100%; 41 | padding: 1em; 42 | height: 70vh; 43 | overflow-y: scroll; 44 | padding-right: 17px; 45 | box-sizing: border-box; 46 | } 47 | 48 | .message__form { 49 | bottom: 1em; 50 | margin-left: 320px; 51 | z-index: 200; 52 | } 53 | 54 | .message__self { 55 | border-left: 2px solid orange; 56 | padding-left: 8px; 57 | } 58 | 59 | .message__image { 60 | padding: 1em; 61 | } 62 | 63 | .scroll { 64 | position: absolute; 65 | bottom: 0; 66 | } 67 | 68 | .icon__add-channel:hover { 69 | color: white; 70 | cursor: pointer; 71 | } 72 | 73 | .menu { 74 | padding-bottom: 2em; 75 | padding-top: 1em; 76 | } 77 | 78 | .color__container { 79 | position: relative; 80 | overflow: hidden; 81 | width: 35px; 82 | border-radius: 3px; 83 | } 84 | 85 | .color__container:hover { 86 | cursor: pointer; 87 | } 88 | 89 | .color__square { 90 | width: 35px; 91 | height: 35px; 92 | } 93 | 94 | .color__overlay { 95 | width: 85px; 96 | height: 35px; 97 | transform: rotate(225deg); 98 | } 99 | 100 | .emoji-mart { 101 | position: absolute; 102 | bottom: 100px; 103 | } 104 | 105 | .user__typing { 106 | font-style: italic; 107 | font-weight: bold; 108 | margin-right: 3px; 109 | } 110 | 111 | .typing { 112 | width: 5em; 113 | height: 2em; 114 | position: relative; 115 | padding: 10px; 116 | margin-left: 5px; 117 | background: #e6e6e6; 118 | border-radius: 20px; 119 | } 120 | 121 | .typing__dot { 122 | float: left; 123 | width: 8px; 124 | height: 8px; 125 | margin: 0 4px; 126 | background: #8d8c91; 127 | border-radius: 50%; 128 | opacity: 0; 129 | animation: loadingFade 1s infinite; 130 | } 131 | 132 | .typing__dot:nth-child(1) { 133 | animation-delay: 0s; 134 | } 135 | 136 | .typing__dot:nth-child(2) { 137 | animation-delay: 0.2s; 138 | } 139 | 140 | .typing__dot:nth-child(3) { 141 | animation-delay: 0.4s; 142 | } 143 | 144 | .skeleton { 145 | position: relative; 146 | overflow: hidden; 147 | height: 40px; 148 | margin-bottom: 10px; 149 | } 150 | 151 | .skeleton::after { 152 | content: ''; 153 | display: block; 154 | position: absolute; 155 | top: 0; 156 | left: 0; 157 | width: 50%; 158 | height: 100%; 159 | animation: sweep 1s infinite; 160 | background-image: linear-gradient( 161 | to left, 162 | transparent, 163 | rgba(255, 255, 255, 0.4), 164 | transparent 165 | ); 166 | } 167 | 168 | .skeleton__avatar { 169 | height: 35px; 170 | width: 35px; 171 | border-radius: 3px; 172 | background-color: rgba(58, 57, 57, 0.3); 173 | } 174 | 175 | .skeleton__author { 176 | background-color: rgba(58, 57, 57, 0.3); 177 | width: 120px; 178 | height: 10px; 179 | border-radius: 3px; 180 | position: absolute; 181 | bottom: 30px; 182 | left: 40px; 183 | right: 0; 184 | } 185 | 186 | .skeleton__details { 187 | background-color: rgba(58, 57, 57, 0.3); 188 | height: 20px; 189 | border-radius: 3px; 190 | position: absolute; 191 | bottom: 5px; 192 | left: 40px; 193 | right: 20px; 194 | } 195 | 196 | @keyframes sweep { 197 | 0% { 198 | transform: translateX(-100%); 199 | } 200 | 201 | 50% { 202 | transform: translateX(150%); 203 | } 204 | 205 | 100% { 206 | transform: translateX(-100%); 207 | } 208 | } 209 | 210 | @keyframes loadingFade { 211 | 0% { 212 | opacity: 0; 213 | } 214 | 215 | 50% { 216 | opacity: 0.8; 217 | } 218 | 219 | 100% { 220 | opacity: 0; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/components/SidePanel/DirectMessages.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { setCurrentChannel, setPrivateChannel } from '../../store/actions'; 4 | import firebase from '../../firebaseSetup'; 5 | import { Menu, Icon } from 'semantic-ui-react'; 6 | 7 | class DirectMessages extends Component { 8 | state = { 9 | activeChannel: '', 10 | users: [], 11 | user: this.props.currentUser, 12 | usersRef: firebase.database().ref('users'), 13 | connectedRef: firebase.database().ref('.info/connected'), 14 | presenceRef: firebase.database().ref('presence') 15 | }; 16 | 17 | componentDidMount() { 18 | if (this.state.user) { 19 | this.addListeners(this.state.user.uid); 20 | } 21 | } 22 | 23 | componentWillUnmount() { 24 | this.removeListeners(); 25 | } 26 | 27 | removeListeners = () => { 28 | this.state.usersRef.off(); 29 | this.state.presenceRef.off(); 30 | this.state.connectedRef.off(); 31 | }; 32 | 33 | addListeners = currentUserUid => { 34 | let loadedUsers = []; 35 | this.state.usersRef.on('child_added', snap => { 36 | if (currentUserUid !== snap.key) { 37 | let user = snap.val(); 38 | user['uid'] = snap.key; 39 | user['status'] = 'offline'; 40 | loadedUsers.push(user); 41 | this.setState({ users: loadedUsers }); 42 | } 43 | }); 44 | 45 | this.state.connectedRef.on('value', snap => { 46 | if (snap.val() === true) { 47 | const ref = this.state.presenceRef.child(currentUserUid); 48 | ref.set(true); 49 | ref.onDisconnect().remove(err => { 50 | if (err !== null) { 51 | console.error(err); 52 | } 53 | }); 54 | } 55 | }); 56 | 57 | this.state.presenceRef.on('child_added', snap => { 58 | if (currentUserUid !== snap.key) { 59 | this.addStatusToUser(snap.key); 60 | } 61 | }); 62 | 63 | this.state.presenceRef.on('child_removed', snap => { 64 | if (currentUserUid !== snap.key) { 65 | this.addStatusToUser(snap.key, false); 66 | } 67 | }); 68 | }; 69 | 70 | addStatusToUser = (userId, connected = true) => { 71 | const updatedUsers = this.state.users.reduce((acc, user) => { 72 | if (user.uid === userId) { 73 | user['status'] = `${connected ? 'online' : 'offline'}`; 74 | } 75 | return acc.concat(user); 76 | }, []); 77 | this.setState({ users: updatedUsers }); 78 | }; 79 | 80 | isUserOnline = user => user.status === 'online'; 81 | 82 | changeChannel = user => { 83 | const channelId = this.getChannelId(user.uid); 84 | const channelData = { 85 | id: channelId, 86 | name: user.name 87 | }; 88 | this.props.setCurrentChannel(channelData); 89 | this.props.setPrivateChannel(true); 90 | this.setActiveChannel(user.uid); 91 | }; 92 | 93 | getChannelId = userId => { 94 | const currentUserId = this.state.user.uid; 95 | return userId < currentUserId 96 | ? `${userId}/${currentUserId}` 97 | : `${currentUserId}/${userId}`; 98 | }; 99 | 100 | setActiveChannel = userId => { 101 | this.setState({ activeChannel: userId }); 102 | }; 103 | 104 | render() { 105 | const { users, activeChannel } = this.state; 106 | 107 | return ( 108 | 109 | 110 | 111 | DIRECT MESSAGES 112 | {' '} 113 | ({users.length}) 114 | 115 | {users.map(user => ( 116 | this.changeChannel(user)} 120 | style={{ opacity: 0.7, fontStyle: 'italic' }} 121 | > 122 | 126 | @ {user.name} 127 | 128 | ))} 129 | 130 | ); 131 | } 132 | } 133 | 134 | export default connect( 135 | null, 136 | { setCurrentChannel, setPrivateChannel } 137 | )(DirectMessages); 138 | -------------------------------------------------------------------------------- /src/components/ColorPanel/ColorPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { setColors } from '../../store/actions'; 4 | import firebase from '../../firebaseSetup'; 5 | import { 6 | Sidebar, 7 | Menu, 8 | Divider, 9 | Button, 10 | Modal, 11 | Label, 12 | Icon, 13 | Segment 14 | } from 'semantic-ui-react'; 15 | import { SwatchesPicker } from 'react-color'; 16 | 17 | class ColorPanel extends Component { 18 | state = { 19 | modal: false, 20 | primary: '9900EF', 21 | secondary: 'FF6900', 22 | usersRef: firebase.database().ref('users'), 23 | userColors: [] 24 | }; 25 | 26 | componentDidMount() { 27 | if (this.props.currentUser) { 28 | this.addListener(this.props.currentUser.uid); 29 | } 30 | } 31 | 32 | componentWillUnmount() { 33 | this.removeListener(); 34 | } 35 | 36 | removeListener = () => { 37 | this.state.usersRef.child(`${this.props.currentUser.uid}/colors`).off(); 38 | }; 39 | 40 | addListener = userId => { 41 | let userColors = []; 42 | this.state.usersRef.child(`${userId}/colors`).on('child_added', snap => { 43 | userColors.unshift(snap.val()); 44 | this.setState({ userColors }); 45 | }); 46 | }; 47 | 48 | openModal = () => this.setState({ modal: true }); 49 | 50 | closeModal = () => this.setState({ modal: false }); 51 | 52 | handleChangePrimary = color => this.setState({ primary: color.hex }); 53 | 54 | handleChangeSecondary = color => this.setState({ secondary: color.hex }); 55 | 56 | handleSaveColors = () => { 57 | if (this.state.primary && this.state.secondary) { 58 | this.saveColors(this.state.primary, this.state.secondary); 59 | } 60 | }; 61 | 62 | saveColors = (primary, secondary) => { 63 | this.state.usersRef 64 | .child(`${this.props.currentUser.uid}/colors`) 65 | .push() 66 | .update({ 67 | primary, 68 | secondary 69 | }) 70 | .then(() => { 71 | this.props.setColors(primary, secondary); 72 | this.closeModal(); 73 | }) 74 | .catch(err => { 75 | console.log(err); 76 | }); 77 | }; 78 | 79 | displayUserColors = colors => { 80 | console.log(colors); 81 | return ( 82 | colors.length > 0 && 83 | colors.map((color, i) => ( 84 | 85 | 86 |
this.props.setColors(color.primary, color.secondary)} 89 | > 90 |
94 |
98 |
99 |
100 | 101 | )) 102 | ); 103 | }; 104 | 105 | render() { 106 | const { modal, primary, secondary, userColors } = this.state; 107 | 108 | return ( 109 | 117 | 118 | 153 | 156 | 157 | 158 | 159 | ); 160 | } 161 | } 162 | 163 | export default connect( 164 | null, 165 | { setColors } 166 | )(ColorPanel); 167 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Auth/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import firebase from '../../firebaseSetup'; 3 | import md5 from 'md5'; 4 | import { 5 | Grid, 6 | Form, 7 | Segment, 8 | Button, 9 | Header, 10 | Message, 11 | Icon 12 | } from 'semantic-ui-react'; 13 | import { Link } from 'react-router-dom'; 14 | 15 | class Register extends React.Component { 16 | state = { 17 | username: '', 18 | email: '', 19 | password: '', 20 | passwordConfirmation: '', 21 | errors: [], 22 | loading: false, 23 | usersRef: firebase.database().ref('users') 24 | }; 25 | 26 | isFormValid = () => { 27 | let errors = []; 28 | let error; 29 | 30 | if (this.isFormEmpty(this.state)) { 31 | //throw error 32 | error = { message: 'Fill in all fields' }; 33 | this.setState({ errors: errors.concat(error) }); 34 | return false; 35 | } else if (!this.isPasswordValid(this.state)) { 36 | // throw error 37 | error = { message: 'Password is invalid' }; 38 | this.setState({ errors: errors.concat(error) }); 39 | return false; 40 | } else { 41 | // form valid 42 | return true; 43 | } 44 | }; 45 | 46 | isFormEmpty = ({ username, email, password, passwordConfirmation }) => { 47 | return ( 48 | !username.length || 49 | !email.length || 50 | !password.length || 51 | !passwordConfirmation.length 52 | ); 53 | }; 54 | 55 | isPasswordValid = ({ password, passwordConfirmation }) => { 56 | if (password.length < 6 || passwordConfirmation < 6) { 57 | return false; 58 | } else if (password !== passwordConfirmation) { 59 | return false; 60 | } else { 61 | return true; 62 | } 63 | }; 64 | 65 | displayErrors = errors => 66 | errors.map((error, i) =>

{error.message}

); 67 | 68 | handleInputError = (errors, inputName) => { 69 | return errors.some(error => error.message.toLowerCase().includes(inputName)) 70 | ? 'error' 71 | : ''; 72 | }; 73 | 74 | handleChange = event => { 75 | this.setState({ [event.target.name]: event.target.value }); 76 | }; 77 | 78 | handleSubmit = event => { 79 | event.preventDefault(); 80 | if (this.isFormValid()) { 81 | this.setState({ errors: [], loading: true }); 82 | firebase 83 | .auth() 84 | .createUserWithEmailAndPassword(this.state.email, this.state.password) 85 | .then(createdUser => { 86 | console.log(createdUser); 87 | createdUser.user 88 | .updateProfile({ 89 | displayName: this.state.username, 90 | photoURL: `http://gravatar.com/avatar/${md5( 91 | createdUser.user.email 92 | )}?d=identicon` 93 | }) 94 | .then(() => { 95 | this.saveUser(createdUser).then(() => { 96 | console.log('user saved'); 97 | }); 98 | }) 99 | .catch(err => { 100 | console.error(err); 101 | this.setState({ 102 | errors: this.state.errors.concat(err), 103 | loading: false 104 | }); 105 | }); 106 | }) 107 | .catch(err => { 108 | console.error(err); 109 | this.setState({ 110 | errors: this.state.errors.concat(err), 111 | loading: false 112 | }); 113 | }); 114 | } 115 | }; 116 | 117 | saveUser = createdUser => { 118 | return this.state.usersRef.child(createdUser.user.uid).set({ 119 | name: createdUser.user.displayName, 120 | avatar: createdUser.user.photoURL 121 | }); 122 | }; 123 | 124 | render() { 125 | const { 126 | username, 127 | email, 128 | password, 129 | passwordConfirmation, 130 | errors, 131 | loading 132 | } = this.state; 133 | return ( 134 |
135 | 136 | 137 |
138 | 139 | Register for chitChat 140 |
141 |
142 | 143 | 153 | 164 | 175 | 186 | 195 | 196 |
197 | {errors.length > 0 && ( 198 | 199 |

Error

200 | {this.displayErrors(errors)} 201 |
202 | )} 203 | 204 | Already a user? Login 205 | 206 |
207 |
208 |
209 | ); 210 | } 211 | } 212 | 213 | export default Register; 214 | -------------------------------------------------------------------------------- /src/components/SidePanel/UserPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import AvatarEditor from 'react-avatar-editor'; 3 | import firebase from '../../firebaseSetup'; 4 | import { 5 | Grid, 6 | Header, 7 | Icon, 8 | Dropdown, 9 | Image, 10 | Modal, 11 | Input, 12 | Button, 13 | Loader 14 | } from 'semantic-ui-react'; 15 | 16 | class UserPanel extends Component { 17 | state = { 18 | modal: false, 19 | previewImage: '', 20 | croppedImage: '', 21 | blob: '', 22 | uploadedCroppedImage: '', 23 | storageRef: firebase.storage().ref(), 24 | userRef: firebase.auth().currentUser, 25 | usersRef: firebase.database().ref('users'), 26 | loading: false, 27 | metadata: { 28 | contentType: 'image/jpeg' 29 | } 30 | }; 31 | 32 | uploadCroppedImage = () => { 33 | const { storageRef, userRef, blob, metadata } = this.state; 34 | this.setState({ loading: true }); 35 | storageRef 36 | .child(`avatar/user/${userRef.uid}`) 37 | .put(blob, metadata) 38 | .then(snap => { 39 | snap.ref.getDownloadURL().then(downloadURL => { 40 | this.setState({ uploadedCroppedImage: downloadURL }, () => 41 | this.changeAvatar() 42 | ); 43 | }); 44 | }); 45 | }; 46 | 47 | changeAvatar = () => { 48 | this.state.userRef 49 | .updateProfile({ 50 | photoURL: this.state.uploadedCroppedImage 51 | }) 52 | .then(() => { 53 | console.log('PhotoURL updated'); 54 | this.setState({ loading: false }); 55 | this.closeModal(); 56 | }) 57 | .catch(err => console.log(err)); 58 | 59 | this.state.usersRef 60 | .child(this.state.userRef.uid) 61 | .update({ avatar: this.state.uploadedCroppedImage }) 62 | .then(() => { 63 | console.log('Avatar Changed'); 64 | }) 65 | .catch(err => console.log(err)); 66 | }; 67 | 68 | openModal = () => this.setState({ modal: true }); 69 | closeModal = () => this.setState({ modal: false }); 70 | 71 | dropdownOptions = () => [ 72 | { 73 | key: 'user', 74 | text: ( 75 | 76 | Signed in as {this.props.currentUser.displayName} 77 | 78 | ), 79 | disabled: true 80 | }, 81 | { 82 | key: 'avatar', 83 | text: Change Avatar 84 | }, 85 | { 86 | key: 'signout', 87 | text: Sign Out 88 | } 89 | ]; 90 | 91 | handleChange = event => { 92 | const file = event.target.files[0]; 93 | const reader = new FileReader(); 94 | 95 | if (file) { 96 | reader.readAsDataURL(file); 97 | reader.addEventListener('load', () => { 98 | this.setState({ previewImage: reader.result }); 99 | }); 100 | } 101 | }; 102 | 103 | handleCropImage = () => { 104 | if (this.avatarEditor) { 105 | this.avatarEditor.getImageScaledToCanvas().toBlob(blob => { 106 | let imageUrl = URL.createObjectURL(blob); 107 | this.setState({ 108 | croppedImage: imageUrl, 109 | blob 110 | }); 111 | }); 112 | } 113 | }; 114 | 115 | handleSignout = () => { 116 | firebase 117 | .auth() 118 | .signOut() 119 | .then(() => console.log('signed out!')); 120 | }; 121 | 122 | render() { 123 | const { modal, previewImage, croppedImage } = this.state; 124 | const { currentUser } = this.props; 125 | return ( 126 | 127 | 128 | 129 |
135 | 136 | chitChat 137 |
138 |
139 |
140 | 143 | 144 | {currentUser.displayName} 145 | 146 | } 147 | options={this.dropdownOptions()} 148 | /> 149 |
150 | 151 | {this.state.loading ? ( 152 | 153 | 154 | 155 | ) : ( 156 | 157 | Change Avatar 158 | 159 | 160 | 161 | {croppedImage && ( 162 | 171 | )} 172 | 173 | 174 | {previewImage && ( 175 | (this.avatarEditor = editorNode)} 177 | image={previewImage} 178 | width={300} 179 | height={300} 180 | border={20} 181 | style={{ margin: '2em', borderRadius: '5px' }} 182 | scale={1.2} 183 | /> 184 | )} 185 | 186 | 187 | 195 | 196 | 197 | {croppedImage && ( 198 | 205 | )} 206 | 209 | 212 | 213 | 214 | )} 215 | 216 |
217 |
218 | ); 219 | } 220 | } 221 | 222 | export default UserPanel; 223 | -------------------------------------------------------------------------------- /src/components/SidePanel/Channels.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import firebase from '../../firebaseSetup'; 3 | import { 4 | Menu, 5 | Icon, 6 | Modal, 7 | Form, 8 | Input, 9 | Button, 10 | Label 11 | } from 'semantic-ui-react'; 12 | import { connect } from 'react-redux'; 13 | import { setCurrentChannel, setPrivateChannel } from '../../store/actions'; 14 | 15 | class Channels extends Component { 16 | state = { 17 | activeChannel: '', 18 | user: this.props.currentUser, 19 | channel: null, 20 | channels: [], 21 | channelName: '', 22 | channelDetails: '', 23 | channelsRef: firebase.database().ref('channels'), 24 | messagesRef: firebase.database().ref('messages'), 25 | typingRef: firebase.database().ref('typing'), 26 | notifications: [], 27 | modal: false, 28 | firstLoad: true 29 | }; 30 | 31 | componentDidMount() { 32 | this.addListeners(); 33 | } 34 | 35 | componentWillMount() { 36 | this.removeListeners(); 37 | } 38 | 39 | addListeners = () => { 40 | let loadedChannels = []; 41 | this.state.channelsRef.on('child_added', snap => { 42 | loadedChannels.push(snap.val()); 43 | this.setState({ channels: loadedChannels }, () => this.setFirstChannel()); 44 | this.addNotificationListener(snap.key); 45 | }); 46 | }; 47 | 48 | addNotificationListener = channelId => { 49 | this.state.messagesRef.child(channelId).on('value', snap => { 50 | if (this.state.channel) { 51 | this.handleNotifications( 52 | channelId, 53 | this.state.channel.id, 54 | this.state.notifications, 55 | snap 56 | ); 57 | } 58 | }); 59 | }; 60 | 61 | handleNotifications = (channelId, currentChannelId, notifications, snap) => { 62 | let lastTotal = 0; 63 | let index = notifications.findIndex( 64 | notification => notification.id === channelId 65 | ); 66 | 67 | if (index !== -1) { 68 | if (channelId !== currentChannelId) { 69 | lastTotal = notifications[index].total; 70 | if (snap.numChildren() - lastTotal > 0) { 71 | notifications[index].count = snap.numChildren() - lastTotal; 72 | } 73 | } 74 | notifications[index].lastKnownTotal = snap.numChildren(); 75 | } else { 76 | notifications.push({ 77 | id: channelId, 78 | total: snap.numChildren(), 79 | lastKnownTotal: snap.numChildren(), 80 | count: 0 81 | }); 82 | } 83 | this.setState({ notifications }); 84 | }; 85 | 86 | removeListeners = () => { 87 | this.state.channelsRef.off(); 88 | this.state.channels.forEach(channel => { 89 | this.state.messagesRef.child(channel.id).off(); 90 | }); 91 | }; 92 | 93 | setFirstChannel = () => { 94 | const firstChannel = this.state.channels[0]; 95 | if (this.state.firstLoad && this.state.channels.length > 0) { 96 | this.props.setCurrentChannel(firstChannel); 97 | this.setActiveChannel(firstChannel); 98 | this.setState({ channel: firstChannel }); 99 | } 100 | this.setState({ firstLoad: false }); 101 | }; 102 | 103 | addChannel = () => { 104 | const { channelsRef, channelName, channelDetails, user } = this.state; 105 | 106 | const key = channelsRef.push().key; 107 | 108 | const newChannel = { 109 | id: key, 110 | name: channelName, 111 | details: channelDetails, 112 | isNew: true, 113 | createdBy: { 114 | name: user.displayName, 115 | avatar: user.photoURL 116 | } 117 | }; 118 | 119 | channelsRef 120 | .child(key) 121 | .update(newChannel) 122 | .then(() => { 123 | this.setState({ channelName: '', channelDetails: '' }); 124 | this.closeModal(); 125 | console.log('channel added'); 126 | }) 127 | .catch(err => { 128 | console.error(err); 129 | }); 130 | }; 131 | 132 | handleChange = event => { 133 | this.setState({ [event.target.name]: event.target.value }); 134 | }; 135 | 136 | handleSubmit = event => { 137 | event.preventDefault(); 138 | if (this.isFormValid(this.state)) { 139 | this.addChannel(); 140 | } 141 | }; 142 | 143 | isFormValid = ({ channelName, channelDetails }) => 144 | channelName && channelDetails; 145 | 146 | closeModal = () => { 147 | this.setState({ modal: false }); 148 | }; 149 | openModal = () => { 150 | this.setState({ modal: true }); 151 | }; 152 | 153 | getNotificationCount = channel => { 154 | let count = 0; 155 | 156 | this.state.notifications.forEach(notification => { 157 | if (notification.id === channel.id) { 158 | count = notification.count; 159 | } 160 | }); 161 | 162 | if (count > 0) return count; 163 | }; 164 | 165 | displayChannels = channels => 166 | channels.length > 0 && 167 | channels.map(channel => ( 168 | this.changeChannel(channel)} 171 | name={channel.name} 172 | style={{ opacity: 0.7 }} 173 | active={channel.id === this.state.activeChannel} 174 | > 175 | {this.getNotificationCount(channel) && ( 176 | 177 | )} 178 | # {channel.name} 179 | 180 | )); 181 | 182 | changeChannel = channel => { 183 | this.setActiveChannel(channel); 184 | this.state.typingRef 185 | .child(this.state.channel.id) 186 | .child(this.state.user.uid) 187 | .remove(); 188 | this.clearNotifications(); 189 | this.props.setCurrentChannel(channel); 190 | this.props.setPrivateChannel(false); 191 | this.setState({ channel }); 192 | }; 193 | 194 | clearNotifications = () => { 195 | let index = this.state.notifications.findIndex( 196 | notification => notification.id === this.state.channel.id 197 | ); 198 | 199 | if (index !== -1) { 200 | let updatedNotifications = [...this.state.notifications]; 201 | updatedNotifications[index].total = this.state.notifications[ 202 | index 203 | ].lastKnownTotal; 204 | updatedNotifications[index].count = 0; 205 | this.setState({ notifications: updatedNotifications }); 206 | } 207 | }; 208 | 209 | setActiveChannel = channel => { 210 | this.setState({ activeChannel: channel.id }); 211 | }; 212 | 213 | render() { 214 | const { channels, modal } = this.state; 215 | return ( 216 | 217 | 218 | 219 | 220 | CHANNELS 221 | {' '} 222 | ({channels.length}) 223 | 224 | {this.displayChannels(channels)} 225 | 226 | 227 | Add a channel 228 | 229 |
230 | 231 | 237 | 238 | 239 | 245 | 246 |
247 |
248 | 249 | 250 | 253 | 256 | 257 |
258 |
259 | ); 260 | } 261 | } 262 | 263 | export default connect( 264 | null, 265 | { setCurrentChannel, setPrivateChannel } 266 | )(Channels); 267 | -------------------------------------------------------------------------------- /src/components/Messages/MessagesForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import firebase from '../../firebaseSetup'; 3 | import uuidv4 from 'uuid/v4'; 4 | import { Segment, Button, Input } from 'semantic-ui-react'; 5 | import FileModal from './FileModal'; 6 | import ProgressBar from './../UI/ProgressBar'; 7 | import { Picker, emojiIndex } from 'emoji-mart'; 8 | import 'emoji-mart/css/emoji-mart.css'; 9 | 10 | class MessageForm extends Component { 11 | state = { 12 | storageRef: firebase.storage().ref(), 13 | typingRef: firebase.database().ref('typing'), 14 | uploadTask: null, 15 | uploadState: '', 16 | percentUploaded: 0, 17 | message: '', 18 | channel: this.props.currentChannel, 19 | user: this.props.currentUser, 20 | loading: false, 21 | errors: [], 22 | modal: false, 23 | emojiPicker: false 24 | }; 25 | 26 | componentWillUnmount() { 27 | if (this.state.uploadTask !== null) { 28 | this.state.uploadTask.cancel(); 29 | this.setState({ uploadTask: null }); 30 | } 31 | } 32 | 33 | openModal = () => this.setState({ modal: true }); 34 | closeModal = () => this.setState({ modal: false }); 35 | 36 | handleChange = event => { 37 | this.setState( 38 | { [event.target.name]: event.target.value }, 39 | this.handleTypingState 40 | ); 41 | }; 42 | 43 | handleTypingState = () => { 44 | const { message, typingRef, channel, user } = this.state; 45 | 46 | if (message) { 47 | typingRef 48 | .child(channel.id) 49 | .child(user.uid) 50 | .set(user.displayName); 51 | } else { 52 | typingRef 53 | .child(channel.id) 54 | .child(user.uid) 55 | .remove(); 56 | } 57 | }; 58 | 59 | createMessage = (fileUrl = null) => { 60 | const message = { 61 | timestamp: firebase.database.ServerValue.TIMESTAMP, 62 | user: { 63 | id: this.state.user.uid, 64 | name: this.state.user.displayName, 65 | avatar: this.state.user.photoURL 66 | } 67 | }; 68 | if (fileUrl !== null) { 69 | message['image'] = fileUrl; 70 | } else { 71 | message['content'] = this.state.message; 72 | } 73 | return message; 74 | }; 75 | 76 | handleKeyPress = event => 77 | event.which === 13 && this.state.message ? this.sendMessage() : null; 78 | 79 | sendMessage = () => { 80 | const { getMessagesRef } = this.props; 81 | const { message, channel } = this.state; 82 | 83 | if (message) { 84 | this.setState({ loading: true }); 85 | getMessagesRef() 86 | .child(channel.id) 87 | .push() 88 | .set(this.createMessage()) 89 | .then(() => { 90 | this.setState( 91 | { loading: false, message: '', errors: [] }, 92 | this.handleTypingState 93 | ); 94 | }) 95 | .catch(err => { 96 | console.error(err); 97 | this.setState({ 98 | loading: false, 99 | errors: this.state.errors.concat(err) 100 | }); 101 | }); 102 | } else { 103 | this.setState({ 104 | errors: this.state.errors.concat({ message: 'Add a message' }) 105 | }); 106 | } 107 | }; 108 | 109 | getPath = () => { 110 | if (this.props.isPrivateChannel) { 111 | return `chat/private/${this.state.channel.id}`; 112 | } else { 113 | return 'chat/public'; 114 | } 115 | }; 116 | 117 | uploadFile = (file, metadata) => { 118 | const pathToUpload = this.state.channel.id; 119 | const ref = this.props.getMessagesRef(); 120 | const filePath = `${this.getPath()}/${uuidv4()}.jpg`; 121 | this.setState( 122 | { 123 | uploadState: 'uploading', 124 | uploadTask: this.state.storageRef.child(filePath).put(file, metadata) 125 | }, 126 | () => { 127 | this.state.uploadTask.on( 128 | 'state_changed', 129 | snap => { 130 | const percentUploaded = Math.round( 131 | (snap.bytesTransferred / snap.totalBytes) * 100 132 | ); 133 | this.props.isProgressBarVisible(percentUploaded); 134 | this.setState({ percentUploaded }); 135 | }, 136 | err => { 137 | console.error(err); 138 | this.setState({ 139 | errors: this.state.errors.concat(err), 140 | uploadState: 'error', 141 | uploadTask: null 142 | }); 143 | }, 144 | () => { 145 | this.state.uploadTask.snapshot.ref 146 | .getDownloadURL() 147 | .then(downloadUrl => { 148 | this.sendFileMessage(downloadUrl, ref, pathToUpload); 149 | }) 150 | .catch(err => { 151 | console.error(err); 152 | this.setState({ 153 | errors: this.state.errors.concat(err), 154 | uploadState: 'error', 155 | uploadTask: null 156 | }); 157 | }); 158 | } 159 | ); 160 | } 161 | ); 162 | }; 163 | 164 | sendFileMessage = (fileUrl, ref, pathToUpload) => { 165 | ref 166 | .child(pathToUpload) 167 | .push() 168 | .set(this.createMessage(fileUrl)) 169 | .then(() => { 170 | this.setState({ uploadState: 'done' }); 171 | }) 172 | .catch(err => { 173 | console.error(err); 174 | this.setState({ 175 | errors: this.state.errors.concat(err) 176 | }); 177 | }); 178 | }; 179 | 180 | handleTogglePicker = () => { 181 | this.setState({ emojiPicker: !this.state.emojiPicker }); 182 | }; 183 | 184 | handleAddEmoji = emoji => { 185 | const oldMessage = this.state.message; 186 | const newMessage = this.colonToUnicode(`${oldMessage} ${emoji.colons} `); 187 | this.setState({ message: newMessage, emojiPicker: false }); 188 | this.messageInput.focus(); 189 | }; 190 | 191 | colonToUnicode = message => { 192 | return message.replace(/:[A-Za-z0-9_+]+:/g, x => { 193 | x = x.replace(/:/g, ''); 194 | let emoji = emojiIndex.emojis[x]; 195 | if (typeof emoji !== 'undefined') { 196 | let unicode = emoji.native; 197 | if (typeof unicode !== 'undefined') { 198 | return unicode; 199 | } 200 | } 201 | x = ':' + x + ':'; 202 | return x; 203 | }); 204 | }; 205 | 206 | render() { 207 | const { 208 | errors, 209 | message, 210 | loading, 211 | modal, 212 | uploadState, 213 | percentUploaded, 214 | emojiPicker 215 | } = this.state; 216 | return ( 217 | 218 | {emojiPicker && ( 219 | 226 | )} 227 | (this.messageInput = inputNode)} 236 | label={ 237 |