├── .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 |
9 | );
10 |
11 | export default Typing;
12 |
--------------------------------------------------------------------------------
/src/components/Messages/Skeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Skeleton = () => (
4 |
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 |
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 |
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 |
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 |
7 |
8 |
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 |
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 |
99 |
100 |
101 | ))
102 | );
103 | };
104 |
105 | render() {
106 | const { modal, primary, secondary, userColors } = this.state;
107 |
108 | return (
109 |
117 |
118 |
119 | {this.displayUserColors(userColors)}
120 |
121 |
128 | Choose App Colors
129 |
130 |
131 |
132 |
133 |
137 |
138 |
139 |
143 |
147 |
148 |
149 |
150 |
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 |
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 |
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 |
242 | }
243 | labelPosition='left'
244 | className={
245 | errors.some(error => error.message.includes('message'))
246 | ? 'error'
247 | : ''
248 | }
249 | placeholder='Write your message'
250 | />
251 |
252 |
260 |
268 |
269 |
274 |
278 |
279 | );
280 | }
281 | }
282 |
283 | export default MessageForm;
284 |
--------------------------------------------------------------------------------
/src/components/Messages/Messages.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { Segment, Comment } from 'semantic-ui-react';
3 | import MessagesHeader from './MessagesHeader';
4 | import MessageForm from './MessagesForm';
5 | import Message from './Message';
6 | import Typing from './Typing';
7 | import Skeleton from './Skeleton';
8 | import firebase from '../../firebaseSetup';
9 | import { setUserPosts } from '../../store/actions';
10 | import { connect } from 'react-redux';
11 |
12 | class Messages extends Component {
13 | state = {
14 | privateChannel: this.props.isPrivateChannel,
15 | privateMessagesRef: firebase.database().ref('privateMessages'),
16 | messagesRef: firebase.database().ref('messages'),
17 | typingUsers: [],
18 | messages: [],
19 | messagesLoading: true,
20 | channel: this.props.currentChannel,
21 | isChannelStarred: false,
22 | user: this.props.currentUser,
23 | usersRef: firebase.database().ref('users'),
24 | progressBar: false,
25 | numUniqueUsers: '',
26 | searchTerm: '',
27 | searchLoading: false,
28 | searchResults: [],
29 | connectedRef: firebase.database().ref('.info/connected'),
30 | typingRef: firebase.database().ref('typing')
31 | };
32 |
33 | componentDidMount() {
34 | const { channel, user } = this.state;
35 |
36 | if (channel && user) {
37 | this.addListeners(channel.id);
38 | this.addUserStarsListener(channel.id, user.uid);
39 | }
40 | }
41 |
42 | componentDidUpdate(prevProps, prevState) {
43 | if (this.messagesEnd) {
44 | this.scrollToBottom();
45 | }
46 | }
47 |
48 | scrollToBottom = () => {
49 | this.messagesEnd.scrollIntoView({ behavior: 'smooth' });
50 | };
51 |
52 | addListeners = channelId => {
53 | this.addMessageListener(channelId);
54 | this.addTypingListeners(channelId);
55 | };
56 |
57 | addTypingListeners = channelId => {
58 | let typingUsers = [];
59 | this.state.typingRef.child(channelId).on('child_added', snap => {
60 | if (snap.key !== this.state.user.uid) {
61 | typingUsers = typingUsers.concat({
62 | id: snap.key,
63 | name: snap.val()
64 | });
65 | this.setState({ typingUsers });
66 | }
67 | });
68 |
69 | this.state.typingRef.child(channelId).on('child_removed', snap => {
70 | const index = typingUsers.findIndex(user => user.id === snap.key);
71 |
72 | if (index !== -1) {
73 | typingUsers = typingUsers.filter(user => user.id !== snap.key);
74 | this.setState({ typingUsers });
75 | }
76 | });
77 |
78 | this.state.connectedRef.on('value', snap => {
79 | if (snap.value === true) {
80 | this.state.typingRef
81 | .child(channelId)
82 | .child(this.state.user.uid)
83 | .onDisconnect()
84 | .remove(err => {
85 | if (err !== null) {
86 | console.log(err);
87 | }
88 | });
89 | }
90 | });
91 | };
92 |
93 | addUserStarsListener = (channelId, userId) => {
94 | this.state.usersRef
95 | .child(userId)
96 | .child('starred')
97 | .once('value')
98 | .then(data => {
99 | if (data.val() !== null) {
100 | const channelIds = Object.keys(data.val());
101 | const prevStarred = channelIds.includes(channelId);
102 | this.setState({ isChannelStarred: prevStarred });
103 | }
104 | });
105 | };
106 |
107 | addMessageListener = channelId => {
108 | let loadedMessages = [];
109 | const ref = this.getMessagesRef();
110 | ref.child(channelId).on('child_added', snap => {
111 | loadedMessages.push(snap.val());
112 | this.setState({
113 | messages: loadedMessages,
114 | messagesLoading: false
115 | });
116 | this.countUniqueUsers(loadedMessages);
117 | this.countUserPosts(loadedMessages);
118 | });
119 | };
120 |
121 | getMessagesRef = () => {
122 | const { messagesRef, privateMessagesRef, privateChannel } = this.state;
123 | return privateChannel ? privateMessagesRef : messagesRef;
124 | };
125 |
126 | countUniqueUsers = messages => {
127 | const uniqueUsers = messages.reduce((acc, message) => {
128 | if (!acc.includes(message.user.name)) {
129 | acc.push(message.user.name);
130 | }
131 | return acc;
132 | }, []);
133 | const plural = uniqueUsers.length > 1 || uniqueUsers === 0;
134 | const numUniqueUsers = `${uniqueUsers.length} user${plural ? 's' : ''}`;
135 | this.setState({ numUniqueUsers });
136 | };
137 |
138 | countUserPosts = messages => {
139 | let userPosts = messages.reduce((acc, message) => {
140 | if (message.user.name in acc) {
141 | acc[message.user.name].count += 1;
142 | } else {
143 | acc[message.user.name] = {
144 | avatar: message.user.avatar,
145 | count: 1
146 | };
147 | }
148 | return acc;
149 | }, {});
150 | this.props.setUserPosts(userPosts);
151 | };
152 |
153 | handleStar = () => {
154 | this.setState(
155 | prevState => ({ isChannelStarred: !prevState.isChannelStarred }),
156 | () => this.starChannel()
157 | );
158 | };
159 |
160 | starChannel = () => {
161 | if (this.state.isChannelStarred) {
162 | this.state.usersRef.child(`${this.state.user.uid}/starred`).update({
163 | [this.state.channel.id]: {
164 | name: this.state.channel.name,
165 | details: this.state.channel.details,
166 | createdBy: {
167 | name: this.state.channel.createdBy.name,
168 | avatar: this.state.channel.createdBy.avatar
169 | }
170 | }
171 | });
172 | } else {
173 | this.state.usersRef
174 | .child(`${this.state.user.uid}/starred`)
175 | .child(this.state.channel.id)
176 | .remove(err => {
177 | if (err !== null) {
178 | console.log(err);
179 | }
180 | });
181 | }
182 | };
183 |
184 | handleSearchChange = event => {
185 | this.setState(
186 | {
187 | searchTerm: event.target.value,
188 | searchLoading: true
189 | },
190 | () => this.handleSearchMessages()
191 | );
192 | };
193 |
194 | handleSearchMessages = () => {
195 | const channelMessages = [...this.state.messages];
196 | const regex = new RegExp(this.state.searchTerm, 'gi');
197 | const searchResults = channelMessages.reduce((acc, message) => {
198 | if (
199 | (message.content && message.content.match(regex)) ||
200 | (message.user.name && message.user.name.match(regex))
201 | ) {
202 | acc.push(message);
203 | }
204 | return acc;
205 | }, []);
206 | this.setState({ searchResults });
207 | setTimeout(() => this.setState({ searchLoading: false }), 1000);
208 | };
209 |
210 | displayMessages = messages =>
211 | messages.length > 0 &&
212 | messages.map(message => (
213 |
218 | ));
219 |
220 | displayMessagesSkeleton = loading =>
221 | loading ? (
222 |
223 | {[...Array(20)].map((val, i) => (
224 |
225 | ))}
226 |
227 | ) : null;
228 |
229 | displayChannelName = channel => {
230 | return channel
231 | ? `${this.state.privateChannel ? '@' : '#'}${channel.name}`
232 | : '';
233 | };
234 |
235 | isProgressBarVisible = percent => {
236 | if (percent > 0) {
237 | this.setState({ progressBar: true });
238 | }
239 | };
240 |
241 | displayTypingUsers = users => {
242 | if (users.length > 1) {
243 | return users.map((user, index) => (
244 |
245 |
246 | {user.name} is typing{users.length - 1 === index ? '' : ','}
247 |
248 |
249 | ));
250 | } else {
251 | return users.map(user => (
252 |
253 | {user.name} is typing
254 |
255 | ));
256 | }
257 | };
258 |
259 | render() {
260 | const {
261 | messagesRef,
262 | messages,
263 | channel,
264 | user,
265 | progressBar,
266 | numUniqueUsers,
267 | searchTerm,
268 | searchResults,
269 | searchLoading,
270 | privateChannel,
271 | isChannelStarred,
272 | typingUsers,
273 | messagesLoading
274 | } = this.state;
275 | return (
276 |
277 |
286 |
287 |
288 |
292 | {!privateChannel && this.displayMessagesSkeleton(messagesLoading)}
293 | {searchTerm
294 | ? this.displayMessages(searchResults)
295 | : this.displayMessages(messages)}
296 | (this.messagesEnd = node)} />
297 |
298 | {typingUsers.length > 0 && this.displayTypingUsers(typingUsers)}
299 | {typingUsers.length > 0 && }
300 |
301 |
302 |
303 |
304 |
312 |
313 | );
314 | }
315 | }
316 |
317 | export default connect(
318 | null,
319 | { setUserPosts }
320 | )(Messages);
321 |
--------------------------------------------------------------------------------