├── .gitignore ├── README.md ├── app ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── api │ │ ├── api.ts │ │ └── apiInfo.ts │ ├── features │ │ ├── activity │ │ │ ├── IdActiveType.ts │ │ │ ├── actionActive.ts │ │ │ └── reducerActive.ts │ │ ├── auth │ │ │ ├── TempLogin.tsx │ │ │ ├── actionAuth.ts │ │ │ ├── epic │ │ │ │ ├── epicAuthListUsers.ts │ │ │ │ ├── epicAuthSocketSignIn.ts │ │ │ │ └── epicCompanyUsers.ts │ │ │ ├── epicAuth.ts │ │ │ ├── reducerAuth.ts │ │ │ └── tempAuth.scss │ │ ├── channels │ │ │ ├── Channels.tsx │ │ │ ├── IChannelInfo.ts │ │ │ ├── actionChannel.ts │ │ │ ├── channels.scss │ │ │ └── reducerChannel.ts │ │ ├── chatBox │ │ │ ├── ChatBox.tsx │ │ │ └── chatBox.scss │ │ ├── conversation │ │ │ ├── actionConverstion.ts │ │ │ ├── epicConversation.ts │ │ │ ├── epics │ │ │ │ └── epicMessageSent.ts │ │ │ └── reducerConversation.ts │ │ ├── generic │ │ │ ├── messageBubble │ │ │ │ ├── MessageBubble.tsx │ │ │ │ └── messageBubble.scss │ │ │ ├── popOver │ │ │ │ └── PopOver.tsx │ │ │ └── scrollWrapper │ │ │ │ └── ScrollSection.tsx │ │ ├── modal │ │ │ ├── IdModalContent.ts │ │ │ ├── IdModalRegistry.ts │ │ │ ├── Modal.tsx │ │ │ ├── actionModal.ts │ │ │ ├── modal.scss │ │ │ └── reducerModal.ts │ │ ├── navBot │ │ │ ├── BotNav.tsx │ │ │ └── botNav.scss │ │ ├── navSide │ │ │ ├── SideNav.tsx │ │ │ └── sideNav.scss │ │ ├── navTop │ │ │ ├── TopNav.tsx │ │ │ └── topNav.scss │ │ ├── newUser │ │ │ └── NewUser.tsx │ │ ├── userList │ │ │ ├── UserList.tsx │ │ │ └── userList.scss │ │ └── users │ │ │ ├── actionUsers.ts │ │ │ ├── epicUsers.ts │ │ │ └── reducerUsers.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── pages │ │ ├── chat │ │ │ ├── Chat.tsx │ │ │ └── chat.scss │ │ └── welcome │ │ │ ├── Welcome.tsx │ │ │ └── welcome.scss │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ ├── setup │ │ ├── IState.ts │ │ ├── rootEpic.ts │ │ ├── rootReducer.ts │ │ └── store.ts │ ├── types │ │ ├── IMessage.ts │ │ └── IUser.ts │ └── util │ │ └── socket │ │ └── utilSocket.ts ├── tsconfig.json └── yarn.lock └── server ├── .gitignore ├── package-lock.json ├── package.json ├── src ├── index.ts ├── socket │ └── infoSocket.ts ├── temp │ └── users.ts └── types │ ├── IMessage.ts │ └── IUser.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple chat (with chat room) 2 | 3 | JavaScript chat app. Using React.js, Socket.io and Node.js. Layout using Material UI. No database, everything readonly. 4 | 5 | ## Setup 6 | 7 | - `npm install` then-> `npm start` (from /server and /app dirs) 8 | 9 | ## Summary 10 | - shows online users 11 | - all users sorted based on online status 12 | - default set of users to signin 13 | - users divided my company 14 | - users from one company cannot see/chat with users from other company 15 | - first 4 users from company A and rest 4 belongs to company B 16 | - default chatroom/channel where all users of a company is listening 17 | - handled multiple tab for same user -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-c", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.22", 7 | "@fortawesome/free-solid-svg-icons": "^5.10.2", 8 | "@fortawesome/react-fontawesome": "^0.1.4", 9 | "@material-ui/core": "^4.3.3", 10 | "@material-ui/icons": "^4.2.1", 11 | "@types/jest": "24.0.18", 12 | "@types/node": "12.7.2", 13 | "@types/react": "16.9.2", 14 | "@types/react-dom": "16.9.0", 15 | "axios": "^0.19.0", 16 | "node-sass": "^4.12.0", 17 | "react": "^16.9.0", 18 | "react-dom": "^16.9.0", 19 | "react-notifications": "^1.4.3", 20 | "react-redux": "^7.1.0", 21 | "react-scripts": "3.1.1", 22 | "react-scrollbar": "^0.5.6", 23 | "redux": "^4.0.4", 24 | "redux-observable": "^1.1.0", 25 | "rxjs": "^6.5.2", 26 | "socket.io-client": "^2.2.0", 27 | "typed-responsive-react": "^1.0.2", 28 | "typescript": "3.5.3" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@types/react-redux": "^7.1.2", 53 | "@types/react-scrollbar": "^0.4.10", 54 | "@types/socket.io-client": "^1.4.32" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiddharthaChowdhury/react-chat-app/888fad6be54949aaca1d904e116e0556ea38c63f/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiddharthaChowdhury/react-chat-app/888fad6be54949aaca1d904e116e0556ea38c63f/app/public/logo192.png -------------------------------------------------------------------------------- /app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiddharthaChowdhury/react-chat-app/888fad6be54949aaca1d904e116e0556ea38c63f/app/public/logo512.png -------------------------------------------------------------------------------- /app/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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from './features/modal/Modal'; 3 | import {Action, Dispatch} from 'redux'; 4 | import {IState} from './setup/IState'; 5 | import { 6 | actionUsersOnlineUserStatusUpdate, 7 | actionUsersPendingUpdate, 8 | actionUsersRequest 9 | } from './features/users/actionUsers'; 10 | import {connect} from 'react-redux'; 11 | import {IReducerAuth} from './features/auth/reducerAuth'; 12 | import {Chat} from './pages/chat/Chat'; 13 | import {Welcome} from './pages/welcome/Welcome'; 14 | import {UtilSocket} from './util/socket/utilSocket'; 15 | import {IdMessageSource, IMessageInfo} from './types/IMessage'; 16 | import {actionMessageReceive} from './features/conversation/actionConverstion'; 17 | import {IReducerActive} from "./features/activity/reducerActive"; 18 | import {IdActiveType} from "./features/activity/IdActiveType"; 19 | import {actionChannelPendingUpdate} from "./features/channels/actionChannel"; 20 | 21 | interface IAppState { 22 | active: IReducerActive; 23 | authInfo: IReducerAuth; 24 | } 25 | interface IAppDispatch { 26 | onGetUserList: () => Action; 27 | onMessageReceived: (messageInfo: IMessageInfo, fromId: number | string) => Action; 28 | onUpdateUserPendingMessage: (userId: number) => Action; 29 | onUpdateChannelPendingMessage: (channelKey: string) => Action; 30 | onOnlineUserUpdate: (userIds: Array) => Action; 31 | } 32 | interface IAppProps extends IAppState, IAppDispatch {} 33 | 34 | export const $conn = new UtilSocket(); 35 | 36 | class AppDOM extends React.Component { 37 | public render () { 38 | const {isLoggedIn} = this.props.authInfo; 39 | return ( 40 |
41 | 42 | {isLoggedIn ? : } 43 |
44 | ) 45 | }; 46 | 47 | public componentDidMount () { 48 | this.props.onGetUserList(); 49 | this.setupSocketListeners(); 50 | }; 51 | 52 | private setupSocketListeners = () => { 53 | $conn.socket.on("message", this.onMessageReceived); 54 | $conn.socket.on("channel-broadcast", this.onChannelMessageReceived); 55 | $conn.socket.on("online-users", this.onOnlineUserUpdate); 56 | $conn.socket.on("reconnect", this.onReconnection); 57 | $conn.socket.on("disconnect", this.onClientDisconnected); 58 | }; 59 | 60 | private onOnlineUserUpdate = (onlineUsers: Array) => { 61 | console.log(onlineUsers); 62 | this.props.onOnlineUserUpdate(onlineUsers); 63 | }; 64 | 65 | private onClientDisconnected = () => { 66 | console.log( 67 | "Connection Lost from server please check your connection.", 68 | "Error!" 69 | ); 70 | }; 71 | 72 | private onReconnection = () => { 73 | console.log('trying reconnect'); 74 | if(!this.props) { 75 | return; 76 | } 77 | const {isLoggedIn, userInfo} = this.props.authInfo; 78 | if (isLoggedIn && userInfo) { 79 | $conn.socket.emit("sign-in", userInfo); 80 | console.log("Connection Established.", "Reconnected!"); 81 | } 82 | }; 83 | 84 | private onMessageReceived = (messageInfo: IMessageInfo) => { 85 | console.log(messageInfo); 86 | const {fromId, selfEcho, toId} = messageInfo; 87 | const {onMessageReceived, onUpdateUserPendingMessage, active} = this.props; 88 | 89 | const index = selfEcho ? toId : fromId; 90 | 91 | onMessageReceived(messageInfo, index!); 92 | if( active.type !== IdActiveType.Individual || 93 | ( active.type === IdActiveType.Individual && 94 | !selfEcho && 95 | active.selectedUser && 96 | active.selectedUser.id !== fromId 97 | ) 98 | ){ 99 | onUpdateUserPendingMessage(fromId); 100 | } 101 | }; 102 | 103 | private onChannelMessageReceived = (messageInfo: IMessageInfo) => { 104 | const {channelId, source} = messageInfo; 105 | const {onMessageReceived, onUpdateChannelPendingMessage, active} = this.props; 106 | if(source === IdMessageSource.Channel_Message) { 107 | onMessageReceived(messageInfo, channelId!); 108 | 109 | if(active.type !== IdActiveType.Channel || 110 | (active.type === IdActiveType.Channel && 111 | active.selectedChannel && 112 | active.selectedChannel.key !== channelId 113 | ) 114 | ){ 115 | onUpdateChannelPendingMessage(channelId!) 116 | } 117 | } 118 | }; 119 | } 120 | 121 | export const mapState = (state: IState): IAppState => ({ 122 | active: state.active, 123 | authInfo: state.auth 124 | }); 125 | export const mapDispatch = (dispatch: Dispatch): IAppDispatch => ({ 126 | onGetUserList: () => dispatch(actionUsersRequest()), 127 | onMessageReceived: (messageInfo: IMessageInfo, fromId: number | string) => dispatch(actionMessageReceive(messageInfo, fromId)), 128 | onUpdateUserPendingMessage: (userId: number) => dispatch(actionUsersPendingUpdate(userId)), 129 | onUpdateChannelPendingMessage: (channelKey: string) => dispatch(actionChannelPendingUpdate(channelKey)), 130 | onOnlineUserUpdate: (userIds: Array) => dispatch(actionUsersOnlineUserStatusUpdate(userIds)) 131 | }); 132 | 133 | export default connect(mapState, mapDispatch)(AppDOM) 134 | -------------------------------------------------------------------------------- /app/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import {host, Methods, TypeAPI} from "./apiInfo"; 2 | 3 | export enum ApiName { 4 | GetUsers = "GetUsers", 5 | GetCompanyUsers = "GetCompanyUsers" 6 | } 7 | 8 | export const API: TypeAPI = { 9 | [ApiName.GetUsers]: {method: Methods.GET, url: host + '/users'}, 10 | [ApiName.GetCompanyUsers]: {method: Methods.GET, url: host + '/users/'}, 11 | }; -------------------------------------------------------------------------------- /app/src/api/apiInfo.ts: -------------------------------------------------------------------------------- 1 | import {ApiName} from "./api"; 2 | 3 | export enum Methods { 4 | POST = 'POST', 5 | GET = 'GET', 6 | PUT = 'PUT' 7 | } 8 | export const host = process.env.NODE_ENV === 'development' ? 'http://localhost:8002' : ''; 9 | 10 | export type TypeAPI = {[id in ApiName]: {method: string; url: string}} -------------------------------------------------------------------------------- /app/src/features/activity/IdActiveType.ts: -------------------------------------------------------------------------------- 1 | export enum IdActiveType { 2 | Channel = "Channel", 3 | Individual = "Individual", 4 | None = "None" 5 | } -------------------------------------------------------------------------------- /app/src/features/activity/actionActive.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | import { IUserInfo } from "../../types/IUser"; 3 | import { IdActiveType } from "./IdActiveType"; 4 | import {IChannelInfo} from "../channels/IChannelInfo"; 5 | 6 | export enum TypeActionActive { 7 | ACTIVE_UPDATE_USER = "Active > Update > User", 8 | ACTIVE_UPDATE_CHANNEL = "Active > Update > Channel" 9 | } 10 | 11 | export interface IActionActive extends Action { 12 | userInfo?: IUserInfo; 13 | channelInfo?: IChannelInfo 14 | activeType: IdActiveType; 15 | type: TypeActionActive 16 | } 17 | 18 | export const actionActiveUserUpdate = (userInfo: IUserInfo, activeType: IdActiveType = IdActiveType.Individual): IActionActive => ({ 19 | userInfo, 20 | activeType, 21 | type: TypeActionActive.ACTIVE_UPDATE_USER 22 | }) 23 | 24 | export const actionActiveChannelUpdate = (channelInfo: IChannelInfo, activeType: IdActiveType = IdActiveType.Channel): IActionActive => ({ 25 | channelInfo, 26 | activeType, 27 | type: TypeActionActive.ACTIVE_UPDATE_CHANNEL 28 | }) -------------------------------------------------------------------------------- /app/src/features/activity/reducerActive.ts: -------------------------------------------------------------------------------- 1 | import { IdActiveType } from "./IdActiveType"; 2 | import { IUserInfo } from "../../types/IUser"; 3 | import { IActionActive, TypeActionActive } from "./actionActive"; 4 | import {IChannelInfo} from "../channels/IChannelInfo"; 5 | 6 | export interface IReducerActive { 7 | type: IdActiveType; 8 | selectedUser?: IUserInfo; 9 | selectedChannel?: IChannelInfo; 10 | } 11 | 12 | const initial: IReducerActive = { 13 | type: IdActiveType.Channel, 14 | selectedChannel: { 15 | key: "channel23", 16 | name: "Weird Company" 17 | } 18 | } 19 | 20 | export default (state: IReducerActive = initial, action: IActionActive): IReducerActive => { 21 | switch(action.type) { 22 | case TypeActionActive.ACTIVE_UPDATE_USER: 23 | return { 24 | ...state, selectedUser: action.userInfo!, 25 | type: action.activeType 26 | } 27 | case TypeActionActive.ACTIVE_UPDATE_CHANNEL: 28 | return { 29 | ...state, 30 | selectedChannel: action.channelInfo!, 31 | selectedUser: undefined, 32 | type: action.activeType 33 | } 34 | default: 35 | return state; 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/features/auth/TempLogin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IUserInfo } from "../../types/IUser"; 3 | import { Action, Dispatch } from "redux"; 4 | import { connect } from 'react-redux'; 5 | import { IState } from '../../setup/IState'; 6 | import './tempAuth.scss'; 7 | import { actionAuthSignInRequest } from './actionAuth'; 8 | import { actionModalCloseAll } from '../modal/actionModal'; 9 | 10 | interface ITempLoginState { 11 | userList: Array 12 | } 13 | 14 | interface ITempLoginDispatch { 15 | onUserSelect: (userInfo: IUserInfo) => Action; 16 | onModalClose: () => Action 17 | } 18 | 19 | interface ITempLoginProps extends ITempLoginState, ITempLoginDispatch {} 20 | 21 | class TempLoginDOM extends React.Component { 22 | public render () { 23 | const userList = this.props.userList; 24 | return ( 25 |
26 | {userList.map((userInfo: IUserInfo, index: number) => { 27 | return ( 28 |
this.handleUserClick(userInfo)}>{userInfo.name} ({userInfo.email})
29 | ) 30 | })} 31 |
32 | ); 33 | } 34 | 35 | private handleUserClick = (userInfo: IUserInfo) => { 36 | this.props.onUserSelect(userInfo) 37 | this.props.onModalClose() 38 | } 39 | } 40 | 41 | const mapState = (state: IState): ITempLoginState => ({ 42 | userList: Object.values(state.users.allUsers) 43 | }) 44 | 45 | const mapDispatch = (dispatch: Dispatch): ITempLoginDispatch => ({ 46 | onUserSelect: (userInfo: IUserInfo) => dispatch(actionAuthSignInRequest(userInfo)), 47 | onModalClose: () => dispatch(actionModalCloseAll()) 48 | }) 49 | 50 | export const TempLogin = connect(mapState, mapDispatch)(TempLoginDOM) 51 | 52 | -------------------------------------------------------------------------------- /app/src/features/auth/actionAuth.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | import { IUserInfo } from "../../types/IUser"; 3 | 4 | 5 | export enum TypeActionAuth { 6 | SIGN_IN_REQUEST = "Auth > SignIn > Request", 7 | SIGN_IN_RESPONSE = "Auth > SignIn > Response", 8 | SIGN_IN_COMPLETE = "Auth > SignIn > Complete", 9 | SIGN_IN_ERROR = "Auth > SignIn > Error", 10 | 11 | SOCKET_STATUS = "Auth > Socket > Status" 12 | } 13 | 14 | export interface IActionAuth extends Action{ 15 | socketStatus?: boolean; 16 | userInfo?: IUserInfo; 17 | type: TypeActionAuth; 18 | } 19 | 20 | export const actionAuthSignInRequest = (userInfo: IUserInfo):IActionAuth => ({ 21 | userInfo, 22 | type: TypeActionAuth.SIGN_IN_REQUEST 23 | }); 24 | 25 | export const actionAuthSignInResponse = (userInfo: IUserInfo):IActionAuth => ({ 26 | userInfo, 27 | type: TypeActionAuth.SIGN_IN_RESPONSE 28 | }); 29 | 30 | export const actionAuthSignInComplete = ():IActionAuth => ({ 31 | type: TypeActionAuth.SIGN_IN_COMPLETE 32 | }); 33 | 34 | export const actionAuthSignInError = ():IActionAuth => ({ 35 | type: TypeActionAuth.SIGN_IN_ERROR 36 | }); 37 | 38 | export const actionAuthSetSocketStatus = (status: boolean):IActionAuth => ({ 39 | socketStatus: status, 40 | type: TypeActionAuth.SOCKET_STATUS 41 | }); -------------------------------------------------------------------------------- /app/src/features/auth/epic/epicAuthListUsers.ts: -------------------------------------------------------------------------------- 1 | import {Epic, ofType} from "redux-observable"; 2 | import {Action} from "redux"; 3 | import {Observable, of} from "rxjs"; 4 | import {switchMap} from "rxjs/operators"; 5 | import { IState } from "../../../setup/IState"; 6 | import { TypeActionUsers } from "../../users/actionUsers"; 7 | import { actionModalOpen } from "../../modal/actionModal"; 8 | import { IdModalRegistry } from "../../modal/IdModalRegistry"; 9 | 10 | export const epicAuthListUsers: Epic = (action$, state$): Observable => 11 | action$.pipe( 12 | ofType(TypeActionUsers.UserListComplete), 13 | switchMap( () => { 14 | return of(actionModalOpen(IdModalRegistry.ModaltempLogin)) 15 | }) 16 | ) -------------------------------------------------------------------------------- /app/src/features/auth/epic/epicAuthSocketSignIn.ts: -------------------------------------------------------------------------------- 1 | import {Epic, ofType} from "redux-observable"; 2 | import {Action} from "redux"; 3 | import {Observable, from} from "rxjs"; 4 | import {switchMap} from "rxjs/operators"; 5 | import { IState } from "../../../setup/IState"; 6 | import { TypeActionAuth, actionAuthSignInComplete } from "../actionAuth"; 7 | import { IUserInfo } from "../../../types/IUser"; 8 | import { $conn } from "../../../App"; 9 | import {IChannelInfo} from "../../channels/IChannelInfo"; 10 | import {actionActiveChannelUpdate} from "../../activity/actionActive"; 11 | 12 | export const epicAuthSocketSignIn: Epic = (action$, state$): Observable => 13 | action$.pipe( 14 | ofType(TypeActionAuth.SIGN_IN_RESPONSE), 15 | switchMap( () => { 16 | const loggedInUser: IUserInfo | undefined = state$.value.auth.userInfo; 17 | const state = state$.value; 18 | 19 | if(loggedInUser) { 20 | $conn.socket.emit("sign-in", loggedInUser); 21 | } 22 | 23 | const actions: Array = [] 24 | const defaultChannel = Object.values(state.channels.channels).find((channel: IChannelInfo) => channel.isDefault === true); 25 | 26 | if( defaultChannel ) { 27 | actions.push(actionActiveChannelUpdate(defaultChannel)) 28 | } 29 | 30 | actions.push(actionAuthSignInComplete()); 31 | 32 | return from(actions) 33 | }) 34 | ); -------------------------------------------------------------------------------- /app/src/features/auth/epic/epicCompanyUsers.ts: -------------------------------------------------------------------------------- 1 | import { Epic, ofType } from "redux-observable"; 2 | import { Action } from "redux"; 3 | import { mergeMap, switchMap, catchError } from "rxjs/operators"; 4 | import {Observable, of, from} from "rxjs"; 5 | import axios, {AxiosError, AxiosResponse} from 'axios'; 6 | import { IState } from "../../../setup/IState"; 7 | import { API } from "../../../api/api"; 8 | import { actionAuthSignInResponse, IActionAuth, TypeActionAuth, actionAuthSignInError } from "../actionAuth"; 9 | import { actionUsersResponse } from "../../users/actionUsers"; 10 | import {actionAddChannels} from "../../channels/actionChannel"; 11 | 12 | export const epicCompanyUsers: Epic = (action$, state$): Observable => 13 | action$.pipe( 14 | ofType(TypeActionAuth.SIGN_IN_REQUEST), 15 | mergeMap((action: IActionAuth) => { 16 | const {userInfo} = action; 17 | const {url} = API.GetCompanyUsers; 18 | return from(axios({ 19 | method: "GET", 20 | url: url + userInfo!.companyId 21 | })).pipe( 22 | switchMap((response: AxiosResponse) => { 23 | return from([ 24 | actionUsersResponse(response.data.data), 25 | actionAddChannels([{key: 'channel'+userInfo!.companyId, name: userInfo!.companyName!, isDefault: true}]), 26 | actionAuthSignInResponse(userInfo!) 27 | ]); 28 | }), 29 | catchError((error: AxiosError) => { 30 | console.log('caught error:', error.response); 31 | return of(actionAuthSignInError); 32 | }) 33 | ) 34 | }) 35 | ); -------------------------------------------------------------------------------- /app/src/features/auth/epicAuth.ts: -------------------------------------------------------------------------------- 1 | import { Epic, combineEpics } from "redux-observable"; 2 | import { Action } from "redux"; 3 | import { IState } from "../../setup/IState"; 4 | import { epicAuthListUsers } from "./epic/epicAuthListUsers"; 5 | import { epicAuthSocketSignIn } from "./epic/epicAuthSocketSignIn"; 6 | import { epicCompanyUsers } from "./epic/epicCompanyUsers"; 7 | 8 | export const epicLogin: Epic = combineEpics( 9 | epicAuthListUsers, 10 | epicAuthSocketSignIn, 11 | epicCompanyUsers, 12 | ) -------------------------------------------------------------------------------- /app/src/features/auth/reducerAuth.ts: -------------------------------------------------------------------------------- 1 | import { IUserInfo } from "../../types/IUser"; 2 | import { IActionAuth, TypeActionAuth } from "./actionAuth"; 3 | 4 | export interface IReducerAuth { 5 | userInfo?: IUserInfo, 6 | live: boolean, 7 | isLoggedIn: boolean, 8 | } 9 | 10 | const initial: IReducerAuth = { 11 | live: false, 12 | isLoggedIn: false 13 | } 14 | 15 | export default (state: IReducerAuth = initial, action: IActionAuth): IReducerAuth => { 16 | switch(action.type) { 17 | case TypeActionAuth.SIGN_IN_RESPONSE: 18 | return { 19 | ...state, 20 | userInfo: action.userInfo!, 21 | isLoggedIn: true 22 | } 23 | case TypeActionAuth.SOCKET_STATUS: 24 | return { 25 | ...state, 26 | live: action.socketStatus || false 27 | } 28 | default: 29 | return state 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/features/auth/tempAuth.scss: -------------------------------------------------------------------------------- 1 | .authContainer { 2 | .userChoose{ 3 | padding: 10xp; 4 | cursor: pointer; 5 | margin: 3px; 6 | 7 | &:hover{ 8 | background: silver; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/features/channels/Channels.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {IState} from '../../setup/IState'; 3 | import {Action, Dispatch} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | import {IChannelInfo} from "./IChannelInfo"; 6 | import {IdActiveType} from "../activity/IdActiveType"; 7 | import {actionActiveChannelUpdate} from "../activity/actionActive"; 8 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; 9 | import {faPlus} from "@fortawesome/free-solid-svg-icons"; 10 | import {ScrollSection} from "../generic/scrollWrapper/ScrollSection"; 11 | import "./channels.scss"; 12 | import {IReducerActive} from "../activity/reducerActive"; 13 | import {actionChannelPendingReset} from "./actionChannel"; 14 | 15 | interface IChannelsState { 16 | active: IReducerActive; 17 | channels: Array; 18 | } 19 | 20 | interface IChannelsDispatch { 21 | onSetActiveContext: (activeInfo: IChannelInfo) => Action; 22 | onResetPendingMsg: (channelKey: string) => Action; 23 | } 24 | 25 | interface IChannelsProps extends IChannelsState, IChannelsDispatch { 26 | handleChannelMore: any; 27 | peopleMoreOpen: boolean; 28 | channelMoreOpen: boolean; 29 | deviceVariant: string; 30 | } 31 | 32 | class ChannelsDOM extends React.Component { 33 | public componentDidMount(): void { 34 | } 35 | 36 | public render(): React.ReactNode { 37 | const {channels, active} = this.props; 38 | const {type, selectedChannel} = active; 39 | const channelsObj = { 40 | important: channels, 41 | more: [] 42 | }; 43 | return ( 44 | <> 45 |
46 | 47 | 48 |
49 | 50 |
51 | {channelsObj.important.map((channel: IChannelInfo, index: number) => { 52 | const {key, name, pendingMessageCount} = channel; 53 | const isSelectedClass = type === IdActiveType.Channel && selectedChannel && selectedChannel.key === key ? 'selected' : ''; 54 | 55 | return ( 56 |
this.onChangeActiveToChannel(channel)}> 57 |
#
58 |
{this.getLengthFilteredName(name)}
59 | {(pendingMessageCount && pendingMessageCount > 0) ? 60 |
{pendingMessageCount === 1 ? 'New': pendingMessageCount}
61 | : null 62 | } 63 |
64 | ) 65 | } 66 | )} 67 |
68 | {this.props.channelMoreOpen && !this.props.peopleMoreOpen && channelsObj.more.map((channel: IChannelInfo, index: number) => { 69 | const {key, name, pendingMessageCount} = channel; 70 | const isSelectedClass = type === IdActiveType.Channel && selectedChannel && selectedChannel.key === key ? 'selected' : ''; 71 | return ( 72 |
this.onChangeActiveToChannel(channel)}> 73 |
#
74 |
{this.getLengthFilteredName(name)}
75 | {(pendingMessageCount && pendingMessageCount > 0) ? 76 |
{pendingMessageCount === 1 ? 'New': pendingMessageCount}
77 | : null 78 | } 79 |
80 | ) 81 | } 82 | )} 83 |
84 | {channelsObj.more.length > 0 && {this.props.channelMoreOpen ? 'Less' : 'More'}} 85 | 86 | ) 87 | }; 88 | 89 | private getLengthFilteredName = (name: string): string => { 90 | if (this.props.deviceVariant !== 'LaptopSmall') { 91 | return name; 92 | } 93 | 94 | return name.length > 18 ? name.substr(0, 18) + '...' : name; 95 | }; 96 | 97 | private onChangeActiveToChannel = (channel: IChannelInfo) => { 98 | const {onSetActiveContext, onResetPendingMsg} = this.props; 99 | onSetActiveContext(channel); 100 | onResetPendingMsg(channel.key); 101 | }; 102 | } 103 | 104 | const mapState = (state: IState): IChannelsState => ({ 105 | active: state.active, 106 | channels: Object.values(state.channels.channels) 107 | }); 108 | const mapDispatch = (dispatch: Dispatch): IChannelsDispatch => ({ 109 | onSetActiveContext: (activeInfo: IChannelInfo) => dispatch(actionActiveChannelUpdate(activeInfo, IdActiveType.Channel)), 110 | onResetPendingMsg: (channelKey: string) => dispatch(actionChannelPendingReset(channelKey)) 111 | }); 112 | 113 | export default connect(mapState, mapDispatch)(ChannelsDOM); 114 | -------------------------------------------------------------------------------- /app/src/features/channels/IChannelInfo.ts: -------------------------------------------------------------------------------- 1 | export interface IChannelInfo { 2 | key: string; 3 | name: string; 4 | pendingMessageCount?: number; 5 | isDefault?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/features/channels/actionChannel.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "redux"; 2 | import {IChannelInfo} from "./IChannelInfo"; 3 | 4 | export enum TypeActionChannel { 5 | ChannelsRequest = "Channels > Request", 6 | ChannelsResponse = "Channels > Response", 7 | ChannelPendingMsgUpdate = "Channels > Pending_msg > Update", 8 | ChannelPendingMsgReset = "Channels > Pending_msg > Reset" 9 | } 10 | 11 | export interface IActionChannel extends Action{ 12 | channelList?: Array; 13 | channelKey?: string; 14 | type: TypeActionChannel; 15 | } 16 | 17 | export const actionAddChannels = (channelList: Array): IActionChannel => ({ 18 | channelList, 19 | type: TypeActionChannel.ChannelsResponse 20 | }); 21 | 22 | export const actionChannelPendingUpdate = (channelKey: string): IActionChannel => ({ 23 | channelKey, 24 | type: TypeActionChannel.ChannelPendingMsgUpdate 25 | }); 26 | 27 | export const actionChannelPendingReset = (channelKey: string): IActionChannel => ({ 28 | channelKey, 29 | type: TypeActionChannel.ChannelPendingMsgReset 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /app/src/features/channels/channels.scss: -------------------------------------------------------------------------------- 1 | .sideNav-section-important-channel{ 2 | min-height: 50px; 3 | max-height: 243px; 4 | } 5 | .selected { 6 | font-weight: 700; 7 | } -------------------------------------------------------------------------------- /app/src/features/channels/reducerChannel.ts: -------------------------------------------------------------------------------- 1 | import {IChannelInfo} from "./IChannelInfo"; 2 | import {IActionChannel, TypeActionChannel} from "./actionChannel"; 3 | 4 | export interface IAllChannels { 5 | [key: string]: IChannelInfo 6 | } 7 | export interface IReducerChannel { 8 | channels: IAllChannels 9 | } 10 | 11 | const initial: IReducerChannel = { 12 | channels: {} 13 | }; 14 | 15 | export default (state: IReducerChannel = initial, action: IActionChannel) => { 16 | switch(action.type) { 17 | case TypeActionChannel.ChannelsResponse: 18 | return reducerAddChannels(state, action); 19 | case TypeActionChannel.ChannelPendingMsgUpdate: 20 | return reducerUpdatePending(state, action); 21 | case TypeActionChannel.ChannelPendingMsgReset: 22 | return reducerUpdatePending(state, action, true); 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | const reducerAddChannels = (state: IReducerChannel, {channelList}: IActionChannel): IReducerChannel => { 29 | const channelObj: IAllChannels = {}; 30 | channelList!.forEach((channel: IChannelInfo) => channelObj[channel.key] = channel); 31 | 32 | return { 33 | ...state, 34 | channels: channelObj 35 | } 36 | }; 37 | 38 | const reducerUpdatePending = (state: IReducerChannel, {channelKey}: IActionChannel, reset: boolean = false): IReducerChannel => { 39 | if(!channelKey || !state.channels[channelKey]) return state; 40 | 41 | const channels = {...state.channels}; 42 | if(reset) { 43 | channels[channelKey].pendingMessageCount = 0; 44 | } else { 45 | const pendingCount = channels[channelKey].pendingMessageCount || 0; 46 | channels[channelKey].pendingMessageCount = pendingCount + 1; 47 | } 48 | 49 | return { 50 | ...state, 51 | channels, 52 | } 53 | }; -------------------------------------------------------------------------------- /app/src/features/chatBox/ChatBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IState} from '../../setup/IState'; 3 | import {Dispatch} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | import "./chatBox.scss"; 6 | import {IReducerActive} from '../activity/reducerActive'; 7 | import {IReducerConversation} from '../conversation/reducerConversation'; 8 | import {IAllUsers} from '../users/reducerUsers'; 9 | import {IdActiveType} from '../activity/IdActiveType'; 10 | import {IMessageInfo} from '../../types/IMessage'; 11 | import {MessageBubble} from "../generic/messageBubble/MessageBubble"; 12 | 13 | interface IChatBoxState { 14 | active: IReducerActive; 15 | conversation: IReducerConversation; 16 | allUsers: IAllUsers; 17 | } 18 | interface IChatBoxDispatch {} 19 | interface IChatBoxProps extends IChatBoxState, IChatBoxDispatch {} 20 | 21 | class ChatBoxDOM extends React.Component { 22 | render (){ 23 | const conversation: Array = this.getConversation() || []; 24 | 25 | return( 26 |
27 | {conversation.map((messageInfo: IMessageInfo, index: number) => { 28 | return ( 29 |
30 | 31 |
32 | ) 33 | })} 34 |
35 | ) 36 | } 37 | 38 | private getConversation = () => { 39 | const {active, conversation, allUsers} = this.props; 40 | 41 | if(!active || !conversation || !allUsers) { 42 | return [] 43 | } 44 | 45 | if(active.type === IdActiveType.Individual) { 46 | return conversation.user[active.selectedUser!.id] 47 | } else if (active.type === IdActiveType.Channel) { 48 | return conversation.channel[active.selectedChannel!.key] 49 | } 50 | 51 | return [] 52 | } 53 | } 54 | 55 | const mapState = (state: IState): IChatBoxState => ({ 56 | active: state.active, 57 | conversation: state.conversation, 58 | allUsers: state.users.allUsers 59 | }); 60 | const mapDispatch = (state: Dispatch): IChatBoxDispatch => ({}); 61 | 62 | export const ChatBox = connect(mapState, mapDispatch)(ChatBoxDOM); 63 | -------------------------------------------------------------------------------- /app/src/features/chatBox/chatBox.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiddharthaChowdhury/react-chat-app/888fad6be54949aaca1d904e116e0556ea38c63f/app/src/features/chatBox/chatBox.scss -------------------------------------------------------------------------------- /app/src/features/conversation/actionConverstion.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "redux"; 2 | import { IMessageInfo } from "../../types/IMessage"; 3 | 4 | export enum TypeActionConversation { 5 | RECEIVE = "Conversation > Message > Receive", 6 | SEND = "Conversation > Message > Send", 7 | 8 | END = "Message > Send > Initiated" 9 | } 10 | 11 | export interface IActionConversation extends Action{ 12 | messageInfo?: IMessageInfo; 13 | messageIndex?: number | string; 14 | type: TypeActionConversation 15 | } 16 | 17 | export const actionMessageSend = (messageInfo: IMessageInfo, toId: number | string): IActionConversation => ({ 18 | messageInfo, 19 | messageIndex: toId, 20 | type: TypeActionConversation.SEND 21 | }); 22 | 23 | export const actionMessageReceive = (messageInfo: IMessageInfo, fromId: number | string): IActionConversation => ({ 24 | messageInfo, 25 | messageIndex: fromId, 26 | type: TypeActionConversation.RECEIVE 27 | }); 28 | 29 | export const actionMessageEnd = (): IActionConversation => ({ 30 | type: TypeActionConversation.END 31 | }) -------------------------------------------------------------------------------- /app/src/features/conversation/epicConversation.ts: -------------------------------------------------------------------------------- 1 | import {combineEpics} from "redux-observable"; 2 | import { epicMessageSent } from "./epics/epicMessageSent"; 3 | 4 | export const epicConversation = combineEpics( 5 | epicMessageSent 6 | ); -------------------------------------------------------------------------------- /app/src/features/conversation/epics/epicMessageSent.ts: -------------------------------------------------------------------------------- 1 | import {Epic, ofType} from "redux-observable"; 2 | import {Action} from "redux"; 3 | import {Observable, of} from "rxjs"; 4 | import {switchMap} from 'rxjs/operators'; 5 | import {IState} from "../../../setup/IState"; 6 | import {actionMessageEnd, IActionConversation, TypeActionConversation} from "../actionConverstion"; 7 | import {$conn} from "../../../App"; 8 | import {IdMessageSource} from "../../../types/IMessage"; 9 | 10 | 11 | export const epicMessageSent: Epic = (action$, state$): Observable => 12 | action$.pipe( 13 | ofType(TypeActionConversation.SEND), 14 | switchMap((action: IActionConversation) => { 15 | const {messageInfo} = action; 16 | 17 | const messagePayload = {...messageInfo!}; 18 | if (messageInfo!.source === IdMessageSource.User_Message) { 19 | messagePayload.selfEcho = true; 20 | $conn.socket.emit("message", {...messagePayload}); 21 | } else { 22 | $conn.socket.emit("channel-broadcast", {...messagePayload}); 23 | } 24 | 25 | return of(actionMessageEnd()) 26 | }) 27 | ); -------------------------------------------------------------------------------- /app/src/features/conversation/reducerConversation.ts: -------------------------------------------------------------------------------- 1 | import { IMessageInfo, IdMessageSource } from "../../types/IMessage"; 2 | import { IActionConversation, TypeActionConversation } from "./actionConverstion"; 3 | 4 | interface IReducerSourceConversation { 5 | [id: string]: Array 6 | } 7 | 8 | export interface IReducerConversation { 9 | user: IReducerSourceConversation; 10 | channel: IReducerSourceConversation; 11 | } 12 | 13 | const initialConversationState = { 14 | user: {}, 15 | channel: {} 16 | }; 17 | 18 | export default (state: IReducerConversation = initialConversationState, action: IActionConversation): IReducerConversation => { 19 | switch (action.type) { 20 | case TypeActionConversation.RECEIVE: 21 | return reducerConversationUpdate(state, action); 22 | default: 23 | return state; 24 | } 25 | } 26 | 27 | const reducerConversationUpdate = (state: IReducerConversation, {messageInfo, messageIndex}: IActionConversation): IReducerConversation => { 28 | let userConv = state.user; 29 | let channelConv = state.channel; 30 | 31 | if(messageInfo!.source === IdMessageSource.Channel_Message) { 32 | if(messageIndex! in channelConv) { 33 | channelConv[messageIndex!].push(messageInfo!) 34 | } else { 35 | channelConv[messageIndex!] = [messageInfo!] 36 | } 37 | } 38 | 39 | if(messageInfo!.source === IdMessageSource.User_Message) { 40 | if(messageIndex! in userConv) { 41 | userConv[messageIndex!] = [...userConv[messageIndex!], messageInfo!] 42 | } else { 43 | userConv[messageIndex!] = [messageInfo!] 44 | } 45 | } 46 | 47 | return { 48 | ...state, 49 | user: {...userConv}, 50 | channel: {...channelConv} 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/features/generic/messageBubble/MessageBubble.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Dispatch} from 'redux'; 3 | import {connect} from "react-redux"; 4 | import {IState} from "../../../setup/IState"; 5 | import {IMessageInfo} from "../../../types/IMessage"; 6 | import {IAllUsers} from "../../users/reducerUsers"; 7 | import "./messageBubble.scss"; 8 | 9 | interface IMessageBubbleState { 10 | allUsers: IAllUsers; 11 | } 12 | 13 | interface IMessageBubbleDispatch { 14 | } 15 | 16 | interface IMessageBubbleProps extends IMessageBubbleState, IMessageBubbleDispatch { 17 | messageInfo: IMessageInfo 18 | } 19 | 20 | class MessageBubbleDOM extends React.Component { 21 | public render () { 22 | const {allUsers, messageInfo:{fromId, message, createdAt }} = this.props; 23 | const user = allUsers[fromId]; 24 | const timeStamp = new Date(createdAt).toLocaleTimeString(); 25 | const style: React.CSSProperties = { 26 | backgroundImage: `url(${user.avatar})`, 27 | backgroundSize: 'cover', 28 | backgroundRepeat: 'no-repeat', 29 | backgroundPosition: 'center, center' 30 | }; 31 | 32 | return ( 33 |
34 |
35 |
36 |
37 |
{user.name}
38 |
{timeStamp}
39 |
40 |
41 | {message.text} 42 |
43 |
44 |
45 | ) 46 | } 47 | }; 48 | 49 | const mapState = (state: IState): IMessageBubbleState => ({ 50 | allUsers: state.users.allUsers 51 | }) 52 | const mapDispatch = (dispatch: Dispatch): IMessageBubbleDispatch => ({}) 53 | 54 | export const MessageBubble = connect(mapState, mapDispatch)(MessageBubbleDOM); 55 | -------------------------------------------------------------------------------- /app/src/features/generic/messageBubble/messageBubble.scss: -------------------------------------------------------------------------------- 1 | .message-wrap{ 2 | width: 100%; 3 | display: flex; 4 | flex-direction: row; 5 | margin: 10px; 6 | 7 | .message-icon{ 8 | width: 50px; 9 | height: 50px; 10 | border: 1px solid #f1f1f1; 11 | border-radius: 3px; 12 | } 13 | 14 | .message-content{ 15 | margin-left: 5px; 16 | width: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | 20 | .message-content-name{ 21 | display: flex; 22 | align-items: center; 23 | 24 | .name { 25 | font-weight: 700; 26 | } 27 | .time { 28 | margin-left: 10px; 29 | } 30 | } 31 | 32 | .message-content-text { 33 | white-space: pre-line; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/features/generic/popOver/PopOver.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Dispatch} from 'redux'; 3 | import {connect} from "react-redux"; 4 | import {Popover} from "@material-ui/core"; 5 | import {IState} from "../../../setup/IState"; 6 | 7 | interface IPopOverState { 8 | } 9 | 10 | interface IPopOverDispatch { 11 | } 12 | 13 | interface IPopOverProps extends IPopOverState, IPopOverDispatch { 14 | isOpen: boolean 15 | } 16 | 17 | const _PopOver: React.FC = (props) => { 18 | return ( 19 | 30 | {props.children} 31 | 32 | ) 33 | }; 34 | 35 | const mapState = (state: IState): IPopOverState => ({}); 36 | const mapDispatch = (dispatch: Dispatch): IPopOverDispatch => ({}); 37 | 38 | export const PopOver = connect(mapState, mapDispatch)(_PopOver); 39 | -------------------------------------------------------------------------------- /app/src/features/generic/scrollWrapper/ScrollSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ScrollBars from 'react-scrollbar'; 3 | 4 | interface IScrollSectionProps { 5 | [key: string]: any 6 | } 7 | 8 | export const ScrollSection: React.FC = ({children, ...rest}) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | }; 15 | -------------------------------------------------------------------------------- /app/src/features/modal/IdModalContent.ts: -------------------------------------------------------------------------------- 1 | import { IdModalRegistry } from "./IdModalRegistry"; 2 | import { TempLogin } from "../auth/TempLogin"; 3 | 4 | export type IdModalContentType = {[id in IdModalRegistry]: any} 5 | 6 | export const IdModalContent: IdModalContentType = { 7 | [IdModalRegistry.ModaltempLogin] : TempLogin 8 | } 9 | -------------------------------------------------------------------------------- /app/src/features/modal/IdModalRegistry.ts: -------------------------------------------------------------------------------- 1 | export enum IdModalRegistry { 2 | ModaltempLogin = 'ModaltempLogin', 3 | } -------------------------------------------------------------------------------- /app/src/features/modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { IdModalRegistry } from './IdModalRegistry'; 3 | import { Action, Dispatch } from 'redux'; 4 | import { IState } from '../../setup/IState'; 5 | import { actionModalClose } from './actionModal'; 6 | import { connect } from 'react-redux'; 7 | import { IdModalContent } from './IdModalContent'; 8 | import './modal.scss'; 9 | 10 | interface IDialogState { 11 | activeModals: Array 12 | } 13 | 14 | interface IDialogDispatch { 15 | onClose: (modalId: IdModalRegistry) => Action 16 | } 17 | 18 | interface IDialogProps extends IDialogState, IDialogDispatch { 19 | } 20 | 21 | class ModalDOM extends React.Component { 22 | 23 | public render = () => { 24 | const dialogs: Array = this.props.activeModals 25 | 26 | if (dialogs.length > 0) { 27 | return ( 28 |
29 | { 30 | dialogs.map((dialog: IdModalRegistry, dialogKey: number) => { 31 | if (!dialog) { 32 | return null 33 | } 34 | 35 | const ModalContent = IdModalContent[dialog] 36 | 37 | return ( 38 |
39 | × 40 |
41 | 42 |
43 |
44 | ) 45 | }) 46 | } 47 |
48 | ) 49 | } 50 | 51 | return null 52 | } 53 | } 54 | 55 | const mapState = (state: IState): IDialogState => ({ 56 | activeModals: state.modal.activeModals 57 | }) 58 | 59 | const mapDispatch = (dispatch: Dispatch): IDialogDispatch => ({ 60 | onClose: (modalId: IdModalRegistry) => dispatch(actionModalClose(modalId)) 61 | }) 62 | 63 | export default connect(mapState, mapDispatch)(ModalDOM) 64 | -------------------------------------------------------------------------------- /app/src/features/modal/actionModal.ts: -------------------------------------------------------------------------------- 1 | import { IdModalRegistry } from './IdModalRegistry' 2 | import { Action } from 'redux' 3 | 4 | export enum TypeActionModal { 5 | Open = 'Modal > Open', 6 | Close = 'Modal > Close', 7 | CloseCurrent = 'Modal > Close > Current', 8 | CloseAll = 'Modal > Close > All' 9 | } 10 | 11 | export interface IActionModal extends Action { 12 | modalId?: IdModalRegistry 13 | type: TypeActionModal 14 | } 15 | 16 | export const actionModalOpen = (modalId: IdModalRegistry): IActionModal => ({ 17 | modalId, 18 | type: TypeActionModal.Open 19 | }) 20 | 21 | export const actionModalClose = (modalId: IdModalRegistry): IActionModal => ({ 22 | modalId, 23 | type: TypeActionModal.Close 24 | }) 25 | 26 | export const actionModalCloseCurrent = (): IActionModal => ({ 27 | type: TypeActionModal.CloseCurrent 28 | }) 29 | 30 | export const actionModalCloseAll = (): IActionModal => ({ 31 | type: TypeActionModal.CloseAll 32 | }) 33 | -------------------------------------------------------------------------------- /app/src/features/modal/modal.scss: -------------------------------------------------------------------------------- 1 | /* The Modal (background) */ 2 | .modal { 3 | position: fixed; /* Stay in place */ 4 | z-index: 1; /* Sit on top */ 5 | left: 0; 6 | top: 0; 7 | width: 100%; /* Full width */ 8 | height: 100%; /* Full height */ 9 | overflow: auto; /* Enable scroll if needed */ 10 | background-color: rgb(0,0,0); /* Fallback color */ 11 | background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ 12 | } 13 | 14 | /* Modal Content/Box */ 15 | .modal-content { 16 | background-color: #fefefe; 17 | margin: 15% auto; /* 15% from the top and centered */ 18 | padding: 20px; 19 | border: 1px solid #888; 20 | width: 50%; /* Could be more or less, depending on screen size */ 21 | } 22 | 23 | /* The Close Button */ 24 | .close { 25 | color: #aaa; 26 | float: right; 27 | font-size: 28px; 28 | font-weight: bold; 29 | } 30 | 31 | .close:hover, 32 | .close:focus { 33 | color: black; 34 | text-decoration: none; 35 | cursor: pointer; 36 | } -------------------------------------------------------------------------------- /app/src/features/modal/reducerModal.ts: -------------------------------------------------------------------------------- 1 | import { IdModalRegistry } from './IdModalRegistry' 2 | import { IActionModal, TypeActionModal } from './actionModal' 3 | 4 | export interface IReducerModal { 5 | activeModals: Array 6 | } 7 | 8 | export const initialModalState: IReducerModal = { 9 | activeModals: [] 10 | } 11 | 12 | export function reducerModal (state: IReducerModal = initialModalState, action: IActionModal): IReducerModal { 13 | switch (action.type) { 14 | case TypeActionModal.Open: 15 | return reducerModalOpen(state, action.modalId!) 16 | case TypeActionModal.CloseCurrent: 17 | return reducerModalCloseCurrent(state) 18 | case TypeActionModal.Close: 19 | return reducerActionModalClose(state, action.modalId!) 20 | case TypeActionModal.CloseAll: 21 | return reducerActionModalCloseAll(state) 22 | default: 23 | return state 24 | } 25 | } 26 | 27 | const reducerModalOpen = (state: IReducerModal, modalId: IdModalRegistry): IReducerModal => ({ 28 | ...state, 29 | activeModals: [modalId, ...state.activeModals] 30 | }) 31 | 32 | const reducerModalCloseCurrent = (state: IReducerModal): IReducerModal => { 33 | const shiftedActiveModalArray: Array = [...state.activeModals] 34 | shiftedActiveModalArray.shift() 35 | 36 | return { 37 | ...state, 38 | activeModals: shiftedActiveModalArray 39 | } 40 | } 41 | 42 | const reducerActionModalClose = (state: IReducerModal, modalId: IdModalRegistry): IReducerModal => { 43 | const index: number = state.activeModals.indexOf(modalId) 44 | 45 | if (index > -1) { 46 | state.activeModals.splice(index, 1) 47 | } 48 | 49 | return { 50 | ...state, 51 | activeModals: [...state.activeModals] 52 | } 53 | } 54 | 55 | const reducerActionModalCloseAll = (state: IReducerModal): IReducerModal => ({ 56 | ...state, 57 | activeModals: [] 58 | }) 59 | -------------------------------------------------------------------------------- /app/src/features/navBot/BotNav.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IState } from '../../setup/IState'; 3 | import {Action, Dispatch} from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import "./botNav.scss"; 6 | import {IUserInfo} from "../../types/IUser"; 7 | import {IReducerActive} from "../activity/reducerActive"; 8 | import {IdMessageSource, IMessageInfo} from "../../types/IMessage"; 9 | import {IdActiveType} from "../activity/IdActiveType"; 10 | import {actionMessageSend} from "../conversation/actionConverstion"; 11 | import {isMobileDevice} from "typed-responsive-react"; 12 | import {Grid} from "@material-ui/core"; 13 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; 14 | import {faEllipsisH} from "@fortawesome/free-solid-svg-icons"; 15 | import {Camera, Send, TagFaces} from "@material-ui/icons"; 16 | 17 | interface IBotNavState { 18 | loggedInUser: IUserInfo; 19 | active: IReducerActive; 20 | } 21 | interface IBotNavDispatch { 22 | onSendMessage: (messageInfo: IMessageInfo, toId: number | string) => Action 23 | } 24 | interface IBotNavProps extends IBotNavState, IBotNavDispatch { 25 | deviceVariant: string; 26 | } 27 | interface IChatInputOwnState { 28 | [id: string]: string 29 | } 30 | 31 | 32 | class BotNavDOM extends React.Component { 33 | public readonly state: IChatInputOwnState = { '0' : ''}; 34 | 35 | render () { 36 | const {selectedUser, selectedChannel, type} = this.props.active; 37 | const textArea: React.CSSProperties = { width: this.props.deviceVariant === 'LaptopSmall' || isMobileDevice() ? '70%' : '77%'}; 38 | const stateIndex = selectedUser ? selectedUser!.id.toString() : selectedChannel!.key + '' + type as string; 39 | 40 | return ( 41 | 42 |
43 | <> 44 |
45 |
46 |
47 |
48 |
49 |
50 |
this.handleMessageSend(stateIndex)} >
51 |
52 |