├── .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 |
48 |
49 |
50 |
this.handleMessageSend(stateIndex)} >
51 |
52 |
62 |
63 | )
64 | }
65 |
66 | private handleMessageInputChange = (e: any, stateIndex: string) => {
67 | this.setState({[stateIndex]: e.target.value})
68 | };
69 |
70 | private handleMessageSend = (stateIndex: string) => {
71 | const {active: {selectedUser, type, selectedChannel}, loggedInUser, onSendMessage} = this.props;
72 |
73 | const messageText = this.state[stateIndex] ? this.state[stateIndex].trim() : undefined;
74 | if(!messageText) return;
75 |
76 | const messageInfo: IMessageInfo = {
77 | fromId: loggedInUser.id,
78 | message: {
79 | text: messageText
80 | },
81 | createdAt: + new Date(),
82 | source: IdMessageSource.User_Message
83 | };
84 |
85 | let toId = null;
86 | if (type === IdActiveType.Individual) {
87 | messageInfo.toId = selectedUser!.id;
88 | toId = selectedUser!.id
89 | } else if (type === IdActiveType.Channel) {
90 | messageInfo.channelId = selectedChannel!.key;
91 | messageInfo.source = IdMessageSource.Channel_Message;
92 | toId = selectedChannel!.key
93 | }
94 |
95 | onSendMessage(messageInfo, toId!);
96 | this.setState({[stateIndex]: ''});
97 | };
98 |
99 | private handleEnter = (evt: any, stateIndex: string) => {
100 | if (evt.keyCode === 13 && evt.shiftKey) {
101 | return;
102 | }
103 | if (evt.keyCode === 13 && !evt.shiftKey) {
104 | this.handleMessageSend(stateIndex);
105 | // this.setState({[stateIndex]: ''});
106 | return;
107 | }
108 | };
109 | }
110 |
111 | const mapState = (state: IState): IBotNavState => ({
112 | loggedInUser: state.auth.userInfo!,
113 | active: state.active!
114 | });
115 | const mapDispatch = (dispatch: Dispatch): IBotNavDispatch => ({
116 | onSendMessage: (messageInfo: IMessageInfo, toId: number | string) => dispatch(actionMessageSend(messageInfo, toId))
117 | });
118 |
119 | export const BotNav = connect(mapState, mapDispatch)(BotNavDOM);
120 |
--------------------------------------------------------------------------------
/app/src/features/navBot/botNav.scss:
--------------------------------------------------------------------------------
1 | .content-bot {
2 | height: 100px;
3 | border-top: 1px solid #f1f1f1;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 10px;
8 |
9 | .content-bot-wrapper{
10 | width: 100%;
11 | position: relative;
12 | height: 50px;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 |
17 | .options-left {
18 | position: absolute;
19 | height: 48px;
20 | width: 100px;
21 | left: 0;
22 | z-index: 10;
23 | display: flex;
24 | align-items: center;
25 | flex-direction: row-reverse;
26 | }
27 |
28 | .options-right {
29 | position: absolute;
30 | height: 48px;
31 | width: 100px;
32 | right: 0;
33 | z-index: 10;
34 | display: flex;
35 | align-items: center;
36 | }
37 |
38 | .textarea {
39 | position: absolute;
40 | margin-top: 2px;
41 | resize: none;
42 | overflow: hidden;
43 | padding: 7px 100px;
44 | border:none;
45 | outline: none;
46 | }
47 |
48 | .option-btn {
49 | width: 40px;
50 | height: 40px;
51 | margin: 0 3px;
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | cursor: pointer;
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/features/navSide/SideNav.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 "./sideNav.scss";
6 | import {
7 | getDeviceTypeInfo,
8 | } from 'typed-responsive-react';
9 | import {IUserInfo} from "../../types/IUser";
10 | import { Grid } from "@material-ui/core";
11 | import Channels from "../channels/Channels";
12 | import {UserList} from "../userList/UserList";
13 |
14 | interface ISideNavState {
15 | userInfo: IUserInfo
16 | }
17 | interface ISideNavDispatch {}
18 | interface ISideNavProps extends ISideNavState, ISideNavDispatch {}
19 |
20 | class SideNavDOM extends React.Component {
21 | state = {peopleMoreOpen: false, channelMoreOpen: false};
22 |
23 | render = () => {
24 | const {deviceTypeVariant} = getDeviceTypeInfo();
25 | const {userInfo: {companyName}, } = this.props;
26 | const shortCompanyName = companyName!.length > 28 ? companyName!.substr(0, 27) + '...' : companyName!;
27 | return (
28 |
29 |
30 |
31 | {shortCompanyName}
32 |
33 |
34 |
35 |
36 |
37 |
38 | {/* --- CHANNEL SECTION ----*/}
39 |
40 |
46 |
47 |
48 | {/* --- PEOPLE SECTION ----*/}
49 |
50 |
56 |
57 |
58 | )
59 | }
60 |
61 | private handlePeopleMore = (e: any) => {
62 | const newState: any = {peopleMoreOpen: !this.state.peopleMoreOpen};
63 | if (this.state.channelMoreOpen) {
64 | newState['channelMoreOpen'] = !this.state.channelMoreOpen;
65 | }
66 | this.setState({...newState});
67 | };
68 |
69 | private handleChannelMore = (e: any) => {
70 | const newState: any = {channelMoreOpen: !this.state.channelMoreOpen};
71 | if (this.state.peopleMoreOpen) {
72 | newState['peopleMoreOpen'] = !this.state.peopleMoreOpen;
73 | }
74 | this.setState({...newState});
75 | };
76 | }
77 |
78 | const mapState = (state: IState): ISideNavState => ({
79 | userInfo: state.auth.userInfo!
80 | });
81 | const mapDispatch = (state: Dispatch): ISideNavDispatch => ({})
82 |
83 | export const SideNav = connect(mapState, mapDispatch)(SideNavDOM);
84 |
--------------------------------------------------------------------------------
/app/src/features/navSide/sideNav.scss:
--------------------------------------------------------------------------------
1 | .sideNav {
2 | border-right: 1px solid #f1f1f1;
3 | width: 20%;
4 | padding-right: 10px;
5 | padding-left: 10px;
6 | font-size: 14px;
7 |
8 | .hide-this {
9 | display: none;
10 | }
11 | .icon-online{
12 | font-size: 8px;
13 | color: green;
14 | margin-top: 3px;
15 | }
16 | .icon-offline{
17 | font-size: 8px;
18 | color: lightgrey;
19 | margin-top: 3px;
20 | }
21 |
22 | .sideNav-section-heading{
23 | font-size: 14px;
24 | font-weight: bold;
25 | margin: 15px 0;
26 | color: dimgrey;
27 |
28 | .section-icon{
29 | font-weight: lighter;
30 | margin-left: 20px;
31 | cursor: pointer;
32 | color: lightgrey;
33 |
34 | &:hover {
35 | color: dimgrey;
36 | }
37 | }
38 | }
39 |
40 | .sideNav-header {
41 | height: 60px;
42 | display: flex;
43 | font-size: 16px;
44 | align-items: center;
45 | font-weight: 500;
46 | }
47 |
48 | .sideNav-search {
49 | margin: 0 0 20px 0;
50 |
51 | .search-box{
52 | width: 80%;
53 | border-style: none;
54 | background-color: #f1f1f1;
55 | outline: none;
56 | height: 40px;
57 | border-radius: 3px;
58 | padding: 0 10px;
59 | }
60 | }
61 |
62 | .sideNav-section{
63 | display: flex;
64 | flex-direction: column;
65 |
66 | .sideNav-sections {
67 | max-height: 243px;
68 | }
69 |
70 | .section-element {
71 | padding: 3px 0;
72 | white-space: nowrap;
73 | overflow: hidden;
74 | margin-left: 15px;
75 | width: 90%;
76 | display: flex;
77 | align-items: center;
78 | cursor: pointer;
79 |
80 | .section-icon{
81 | margin-right: 7px;
82 | }
83 |
84 | .new-message-tag {
85 | margin-left: 10px;
86 | font-size: 10px;
87 | color: #ffffff;
88 | background-color: #C61881;
89 | animation: tagAnimation 5s infinite;
90 | padding: 1px 5px;
91 | border-radius: 4px;
92 | }
93 | }
94 | .more {
95 | margin: 15px;
96 | cursor: pointer;
97 | }
98 | }
99 | }
100 |
101 | @-moz-keyframes tagAnimation /* Firefox */ {
102 | 0% {background:#C61881;}
103 | 50% {background:#C61818;}
104 | 100% {background:#C61881;}
105 | }
106 |
107 | @-webkit-keyframes tagAnimation /* Safari and Chrome */ {
108 | 0% {background:#C61881;}
109 | 50% {background:#C61818;}
110 | 100% {background:#C61881;}
111 | }
--------------------------------------------------------------------------------
/app/src/features/navTop/TopNav.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 "./topNav.scss";
6 | import {IUserInfo} from '../../types/IUser';
7 | import {Grid} from "@material-ui/core";
8 | import {IReducerActive} from "../activity/reducerActive";
9 | import {IdActiveType} from "../activity/IdActiveType";
10 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
11 | import {faCog} from "@fortawesome/free-solid-svg-icons";
12 |
13 | interface ITopNavState {
14 | loggedInUser: IUserInfo
15 | active: IReducerActive
16 | }
17 | interface ITopNavDispatch {}
18 | interface ITopNavProps extends ITopNavState, ITopNavDispatch {}
19 |
20 | class TopNavDOM extends React.Component {
21 | render () {
22 | const {loggedInUser, active:{type, selectedChannel, selectedUser}} = this.props;
23 | const [loggedInName] = loggedInUser.name.split(" ");
24 | let style: React.CSSProperties = {};
25 | if(type === IdActiveType.Individual) {
26 | style = {
27 | backgroundImage: `url(${selectedUser!.avatar})`,
28 | backgroundSize: 'cover',
29 | backgroundRepeat: 'no-repeat',
30 | backgroundPosition: 'center, center'
31 | }
32 | }
33 |
34 | const user = selectedUser ? selectedUser.name.split(" ") : [];
35 | return (
36 |
37 | {type === IdActiveType.Individual &&
38 | <>
39 |
40 |
41 |
{user[0]}
42 |
{user[1] || null}
43 |
44 | >
45 | }{type === IdActiveType.Channel &&
46 |
47 | {'# ' + selectedChannel!.name}
48 |
49 | }
50 |
51 |
Logged in as
52 |
53 | {loggedInName}
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 | }
62 |
63 | const mapState = (state: IState): ITopNavState => ({
64 | loggedInUser: state.auth.userInfo!,
65 | active: state.active
66 | });
67 | const mapDispatch = (state: Dispatch): ITopNavDispatch => ({})
68 |
69 | export const TopNav = connect(mapState, mapDispatch)(TopNavDOM);
70 |
--------------------------------------------------------------------------------
/app/src/features/navTop/topNav.scss:
--------------------------------------------------------------------------------
1 | .content-top {
2 | height: 80px;
3 | border-bottom: 1px solid #f1f1f1;
4 | width: 100%;
5 | display: flex;
6 | align-items: center;
7 |
8 | .user-avatar{
9 | border: 1px solid silver;
10 | border-radius: 5px;
11 | width: 40px;
12 | height: 40px;
13 | margin-left: 15px;
14 | margin-right: 10px;
15 | }
16 | .user-name{
17 | display: flex;
18 | flex-direction: column;
19 |
20 | .user-name-f {
21 | font-size: 16px;
22 | font-weight: 500;
23 | margin: 2px 0;
24 | }
25 |
26 | .user-name-l {
27 | font-size: 12px;
28 | }
29 | }
30 |
31 | .channel-name {
32 | margin-left: 15px;
33 | font-weight: 500;
34 | }
35 |
36 | .logged-in-as{
37 | margin-left: auto;
38 | margin-right: 15px;
39 | display: flex;
40 | align-items: center;
41 | color: #888888;
42 |
43 | .logged-name{
44 | font-size: 12px;
45 | margin-right: 15px;
46 | display: flex;
47 | align-items: center;
48 |
49 | .name-tag{
50 | background-color: #888888;
51 | border-radius: 3px;
52 | padding: 5px 10px;
53 | color: #ffffff;
54 | text-align: center;
55 | margin-left: 10px;
56 |
57 | .section-icon{
58 | margin-left: 10px;
59 | }
60 |
61 | &:hover {
62 | background-color: dimgrey;
63 | cursor: pointer;
64 | }
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/features/newUser/NewUser.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 {PopOver} from "../generic/popOver/PopOver";
6 |
7 | interface INewUserState {
8 | }
9 |
10 | interface INewUserDispatch {
11 | }
12 |
13 | interface INewUserProps extends INewUserState, INewUserDispatch {
14 | openState: boolean;
15 | onClose: () => any;
16 | }
17 |
18 | class NewUserDOM extends React.Component
{
19 | render () {
20 | return (
21 |
22 | New User form
23 |
24 |
25 | )
26 | }
27 | }
28 |
29 | const mapState = (state: IState): INewUserState => ({});
30 | const mapDispatch = (dispatch: Dispatch): INewUserDispatch => ({});
31 |
32 | export const NewUser = connect(mapState, mapDispatch)(NewUserDOM);
33 |
--------------------------------------------------------------------------------
/app/src/features/userList/UserList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IState } from '../../setup/IState';
3 | import { IUserInfo } from '../../types/IUser';
4 | import { actionActiveUserUpdate } from '../activity/actionActive';
5 | import { Dispatch, Action } from 'redux';
6 | import { connect } from 'react-redux';
7 | import "./userList.scss";
8 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
9 | import {faCircle, faPlus} from "@fortawesome/free-solid-svg-icons";
10 | import {ScrollSection} from "../generic/scrollWrapper/ScrollSection";
11 | import {IdActiveType} from "../activity/IdActiveType";
12 | import {IReducerActive} from "../activity/reducerActive";
13 | import {IUserDetails} from "../users/reducerUsers";
14 | import {actionUsersPendingReset} from "../users/actionUsers";
15 |
16 | interface IUserListState {
17 | allUsers: Array;
18 | selectedUser: IUserInfo;
19 | loggedInUser: IUserInfo;
20 | active: IReducerActive;
21 | }
22 | interface IUserListDispatch {
23 | onUserMakeActive: (user: IUserInfo) => Action;
24 | onResetPendingMsg: (userId: number) => Action;
25 | }
26 | interface IUserListProps extends IUserListState, IUserListDispatch {
27 | handlePeopleMore: any;
28 | peopleMoreOpen: boolean;
29 | channelMoreOpen: boolean;
30 | deviceVariant: string;
31 | }
32 |
33 | class UserListDOM extends React.Component {
34 | public render () {
35 | const {active} = this.props;
36 | const {type, selectedUser} = active;
37 | const sortedUsers = this.sortByOnline();
38 | const people = {
39 | important: sortedUsers.length > 5 ? sortedUsers.slice(0, 4): sortedUsers,
40 | more: sortedUsers.length > 5 ? sortedUsers.slice(5, sortedUsers.length): []
41 | };
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 |
50 |
51 | {people.important.map((person: IUserDetails, index: number) => {
52 | const { id, name, pendingMessageCount, isOnline} = person;
53 | const fullName = this.getLengthFilteredName(name);
54 | const isSelectedClass = type === IdActiveType.Individual && selectedUser && selectedUser.id === id ? 'selected' : '';
55 |
56 | return (
57 |
this.handleUserClick(person)}>
58 |
59 |
{fullName}
60 | {(pendingMessageCount && pendingMessageCount > 0) ?
61 |
{pendingMessageCount === 1 ? 'New': pendingMessageCount}
62 | : null
63 | }
64 |
65 | )
66 | })}
67 |
68 | {this.props.peopleMoreOpen && !this.props.channelMoreOpen && people.more.map((person: IUserDetails, index: number) => {
69 | const { id, name, pendingMessageCount, isOnline} = person;
70 | const fullName = this.getLengthFilteredName(name);
71 | const isSelectedClass = type === IdActiveType.Individual && selectedUser && selectedUser.id === id ? 'selected' : '';
72 |
73 | return (
74 | this.handleUserClick(person)}>
75 |
76 |
{fullName}
77 | {(pendingMessageCount && pendingMessageCount > 0) ?
78 |
{pendingMessageCount === 1 ? 'New': pendingMessageCount}
79 | : null
80 | }
81 |
82 | )
83 | })}
84 |
85 | {people.more.length > 0 && {this.props.peopleMoreOpen ? 'Less' : 'More'}}
86 | >
87 | )
88 | }
89 |
90 | private handleUserClick = (user: IUserInfo) => {
91 | const {onUserMakeActive, onResetPendingMsg} = this.props;
92 | onUserMakeActive(user);
93 | onResetPendingMsg(user.id);
94 | };
95 |
96 | private getLengthFilteredName = (name: string): string => {
97 | if (this.props.deviceVariant !== 'LaptopSmall') {
98 | return name;
99 | }
100 |
101 | return name.length > 18 ? name.substr(0, 18) + '...' : name;
102 | };
103 |
104 | private sortByOnline = () => {
105 | const {allUsers} = this.props;
106 | const sortedUsers = [...allUsers];
107 | sortedUsers.sort(function(a: any,b: any){ return b.isOnline - a.isOnline });
108 |
109 | return sortedUsers;
110 | }
111 | }
112 |
113 | const mapState = (state: IState): IUserListState => ({
114 | allUsers: Object.values(state.users.allUsers),
115 | selectedUser: state.active.selectedUser!,
116 | loggedInUser: state.auth.userInfo!,
117 | active: state.active
118 | });
119 |
120 | const mapDispatch = (dispatch: Dispatch): IUserListDispatch => ({
121 | onUserMakeActive: (user: IUserInfo) => dispatch(actionActiveUserUpdate(user)),
122 | onResetPendingMsg: (userId: number) => dispatch(actionUsersPendingReset(userId))
123 | });
124 | export const UserList = connect(mapState, mapDispatch)(UserListDOM);
--------------------------------------------------------------------------------
/app/src/features/userList/userList.scss:
--------------------------------------------------------------------------------
1 | .sideNav-section-important-people {
2 | min-height: 150px;
3 | max-height: 243px;
4 | }
5 | .selected {
6 | font-weight: 700;
7 | }
--------------------------------------------------------------------------------
/app/src/features/users/actionUsers.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "redux";
2 | import { IUserInfo } from "../../types/IUser";
3 |
4 |
5 | export enum TypeActionUsers {
6 | UserListRequest = "User > List > Request",
7 | UserListResponse = "User > List > Response",
8 | UserListComplete = "User > List > Complete",
9 |
10 | UserPendingMsgUpdate = "User > Pending_msg > Update",
11 | UserPendingMsgReset = "User > Pending_msg > Reset",
12 |
13 | UserOnlineStatusUpdate = "User > Online_status > Update",
14 | }
15 |
16 | export interface IActionUsers extends Action{
17 | onlineUserIds?: Array;
18 | userId?: number;
19 | userList?: Array;
20 | type: TypeActionUsers
21 | }
22 |
23 | export const actionUsersRequest = (): IActionUsers => ({
24 | type: TypeActionUsers.UserListRequest
25 | });
26 |
27 | export const actionUsersResponse = (userList: Array): IActionUsers => ({
28 | userList,
29 | type: TypeActionUsers.UserListResponse
30 | });
31 |
32 | export const actionUsersComplete = (): IActionUsers => ({
33 | type: TypeActionUsers.UserListComplete
34 | });
35 |
36 | export const actionUsersPendingUpdate = (userId: number): IActionUsers => ({
37 | userId,
38 | type: TypeActionUsers.UserPendingMsgUpdate
39 | });
40 |
41 | export const actionUsersPendingReset = (userId: number): IActionUsers => ({
42 | userId,
43 | type: TypeActionUsers.UserPendingMsgReset
44 | });
45 |
46 | export const actionUsersOnlineUserStatusUpdate = (onlineUserIds: Array): IActionUsers => ({
47 | onlineUserIds,
48 | type: TypeActionUsers.UserOnlineStatusUpdate
49 | });
50 |
--------------------------------------------------------------------------------
/app/src/features/users/epicUsers.ts:
--------------------------------------------------------------------------------
1 | import { Epic, ofType } from "redux-observable";
2 | import { Action } from "redux";
3 | import { IState } from "../../setup/IState";
4 | import { mergeMap, switchMap, catchError } from "rxjs/operators";
5 | import {Observable, of, from} from "rxjs";
6 | import { TypeActionUsers, IActionUsers, actionUsersComplete, actionUsersResponse } from "./actionUsers";
7 | import { API } from "../../api/api";
8 | import axios, {AxiosError, AxiosResponse} from 'axios';
9 |
10 | export const epicUsers: Epic = (action$, state$): Observable =>
11 | action$.pipe(
12 | ofType(TypeActionUsers.UserListRequest),
13 | mergeMap((action: IActionUsers) => {
14 |
15 | const {url} = API.GetUsers;
16 | return from(axios({
17 | method: "GET",
18 | url: url
19 | })).pipe(
20 | switchMap((response: AxiosResponse) => {
21 | return from([
22 | actionUsersResponse(response.data.data),
23 | actionUsersComplete()
24 | ]);
25 | }),
26 | catchError((error: AxiosError) => {
27 | console.log('caught error:', error.response);
28 | return of(actionUsersComplete);
29 | })
30 | )
31 | })
32 | )
--------------------------------------------------------------------------------
/app/src/features/users/reducerUsers.ts:
--------------------------------------------------------------------------------
1 | import {IUserInfo} from "../../types/IUser";
2 | import {IActionUsers, TypeActionUsers} from "./actionUsers";
3 |
4 | export interface IUserDetails extends IUserInfo {
5 | pendingMessageCount?: number;
6 | isOnline?: boolean
7 | }
8 |
9 | export interface IAllUsers {[key: number]: IUserDetails}
10 |
11 | export interface IReducerUsers {
12 | allUsers: IAllUsers;
13 | }
14 |
15 | const initial: IReducerUsers = {
16 | allUsers: {}
17 | }
18 |
19 | export default (state: IReducerUsers = initial, action: IActionUsers) => {
20 | switch(action.type) {
21 | case TypeActionUsers.UserListResponse:
22 | return {
23 | ...state,
24 | allUsers: convertUserArrayToObj(action.userList || [])
25 | };
26 | case TypeActionUsers.UserPendingMsgUpdate:
27 | return reducerUpdatePending(state, action);
28 | case TypeActionUsers.UserPendingMsgReset:
29 | return reducerUpdatePending(state, action, true);
30 | case TypeActionUsers.UserOnlineStatusUpdate:
31 | return reducerUpdateOnlineUsers(state, action);
32 | default:
33 | return state;
34 | }
35 | }
36 |
37 | const convertUserArrayToObj = (userList: Array): IAllUsers => {
38 | const userObj: IAllUsers = {};
39 | userList.forEach((user: IUserInfo) => {
40 | userObj[user.id] = user;
41 | });
42 |
43 | return userObj
44 | };
45 |
46 |
47 | const reducerUpdatePending = (state: IReducerUsers, {userId}: IActionUsers, reset: boolean = false): IReducerUsers => {
48 | if(!userId || !state.allUsers[userId]) return state;
49 |
50 | const allUsers = {...state.allUsers};
51 | if(reset) {
52 | allUsers[userId].pendingMessageCount = 0;
53 | } else {
54 | const pendingCount = allUsers[userId].pendingMessageCount || 0;
55 | allUsers[userId].pendingMessageCount = pendingCount + 1;
56 | }
57 |
58 | return {
59 | ...state,
60 | allUsers,
61 | }
62 | };
63 |
64 | const reducerUpdateOnlineUsers = (state: IReducerUsers, {onlineUserIds}: IActionUsers): IReducerUsers => {
65 | const updatedUsers = {...state.allUsers};
66 |
67 | for(let id in updatedUsers) {
68 | updatedUsers[id].isOnline = onlineUserIds!.indexOf(id) > -1;
69 | }
70 |
71 | return {
72 | allUsers: updatedUsers
73 | };
74 | };
75 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 | import { Provider } from 'react-redux';
7 | import { Store } from './setup/store';
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 | , document.getElementById('root'));
14 |
15 | // If you want your app to work offline and load faster, you can change
16 | // unregister() to register() below. Note this comes with some pitfalls.
17 | // Learn more about service workers: https://bit.ly/CRA-PWA
18 | serviceWorker.unregister();
19 |
--------------------------------------------------------------------------------
/app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/src/pages/chat/Chat.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 {
6 | getDeviceTypeInfo,
7 | isMobileDevice,
8 | isTabletDevice,
9 | } from 'typed-responsive-react';
10 | import "./chat.scss";
11 | import { TopNav } from '../../features/navTop/TopNav';
12 | import { ChatBox } from '../../features/chatBox/ChatBox';
13 | import {Grid} from "@material-ui/core";
14 | import {SideNav} from "../../features/navSide/SideNav";
15 | import {ScrollSection} from "../../features/generic/scrollWrapper/ScrollSection";
16 | import {BotNav} from "../../features/navBot/BotNav";
17 |
18 | interface IChatState {}
19 | interface IChatDispatch {}
20 | interface IChatProps extends IChatState, IChatDispatch {}
21 |
22 | class ChatDOM extends React.Component {
23 | render () {
24 | const {height, deviceTypeVariant} = getDeviceTypeInfo();
25 | const mainBoxWidth: React.CSSProperties = { width: deviceTypeVariant === 'LaptopSmall' || isMobileDevice() ? '100%' : '80%'};
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | {/*TODO: Later separate it out during chat implementation*/}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | const mapState = (state: IState): IChatState => ({})
50 | const mapDispatch = (state: Dispatch): IChatDispatch => ({})
51 |
52 | export const Chat = connect(mapState, mapDispatch)(ChatDOM);
53 |
--------------------------------------------------------------------------------
/app/src/pages/chat/chat.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | .main-box-laptop{
6 | height: 100%;
7 | padding: 50px auto;
8 | min-height: 500px;
9 | }
10 | .main-box-mobile{
11 | height: 100%;
12 | }
13 | .main-box{
14 | border-right: 1px solid #f1f1f1;
15 | display: flex;
16 |
17 | .content-wrapper{
18 | height: 100%;
19 | display: flex;
20 | width: 80%;
21 | flex-direction: column;
22 |
23 | .main-content {
24 | width: 100%;
25 | height:100%;
26 | display: flex;
27 | flex-direction: row;
28 | //background-image: url("http://localhost:1337/asset/logo.png");
29 | //background-position: bottom left;
30 | //background-repeat: no-repeat;
31 |
32 | .scrollarea-content {
33 | align-self: flex-end;
34 | width: 100%;
35 |
36 | .main-content-child {
37 | padding: 5px;
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/pages/welcome/Welcome.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 "./welcome.scss";
6 |
7 | interface IWelcomeState {}
8 | interface IWelcomeDispatch {}
9 | interface IWelcomeProps extends IWelcomeState, IWelcomeDispatch {}
10 |
11 | class WelcomeDOM extends React.Component {
12 | render () {
13 | return (
14 | Welcome whoever!
15 | )
16 | }
17 | }
18 |
19 | const mapState = (state: IState): IWelcomeState => ({})
20 | const mapDispatch = (state: Dispatch): IWelcomeDispatch => ({})
21 |
22 | export const Welcome = connect(mapState, mapDispatch)(WelcomeDOM);
23 |
--------------------------------------------------------------------------------
/app/src/pages/welcome/welcome.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiddharthaChowdhury/react-chat-app/888fad6be54949aaca1d904e116e0556ea38c63f/app/src/pages/welcome/welcome.scss
--------------------------------------------------------------------------------
/app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app/src/serviceWorker.ts:
--------------------------------------------------------------------------------
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 https://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 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/app/src/setup/IState.ts:
--------------------------------------------------------------------------------
1 | import { IReducerModal } from "../features/modal/reducerModal";
2 | import { IReducerUsers } from "../features/users/reducerUsers";
3 | import { IReducerAuth } from "../features/auth/reducerAuth";
4 | import { IReducerActive } from "../features/activity/reducerActive";
5 | import { IReducerConversation } from "../features/conversation/reducerConversation";
6 | import {IReducerChannel} from "../features/channels/reducerChannel";
7 |
8 | export interface IState {
9 | modal: IReducerModal;
10 | users: IReducerUsers;
11 | auth: IReducerAuth;
12 | active: IReducerActive;
13 | conversation: IReducerConversation;
14 | channels: IReducerChannel;
15 | }
--------------------------------------------------------------------------------
/app/src/setup/rootEpic.ts:
--------------------------------------------------------------------------------
1 | import {combineEpics} from "redux-observable";
2 | import {Action} from "redux";
3 | import {IState} from "./IState";
4 | import { epicUsers } from "../features/users/epicUsers";
5 | import { epicLogin } from "../features/auth/epicAuth";
6 | import { epicConversation } from "../features/conversation/epicConversation";
7 |
8 | export default combineEpics, Action, IState>(
9 | epicUsers,
10 | epicLogin,
11 | epicConversation,
12 | );
--------------------------------------------------------------------------------
/app/src/setup/rootReducer.ts:
--------------------------------------------------------------------------------
1 | import {combineReducers} from "redux";
2 | import {IState} from "./IState";
3 | import { reducerModal } from "../features/modal/reducerModal";
4 | import reducerUsers from "../features/users/reducerUsers";
5 | import reducerAuth from "../features/auth/reducerAuth";
6 | import reducerActive from "../features/activity/reducerActive";
7 | import reducerConversation from "../features/conversation/reducerConversation";
8 | import reducerChannel from "../features/channels/reducerChannel";
9 |
10 | export default combineReducers({
11 | modal: reducerModal,
12 | users: reducerUsers,
13 | auth: reducerAuth,
14 | active: reducerActive,
15 | conversation: reducerConversation,
16 | channels: reducerChannel
17 | })
--------------------------------------------------------------------------------
/app/src/setup/store.ts:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, compose, Action} from "redux";
2 | import { createEpicMiddleware } from 'redux-observable';
3 | import rootReducer from "./rootReducer";
4 | import rootEpic from "./rootEpic";
5 | import {IState} from "./IState";
6 |
7 | const epicMiddleware = createEpicMiddleware, Action, IState>();
8 |
9 | const middlewares = [
10 | epicMiddleware
11 | ];
12 | const initialState = {};
13 |
14 | const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
15 | const enhancers = composeEnhancers(applyMiddleware(...middlewares));
16 |
17 | export const Store = createStore(rootReducer, initialState, enhancers);
18 |
19 | epicMiddleware.run(rootEpic);
--------------------------------------------------------------------------------
/app/src/types/IMessage.ts:
--------------------------------------------------------------------------------
1 | export interface IMessageInfo {
2 | channelId?: string;
3 | fromId: number;
4 | toId?: number;
5 | message: IMessage;
6 | createdAt: number;
7 | source: IdMessageSource;
8 | companyId?: number;
9 | selfEcho?: boolean;
10 | }
11 |
12 | export enum IdMessageSource {
13 | User_Message = "User_Message",
14 | Channel_Message = "Channel_Message"
15 | }
16 |
17 | export interface IMessage {
18 | text?: string;
19 | imageLink?: string;
20 | fileLink?: string;
21 | webLink?: string;
22 | }
--------------------------------------------------------------------------------
/app/src/types/IUser.ts:
--------------------------------------------------------------------------------
1 | export interface IUserInfo {
2 | id: number;
3 | name: string;
4 | email: string;
5 | companyId?: string;
6 | companyName?: string;
7 | avatar?: string;
8 | }
--------------------------------------------------------------------------------
/app/src/util/socket/utilSocket.ts:
--------------------------------------------------------------------------------
1 | import io from "socket.io-client";
2 |
3 | export class UtilSocket {
4 | public socket: any = null;
5 | private SOCKET_URI = 'http://localhost:8002';
6 |
7 | constructor (){
8 | this.socket = io.connect(this.SOCKET_URI);
9 | }
10 | }
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/server/.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 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-c",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/body-parser": {
8 | "version": "1.17.1",
9 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz",
10 | "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==",
11 | "dev": true,
12 | "requires": {
13 | "@types/connect": "3.4.32",
14 | "@types/node": "12.7.2"
15 | }
16 | },
17 | "@types/connect": {
18 | "version": "3.4.32",
19 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
20 | "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
21 | "dev": true,
22 | "requires": {
23 | "@types/node": "12.7.2"
24 | }
25 | },
26 | "@types/cors": {
27 | "version": "2.8.6",
28 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz",
29 | "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==",
30 | "dev": true,
31 | "requires": {
32 | "@types/express": "4.17.1"
33 | }
34 | },
35 | "@types/express": {
36 | "version": "4.17.1",
37 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.1.tgz",
38 | "integrity": "sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==",
39 | "dev": true,
40 | "requires": {
41 | "@types/body-parser": "1.17.1",
42 | "@types/express-serve-static-core": "4.16.9",
43 | "@types/serve-static": "1.13.3"
44 | }
45 | },
46 | "@types/express-serve-static-core": {
47 | "version": "4.16.9",
48 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz",
49 | "integrity": "sha512-GqpaVWR0DM8FnRUJYKlWgyARoBUAVfRIeVDZQKOttLFp5SmhhF9YFIYeTPwMd/AXfxlP7xVO2dj1fGu0Q+krKQ==",
50 | "dev": true,
51 | "requires": {
52 | "@types/node": "12.7.2",
53 | "@types/range-parser": "1.2.3"
54 | }
55 | },
56 | "@types/mime": {
57 | "version": "2.0.1",
58 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
59 | "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
60 | "dev": true
61 | },
62 | "@types/node": {
63 | "version": "12.7.2",
64 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz",
65 | "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==",
66 | "dev": true
67 | },
68 | "@types/range-parser": {
69 | "version": "1.2.3",
70 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
71 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
72 | "dev": true
73 | },
74 | "@types/serve-static": {
75 | "version": "1.13.3",
76 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
77 | "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
78 | "dev": true,
79 | "requires": {
80 | "@types/express-serve-static-core": "4.16.9",
81 | "@types/mime": "2.0.1"
82 | }
83 | },
84 | "@types/socket.io": {
85 | "version": "2.1.2",
86 | "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.2.tgz",
87 | "integrity": "sha512-Ind+4qMNfQ62llyB4IMs1D8znMEBsMKohZBPqfBUIXqLQ9bdtWIbNTBWwtdcBWJKnokMZGcmWOOKslatni5vtA==",
88 | "dev": true,
89 | "requires": {
90 | "@types/node": "12.7.2"
91 | }
92 | },
93 | "accepts": {
94 | "version": "1.3.7",
95 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
96 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
97 | "requires": {
98 | "mime-types": "2.1.24",
99 | "negotiator": "0.6.2"
100 | }
101 | },
102 | "after": {
103 | "version": "0.8.2",
104 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
105 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
106 | },
107 | "arg": {
108 | "version": "4.1.1",
109 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz",
110 | "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw=="
111 | },
112 | "array-flatten": {
113 | "version": "1.1.1",
114 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
115 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
116 | },
117 | "arraybuffer.slice": {
118 | "version": "0.0.7",
119 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
120 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
121 | },
122 | "async-limiter": {
123 | "version": "1.0.1",
124 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
125 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
126 | },
127 | "backo2": {
128 | "version": "1.0.2",
129 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
130 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
131 | },
132 | "base64-arraybuffer": {
133 | "version": "0.1.5",
134 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
135 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
136 | },
137 | "base64id": {
138 | "version": "1.0.0",
139 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
140 | "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
141 | },
142 | "better-assert": {
143 | "version": "1.0.2",
144 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
145 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
146 | "requires": {
147 | "callsite": "1.0.0"
148 | }
149 | },
150 | "blob": {
151 | "version": "0.0.5",
152 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
153 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
154 | },
155 | "body-parser": {
156 | "version": "1.19.0",
157 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
158 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
159 | "requires": {
160 | "bytes": "3.1.0",
161 | "content-type": "1.0.4",
162 | "debug": "2.6.9",
163 | "depd": "1.1.2",
164 | "http-errors": "1.7.2",
165 | "iconv-lite": "0.4.24",
166 | "on-finished": "2.3.0",
167 | "qs": "6.7.0",
168 | "raw-body": "2.4.0",
169 | "type-is": "1.6.18"
170 | }
171 | },
172 | "buffer-from": {
173 | "version": "1.1.1",
174 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
175 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
176 | },
177 | "bytes": {
178 | "version": "3.1.0",
179 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
180 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
181 | },
182 | "callsite": {
183 | "version": "1.0.0",
184 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
185 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
186 | },
187 | "component-bind": {
188 | "version": "1.0.0",
189 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
190 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
191 | },
192 | "component-emitter": {
193 | "version": "1.2.1",
194 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
195 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
196 | },
197 | "component-inherit": {
198 | "version": "0.0.3",
199 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
200 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
201 | },
202 | "content-disposition": {
203 | "version": "0.5.3",
204 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
205 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
206 | "requires": {
207 | "safe-buffer": "5.1.2"
208 | }
209 | },
210 | "content-type": {
211 | "version": "1.0.4",
212 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
213 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
214 | },
215 | "cookie": {
216 | "version": "0.4.0",
217 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
218 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
219 | },
220 | "cookie-signature": {
221 | "version": "1.0.6",
222 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
223 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
224 | },
225 | "cors": {
226 | "version": "2.8.5",
227 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
228 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
229 | "requires": {
230 | "object-assign": "4.1.1",
231 | "vary": "1.1.2"
232 | }
233 | },
234 | "debug": {
235 | "version": "2.6.9",
236 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
237 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
238 | "requires": {
239 | "ms": "2.0.0"
240 | }
241 | },
242 | "depd": {
243 | "version": "1.1.2",
244 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
245 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
246 | },
247 | "destroy": {
248 | "version": "1.0.4",
249 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
250 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
251 | },
252 | "diff": {
253 | "version": "4.0.1",
254 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
255 | "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q=="
256 | },
257 | "ee-first": {
258 | "version": "1.1.1",
259 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
260 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
261 | },
262 | "encodeurl": {
263 | "version": "1.0.2",
264 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
265 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
266 | },
267 | "engine.io": {
268 | "version": "3.3.2",
269 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz",
270 | "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==",
271 | "requires": {
272 | "accepts": "1.3.7",
273 | "base64id": "1.0.0",
274 | "cookie": "0.3.1",
275 | "debug": "3.1.0",
276 | "engine.io-parser": "2.1.3",
277 | "ws": "6.1.4"
278 | },
279 | "dependencies": {
280 | "cookie": {
281 | "version": "0.3.1",
282 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
283 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
284 | },
285 | "debug": {
286 | "version": "3.1.0",
287 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
288 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
289 | "requires": {
290 | "ms": "2.0.0"
291 | }
292 | }
293 | }
294 | },
295 | "engine.io-client": {
296 | "version": "3.3.2",
297 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz",
298 | "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==",
299 | "requires": {
300 | "component-emitter": "1.2.1",
301 | "component-inherit": "0.0.3",
302 | "debug": "3.1.0",
303 | "engine.io-parser": "2.1.3",
304 | "has-cors": "1.1.0",
305 | "indexof": "0.0.1",
306 | "parseqs": "0.0.5",
307 | "parseuri": "0.0.5",
308 | "ws": "6.1.4",
309 | "xmlhttprequest-ssl": "1.5.5",
310 | "yeast": "0.1.2"
311 | },
312 | "dependencies": {
313 | "debug": {
314 | "version": "3.1.0",
315 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
316 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
317 | "requires": {
318 | "ms": "2.0.0"
319 | }
320 | }
321 | }
322 | },
323 | "engine.io-parser": {
324 | "version": "2.1.3",
325 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
326 | "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
327 | "requires": {
328 | "after": "0.8.2",
329 | "arraybuffer.slice": "0.0.7",
330 | "base64-arraybuffer": "0.1.5",
331 | "blob": "0.0.5",
332 | "has-binary2": "1.0.3"
333 | }
334 | },
335 | "escape-html": {
336 | "version": "1.0.3",
337 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
338 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
339 | },
340 | "etag": {
341 | "version": "1.8.1",
342 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
343 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
344 | },
345 | "express": {
346 | "version": "4.17.1",
347 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
348 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
349 | "requires": {
350 | "accepts": "1.3.7",
351 | "array-flatten": "1.1.1",
352 | "body-parser": "1.19.0",
353 | "content-disposition": "0.5.3",
354 | "content-type": "1.0.4",
355 | "cookie": "0.4.0",
356 | "cookie-signature": "1.0.6",
357 | "debug": "2.6.9",
358 | "depd": "1.1.2",
359 | "encodeurl": "1.0.2",
360 | "escape-html": "1.0.3",
361 | "etag": "1.8.1",
362 | "finalhandler": "1.1.2",
363 | "fresh": "0.5.2",
364 | "merge-descriptors": "1.0.1",
365 | "methods": "1.1.2",
366 | "on-finished": "2.3.0",
367 | "parseurl": "1.3.3",
368 | "path-to-regexp": "0.1.7",
369 | "proxy-addr": "2.0.5",
370 | "qs": "6.7.0",
371 | "range-parser": "1.2.1",
372 | "safe-buffer": "5.1.2",
373 | "send": "0.17.1",
374 | "serve-static": "1.14.1",
375 | "setprototypeof": "1.1.1",
376 | "statuses": "1.5.0",
377 | "type-is": "1.6.18",
378 | "utils-merge": "1.0.1",
379 | "vary": "1.1.2"
380 | }
381 | },
382 | "finalhandler": {
383 | "version": "1.1.2",
384 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
385 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
386 | "requires": {
387 | "debug": "2.6.9",
388 | "encodeurl": "1.0.2",
389 | "escape-html": "1.0.3",
390 | "on-finished": "2.3.0",
391 | "parseurl": "1.3.3",
392 | "statuses": "1.5.0",
393 | "unpipe": "1.0.0"
394 | }
395 | },
396 | "forwarded": {
397 | "version": "0.1.2",
398 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
399 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
400 | },
401 | "fresh": {
402 | "version": "0.5.2",
403 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
404 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
405 | },
406 | "has-binary2": {
407 | "version": "1.0.3",
408 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
409 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
410 | "requires": {
411 | "isarray": "2.0.1"
412 | }
413 | },
414 | "has-cors": {
415 | "version": "1.1.0",
416 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
417 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
418 | },
419 | "http-errors": {
420 | "version": "1.7.2",
421 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
422 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
423 | "requires": {
424 | "depd": "1.1.2",
425 | "inherits": "2.0.3",
426 | "setprototypeof": "1.1.1",
427 | "statuses": "1.5.0",
428 | "toidentifier": "1.0.0"
429 | }
430 | },
431 | "iconv-lite": {
432 | "version": "0.4.24",
433 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
434 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
435 | "requires": {
436 | "safer-buffer": "2.1.2"
437 | }
438 | },
439 | "indexof": {
440 | "version": "0.0.1",
441 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
442 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
443 | },
444 | "inherits": {
445 | "version": "2.0.3",
446 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
447 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
448 | },
449 | "ipaddr.js": {
450 | "version": "1.9.0",
451 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
452 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
453 | },
454 | "isarray": {
455 | "version": "2.0.1",
456 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
457 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
458 | },
459 | "make-error": {
460 | "version": "1.3.5",
461 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
462 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
463 | },
464 | "media-typer": {
465 | "version": "0.3.0",
466 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
467 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
468 | },
469 | "merge-descriptors": {
470 | "version": "1.0.1",
471 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
472 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
473 | },
474 | "methods": {
475 | "version": "1.1.2",
476 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
477 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
478 | },
479 | "mime": {
480 | "version": "1.6.0",
481 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
482 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
483 | },
484 | "mime-db": {
485 | "version": "1.40.0",
486 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
487 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
488 | },
489 | "mime-types": {
490 | "version": "2.1.24",
491 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
492 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
493 | "requires": {
494 | "mime-db": "1.40.0"
495 | }
496 | },
497 | "ms": {
498 | "version": "2.0.0",
499 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
500 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
501 | },
502 | "negotiator": {
503 | "version": "0.6.2",
504 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
505 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
506 | },
507 | "object-assign": {
508 | "version": "4.1.1",
509 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
510 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
511 | },
512 | "object-component": {
513 | "version": "0.0.3",
514 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
515 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
516 | },
517 | "on-finished": {
518 | "version": "2.3.0",
519 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
520 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
521 | "requires": {
522 | "ee-first": "1.1.1"
523 | }
524 | },
525 | "parseqs": {
526 | "version": "0.0.5",
527 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
528 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
529 | "requires": {
530 | "better-assert": "1.0.2"
531 | }
532 | },
533 | "parseuri": {
534 | "version": "0.0.5",
535 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
536 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
537 | "requires": {
538 | "better-assert": "1.0.2"
539 | }
540 | },
541 | "parseurl": {
542 | "version": "1.3.3",
543 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
544 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
545 | },
546 | "path-to-regexp": {
547 | "version": "0.1.7",
548 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
549 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
550 | },
551 | "proxy-addr": {
552 | "version": "2.0.5",
553 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
554 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
555 | "requires": {
556 | "forwarded": "0.1.2",
557 | "ipaddr.js": "1.9.0"
558 | }
559 | },
560 | "qs": {
561 | "version": "6.7.0",
562 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
563 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
564 | },
565 | "range-parser": {
566 | "version": "1.2.1",
567 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
568 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
569 | },
570 | "raw-body": {
571 | "version": "2.4.0",
572 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
573 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
574 | "requires": {
575 | "bytes": "3.1.0",
576 | "http-errors": "1.7.2",
577 | "iconv-lite": "0.4.24",
578 | "unpipe": "1.0.0"
579 | }
580 | },
581 | "safe-buffer": {
582 | "version": "5.1.2",
583 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
584 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
585 | },
586 | "safer-buffer": {
587 | "version": "2.1.2",
588 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
589 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
590 | },
591 | "send": {
592 | "version": "0.17.1",
593 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
594 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
595 | "requires": {
596 | "debug": "2.6.9",
597 | "depd": "1.1.2",
598 | "destroy": "1.0.4",
599 | "encodeurl": "1.0.2",
600 | "escape-html": "1.0.3",
601 | "etag": "1.8.1",
602 | "fresh": "0.5.2",
603 | "http-errors": "1.7.2",
604 | "mime": "1.6.0",
605 | "ms": "2.1.1",
606 | "on-finished": "2.3.0",
607 | "range-parser": "1.2.1",
608 | "statuses": "1.5.0"
609 | },
610 | "dependencies": {
611 | "ms": {
612 | "version": "2.1.1",
613 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
614 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
615 | }
616 | }
617 | },
618 | "serve-static": {
619 | "version": "1.14.1",
620 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
621 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
622 | "requires": {
623 | "encodeurl": "1.0.2",
624 | "escape-html": "1.0.3",
625 | "parseurl": "1.3.3",
626 | "send": "0.17.1"
627 | }
628 | },
629 | "setprototypeof": {
630 | "version": "1.1.1",
631 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
632 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
633 | },
634 | "socket.io": {
635 | "version": "2.2.0",
636 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz",
637 | "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==",
638 | "requires": {
639 | "debug": "4.1.1",
640 | "engine.io": "3.3.2",
641 | "has-binary2": "1.0.3",
642 | "socket.io-adapter": "1.1.1",
643 | "socket.io-client": "2.2.0",
644 | "socket.io-parser": "3.3.0"
645 | },
646 | "dependencies": {
647 | "debug": {
648 | "version": "4.1.1",
649 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
650 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
651 | "requires": {
652 | "ms": "2.1.2"
653 | }
654 | },
655 | "ms": {
656 | "version": "2.1.2",
657 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
658 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
659 | }
660 | }
661 | },
662 | "socket.io-adapter": {
663 | "version": "1.1.1",
664 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
665 | "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
666 | },
667 | "socket.io-client": {
668 | "version": "2.2.0",
669 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz",
670 | "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==",
671 | "requires": {
672 | "backo2": "1.0.2",
673 | "base64-arraybuffer": "0.1.5",
674 | "component-bind": "1.0.0",
675 | "component-emitter": "1.2.1",
676 | "debug": "3.1.0",
677 | "engine.io-client": "3.3.2",
678 | "has-binary2": "1.0.3",
679 | "has-cors": "1.1.0",
680 | "indexof": "0.0.1",
681 | "object-component": "0.0.3",
682 | "parseqs": "0.0.5",
683 | "parseuri": "0.0.5",
684 | "socket.io-parser": "3.3.0",
685 | "to-array": "0.1.4"
686 | },
687 | "dependencies": {
688 | "debug": {
689 | "version": "3.1.0",
690 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
691 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
692 | "requires": {
693 | "ms": "2.0.0"
694 | }
695 | }
696 | }
697 | },
698 | "socket.io-parser": {
699 | "version": "3.3.0",
700 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
701 | "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
702 | "requires": {
703 | "component-emitter": "1.2.1",
704 | "debug": "3.1.0",
705 | "isarray": "2.0.1"
706 | },
707 | "dependencies": {
708 | "debug": {
709 | "version": "3.1.0",
710 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
711 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
712 | "requires": {
713 | "ms": "2.0.0"
714 | }
715 | }
716 | }
717 | },
718 | "source-map": {
719 | "version": "0.6.1",
720 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
721 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
722 | },
723 | "source-map-support": {
724 | "version": "0.5.13",
725 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
726 | "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
727 | "requires": {
728 | "buffer-from": "1.1.1",
729 | "source-map": "0.6.1"
730 | }
731 | },
732 | "statuses": {
733 | "version": "1.5.0",
734 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
735 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
736 | },
737 | "to-array": {
738 | "version": "0.1.4",
739 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
740 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
741 | },
742 | "toidentifier": {
743 | "version": "1.0.0",
744 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
745 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
746 | },
747 | "ts-node": {
748 | "version": "8.3.0",
749 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz",
750 | "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==",
751 | "requires": {
752 | "arg": "4.1.1",
753 | "diff": "4.0.1",
754 | "make-error": "1.3.5",
755 | "source-map-support": "0.5.13",
756 | "yn": "3.1.1"
757 | }
758 | },
759 | "type-is": {
760 | "version": "1.6.18",
761 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
762 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
763 | "requires": {
764 | "media-typer": "0.3.0",
765 | "mime-types": "2.1.24"
766 | }
767 | },
768 | "typescript": {
769 | "version": "3.5.3",
770 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz",
771 | "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g=="
772 | },
773 | "unpipe": {
774 | "version": "1.0.0",
775 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
776 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
777 | },
778 | "utils-merge": {
779 | "version": "1.0.1",
780 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
781 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
782 | },
783 | "vary": {
784 | "version": "1.1.2",
785 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
786 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
787 | },
788 | "ws": {
789 | "version": "6.1.4",
790 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
791 | "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
792 | "requires": {
793 | "async-limiter": "1.0.1"
794 | }
795 | },
796 | "xmlhttprequest-ssl": {
797 | "version": "1.5.5",
798 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
799 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
800 | },
801 | "yeast": {
802 | "version": "0.1.2",
803 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
804 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
805 | },
806 | "yn": {
807 | "version": "3.1.1",
808 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
809 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
810 | }
811 | }
812 | }
813 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-c",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "ts-node src/index.ts"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "cors": "^2.8.5",
14 | "express": "^4.17.1",
15 | "socket.io": "^2.2.0",
16 | "ts-node": "^8.3.0",
17 | "typescript": "^3.5.3"
18 | },
19 | "devDependencies": {
20 | "@types/cors": "^2.8.6",
21 | "@types/express": "^4.17.1",
22 | "@types/node": "^12.7.2",
23 | "@types/socket.io": "^2.1.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import http from 'http';
3 | import socket, {Socket} from 'socket.io';
4 | import cors from 'cors';
5 | import { IdSocketKey } from './socket/infoSocket';
6 | import users from './temp/users';
7 | import { IUserInfo } from './types/IUser';
8 |
9 | const app = express();
10 | const PORT = 8002;
11 | const server = http.createServer(app);
12 | const io = socket(server);
13 |
14 | const clients: any = {};
15 |
16 | io.on(IdSocketKey.connection, (client: any) => {
17 | client.on(IdSocketKey.signIn, (userInfo: IUserInfo) => {
18 | const user_id = userInfo.id;
19 | const company_id = userInfo.companyId;
20 |
21 | if (!user_id || !company_id) return;
22 |
23 | client['myId'] = {id: user_id, companyId: userInfo.companyId};
24 |
25 | if(!clients[company_id]) {
26 | clients[company_id] = {}
27 | }
28 |
29 | if (clients[company_id][user_id]) {
30 | clients[company_id][user_id].push(client);
31 | } else {
32 | clients[company_id][user_id] = [client];
33 | }
34 | // Join default chatRoom
35 | const defaultRoomName = 'channel'+company_id;
36 | client.join(defaultRoomName);
37 |
38 | // Announce new user connected
39 | const onlineUserIdList = Object.keys(clients[company_id]);
40 | for(let user in clients[company_id]) {
41 | clients[company_id][user].forEach((clientSession: any) => {
42 | clientSession.emit(IdSocketKey.onlineUsers, onlineUserIdList)
43 | })
44 | }
45 | });
46 |
47 | client.on('error', (error: any) => {
48 | console.log("ERROR!!", error)
49 | });
50 |
51 | client.on('disconnecting', (reason: any) => {
52 | // console.log("DISCONNECTING, ", reason)
53 | });
54 |
55 | client.on(IdSocketKey.channelBroadcast, (payload: any) => {
56 | // console.log('CHANNEL BROADCAST', io.sockets.adapter.rooms[payload.channelId]);
57 | // io.sockets.adapter.rooms[payload.channelId].length {to get how many in the room live}
58 | if(!!io.sockets.adapter.rooms[payload.channelId]) {
59 | io.to(payload.channelId).emit(IdSocketKey.channelBroadcast, payload);
60 | }
61 | });
62 |
63 | client.on(IdSocketKey.message, (msg: any)=> {
64 | console.log('connections', Object.keys(clients));
65 | const targetId = msg.toId;
66 | const sourceId = client['myId'].id;
67 | const companyId = client['myId'].companyId;
68 |
69 |
70 | if(targetId && clients[companyId][targetId]) {
71 |
72 | clients[companyId][targetId].forEach((cli: any) => {
73 | cli.emit(IdSocketKey.message, {...msg, selfEcho: false});
74 | });
75 | }
76 |
77 | if(sourceId === targetId) {
78 | return;
79 | }
80 |
81 | if(sourceId && clients[companyId][sourceId]) {
82 | // Echo back
83 | clients[companyId][sourceId].forEach((cli: any) => {
84 | cli.emit(IdSocketKey.message, {...msg, selfEcho: true});
85 | });
86 |
87 | }
88 | });
89 |
90 | client.on(IdSocketKey.disconnect, function() {
91 | if (!client['myId'] ) {
92 | return;
93 | }
94 |
95 | const user_id = client['myId'].id;
96 | const company_id = client['myId'].companyId;
97 |
98 | // Leave chat rooms
99 | const defaultRoomName = 'channel'+company_id;
100 | client.leave(defaultRoomName);
101 |
102 | // Remove from clients object
103 | if (!clients[company_id] || !clients[company_id][user_id]) {
104 | return
105 | }
106 |
107 | let targetClients = clients[company_id][user_id];
108 |
109 | for (let i = 0; i < targetClients.length; ++i) {
110 | if (targetClients[i] == client) {
111 | targetClients.splice(i, 1);
112 | }
113 | }
114 |
115 | if(targetClients.length === 0) {
116 | delete clients[company_id][user_id]
117 | }
118 |
119 | if(Object.keys(clients[company_id]).length === 0){
120 | delete clients[company_id]
121 | }
122 |
123 | if(clients[company_id]) {
124 | // Announce new user connected
125 | const onlineUserIdList = Object.keys(clients[company_id]);
126 | for(let user in clients[company_id]) {
127 | clients[company_id][user].forEach((clientSession: any) => {
128 | clientSession.emit(IdSocketKey.onlineUsers, onlineUserIdList)
129 | })
130 | }
131 | }
132 | });
133 | });
134 |
135 | app.use(cors());
136 | app.get("/users", (req, res) => {
137 | res.send({ data: users });
138 | });
139 |
140 | app.get("/users/:id", (req, res) => {
141 | const companyId = req.params.id;
142 | res.send({ data: users.filter((user: IUserInfo) => user.companyId === companyId) });
143 | });
144 |
145 | server.listen(PORT, () =>
146 | console.log(`Example app listening on port ${PORT}!`)
147 | );
148 |
--------------------------------------------------------------------------------
/server/src/socket/infoSocket.ts:
--------------------------------------------------------------------------------
1 | export enum IdSocketKey {
2 | connection = 'connection',
3 | disconnect = 'disconnect',
4 | signIn = 'sign-in',
5 | message = 'message',
6 | channelBroadcast = 'channel-broadcast',
7 | onlineUsers = 'online-users'
8 | }
--------------------------------------------------------------------------------
/server/src/temp/users.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | id: 1,
4 | name: 'Alexa',
5 | email: 'Alexa@some.com',
6 | companyId: '33',
7 | companyName: 'Crazy org',
8 | avatar: 'https://www.tinygraphs.com/squares/alex?theme=summerwarmth&numcolors=4&size=220&fmt=svg'
9 | },
10 | {
11 | id: 2,
12 | name: 'Google Assistant',
13 | email: 'GoogleAssistant@some.com',
14 | companyId: '33',
15 | companyName: 'Crazy org',
16 | avatar: 'https://www.tinygraphs.com/squares/goog?theme=frogideas&numcolors=4&size=220&fmt=svg'
17 | },
18 | {
19 | id: 3,
20 | name: 'Siri',
21 | email: 'Siri@some.com',
22 | companyId: '33',
23 | companyName: 'Crazy org',
24 | avatar: 'https://www.tinygraphs.com/squares/siri?theme=sugarsweets&numcolors=4&size=220&fmt=svg'
25 | },
26 | {
27 | id: 4,
28 | name: 'TARS',
29 | email: 'TARS@some.com',
30 | companyId: '33',
31 | companyName: 'Crazy org',
32 | avatar: 'https://www.tinygraphs.com/squares/tars?theme=bythepool&numcolors=4&size=220&fmt=svg'
33 | },
34 | {
35 | id: 5,
36 | name: 'CASE',
37 | email: 'CASE@Weird.com',
38 | companyId: '23',
39 | companyName: 'Weird Company',
40 | avatar: 'https://www.tinygraphs.com/squares/case?theme=duskfalling&numcolors=4&size=220&fmt=svg'
41 | },
42 | {
43 | id: 6,
44 | name: 'Cortana',
45 | email: 'cortana@Weird.com',
46 | companyId: '23',
47 | companyName: 'Weird Company',
48 | avatar: 'https://www.tinygraphs.com/squares/cortana?theme=summerwarmth&numcolors=4&size=220&fmt=svg'
49 | },
50 | {
51 | id: 7,
52 | name: 'Jarvis',
53 | email: 'Jarvis@Weird.com',
54 | companyId: '23',
55 | companyName: 'Weird Company',
56 | avatar: 'https://www.tinygraphs.com/squares/jarvis?theme=berrypie&numcolors=4&size=220&fmt=svg'
57 | },
58 | {
59 | id: 8,
60 | name: 'Dummy',
61 | email: 'dummy@Weird.com',
62 | companyId: '23',
63 | companyName: 'Weird Company',
64 | avatar: 'https://www.tinygraphs.com/squares/Dummy?theme=summerwarmth&numcolors=4&size=220&fmt=svg'
65 | }
66 | ]
--------------------------------------------------------------------------------
/server/src/types/IMessage.ts:
--------------------------------------------------------------------------------
1 | export interface IMessageInfo {
2 | channelId?: number;
3 | fromId: number;
4 | toId?: number;
5 | message: IMessage;
6 | createdAt: number;
7 | source: IdMessageSource;
8 | companyId?: number;
9 | selfEcho?: boolean;
10 | }
11 |
12 | export enum IdMessageSource {
13 | User_Message = "User_Message",
14 | Channel_Message = "Channel_Message"
15 | }
16 |
17 | export interface IMessage {
18 | text?: string;
19 | imageLink?: string;
20 | fileLink?: string;
21 | webLink?: string;
22 | }
--------------------------------------------------------------------------------
/server/src/types/IUser.ts:
--------------------------------------------------------------------------------
1 | export interface IUserInfo {
2 | id: number;
3 | name: string;
4 | email: string;
5 | companyId?: string;
6 | companyName?: string;
7 | }
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "./build", /* Redirect output structure to the directory. */
15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise errorMessage on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
30 | // "noImplicitThis": true, /* Raise errorMessage on 'this' expressions with an implied 'any' type. */
31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
32 |
33 | /* Additional Checks */
34 | // "noUnusedLocals": true, /* Report errors on unused locals. */
35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
36 | // "noImplicitReturns": true, /* Report errorMessage when not all code paths in function return a value. */
37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
38 |
39 | /* Module Resolution Options */
40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
44 | // "typeRoots": [], /* List of folders to include type definitions from. */
45 | // "types": [], /* Type declaration files to be included in compilation. */
46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
49 |
50 | /* Source Map Options */
51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
55 |
56 | /* Experimental Options */
57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
59 | }
60 | }
--------------------------------------------------------------------------------