├── OWNERS
├── test
├── style.mock.js
├── file.mock.js
└── jest.setup.js
├── src
├── components
│ ├── NotificationBar
│ │ ├── NotificationBar.text.jsx
│ │ ├── NotificationBar.css
│ │ └── NotificationBar.jsx
│ ├── ClientList
│ │ ├── ClientList.css
│ │ ├── ClientList.test.jsx
│ │ ├── ClientList.jsx
│ │ └── __snapshots__
│ │ │ └── ClientList.test.jsx.snap
│ ├── ClientsPanel
│ │ ├── ClientsPanel.css
│ │ ├── __snapshots__
│ │ │ └── ClientsPanel.test.jsx.snap
│ │ ├── ClientsPanel.test.jsx
│ │ └── ClientsPanel.jsx
│ ├── TitleBar
│ │ ├── TitleBar.jsx
│ │ ├── __snapshots__
│ │ │ └── TitleBar.test.jsx.snap
│ │ ├── TitleBar.css
│ │ └── TitleBar.test.jsx
│ ├── MessageMenuBar
│ │ ├── MessageMenuBar.css
│ │ ├── MessageMenuBar.test.jsx
│ │ ├── __snapshots__
│ │ │ └── MessageMenuBar.test.jsx.snap
│ │ └── MessageMenuBar.jsx
│ ├── SearchBar
│ │ ├── SearchBar.css
│ │ ├── __snapshots__
│ │ │ └── SearchBar.test.jsx.snap
│ │ ├── SearchBar.test.jsx
│ │ └── SearchBar.jsx
│ ├── InputBar
│ │ ├── InputBar.css
│ │ ├── InputBar.test.jsx
│ │ ├── __snapshots__
│ │ │ └── InputBar.test.jsx.snap
│ │ └── InputBar.jsx
│ ├── MessagePanel
│ │ ├── MessagePanel.css
│ │ ├── MessagePanel.test.jsx
│ │ ├── __snapshots__
│ │ │ └── MessagePanel.test.jsx.snap
│ │ └── MessagePanel.jsx
│ ├── Skeleton
│ │ └── Stateless
│ ├── Message
│ │ ├── __snapshots__
│ │ │ └── Message.test.jsx.snap
│ │ ├── Message.test.jsx
│ │ ├── Message.css
│ │ └── Message.jsx
│ ├── Application
│ │ ├── Application.css
│ │ ├── Application.test.jsx
│ │ ├── __snapshots__
│ │ │ └── Application.test.jsx.snap
│ │ └── Application.jsx
│ ├── OperatorPanel
│ │ ├── OperatorPanel.test.jsx
│ │ ├── __snapshots__
│ │ │ └── OperatorPanel.test.jsx.snap
│ │ ├── OperatorPanel.jsx
│ │ └── OperatorPanel.css
│ ├── OperatorSettingsMenu
│ │ ├── OperatorSettingsMenu.test.jsx
│ │ ├── OperatorSettingsStyles.css
│ │ ├── __snapshots__
│ │ │ └── OperatorSettingsMenu.test.jsx.snap
│ │ └── OperatorSettingsMenu.jsx
│ ├── WelcomeScreen
│ │ ├── WelcomeScreen.test.jsx
│ │ ├── __snapshots__
│ │ │ └── WelcomeScreen.test.jsx.snap
│ │ ├── WelcomeScreen.css
│ │ └── WelcomeScreen.jsx
│ ├── MessageList
│ │ ├── MessageList.test.jsx
│ │ ├── MessageList.css
│ │ ├── __snapshots__
│ │ │ └── MessageList.test.jsx.snap
│ │ └── MessageList.jsx
│ ├── OperatorClientMenu
│ │ ├── OperatorClientMenu.test.jsx
│ │ ├── OperatorClientMenu.css
│ │ ├── __snapshots__
│ │ │ └── OperatorClientMenu.test.jsx.snap
│ │ └── OperatorClientMenu.jsx
│ ├── SVG
│ │ └── BrandLogo.jsx
│ ├── Button
│ │ ├── Button.jsx
│ │ ├── Button.css
│ │ ├── __snapshots__
│ │ │ └── Button.test.jsx.snap
│ │ └── Button.test.jsx
│ ├── SettingsPanel
│ │ ├── SettingsPanel.css
│ │ └── SettingsPanel.jsx
│ ├── ClientCard
│ │ ├── ClientCard.css
│ │ └── ClientCard.jsx
│ └── Toggle
│ │ └── index.css
├── store
│ ├── endpoints.js
│ ├── middleware.js
│ ├── Socket
│ │ └── index.js
│ ├── UI
│ │ └── index.js
│ ├── dummy.js
│ ├── Config
│ │ └── index.js
│ └── Chat
│ │ └── index.js
├── init.jsx
└── socket.js
├── assets
├── icons
│ ├── ss-air.eot
│ ├── ss-air.ttf
│ ├── ss-air.woff
│ └── ss-air_02mf.eot
├── fonts
│ ├── Roboto-Black.ttf
│ ├── Roboto-Bold.ttf
│ ├── Roboto-Light.ttf
│ ├── Roboto-Thin.ttf
│ ├── Roboto-Italic.ttf
│ ├── Roboto-Medium.ttf
│ ├── Roboto-Regular.ttf
│ ├── Roboto-BoldItalic.ttf
│ ├── Roboto-ThinItalic.ttf
│ ├── Roboto-BlackItalic.ttf
│ ├── Roboto-LightItalic.ttf
│ └── Roboto-MediumItalic.ttf
├── images
│ ├── icon_16x16.png
│ ├── icon_32x32.png
│ └── icon_96x96.png
└── css
│ ├── Roboto.css
│ └── ss-air.css
├── .babelrc
├── .gitignore
├── Dockerfile
├── CONTRIBUTING.md
├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── index.html
├── main.js
├── .eslintrc
├── LICENSE
├── server
├── events.js
└── build-menu.js
├── README.md
├── webpack.config.js
├── Makefile
├── Window.js
├── CODE_OF_CONDUCT.md
└── package.json
/OWNERS:
--------------------------------------------------------------------------------
1 | mihok
2 | teesloane
3 | broneks
4 |
--------------------------------------------------------------------------------
/test/style.mock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/src/components/NotificationBar/NotificationBar.text.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/file.mock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/assets/icons/ss-air.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/icons/ss-air.eot
--------------------------------------------------------------------------------
/assets/icons/ss-air.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/icons/ss-air.ttf
--------------------------------------------------------------------------------
/assets/icons/ss-air.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/icons/ss-air.woff
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Black.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Light.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/assets/icons/ss-air_02mf.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/icons/ss-air_02mf.eot
--------------------------------------------------------------------------------
/assets/images/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/images/icon_16x16.png
--------------------------------------------------------------------------------
/assets/images/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/images/icon_32x32.png
--------------------------------------------------------------------------------
/assets/images/icon_96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/images/icon_96x96.png
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-BoldItalic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-ThinItalic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-BlackItalic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-LightItalic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minimalchat/operator-app/HEAD/assets/fonts/Roboto-MediumItalic.ttf
--------------------------------------------------------------------------------
/src/components/ClientList/ClientList.css:
--------------------------------------------------------------------------------
1 | .ClientList__list {
2 | margin: 0;
3 | padding: 0;
4 | height: 100%;
5 | overflow-y: auto;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/ClientsPanel/ClientsPanel.css:
--------------------------------------------------------------------------------
1 | .ClientsPanel {
2 | background: #F7F8FA;
3 | box-shadow: 1px 0 1px rgba(0,0,0,0.1);
4 | display: flex;
5 | flex-direction: column;
6 | flex: 2 0 350px;
7 | z-index: 1;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/TitleBar/TitleBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './TitleBar.css';
3 |
4 | const TitleBar = () => (
5 |
6 | Operator
7 |
8 | );
9 |
10 | export default TitleBar;
11 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ [ "env", {
3 | "targets": {
4 | "electron": ["4"]
5 | },
6 | "useBuiltIns": "usage"
7 | } ] ],
8 | "plugins": [
9 | "@babel/plugin-proposal-class-properties",
10 | "@babel/plugin-transform-react-jsx"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/TitleBar/__snapshots__/TitleBar.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TitleBar matches snapshot 1`] = `
4 |
8 |
9 | Operator
10 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/src/components/TitleBar/TitleBar.css:
--------------------------------------------------------------------------------
1 | #title {
2 | background: #666666;
3 | }
4 |
5 | #title span {
6 | display: block;
7 | padding: 9px 0;
8 | text-transform: uppercase;
9 | text-align: center;
10 | height: 50px;
11 | font-weight: 800;
12 | font-size: 22px;
13 | color: white;
14 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | yarn.lock
3 | dist/
4 | dist/*-linux-armv7l
5 | dist/*-linux-arm64
6 | dist/*-win32-x64
7 | dist/*-mas-x64
8 | dist/*-linux-x64
9 | dist/*-darwin-x64
10 | dist/*-win32-ia32
11 | dist/*-linux-ia32
12 |
13 | assets/js/**
14 | .DS_Store
15 |
16 | config.json
17 |
18 | *.swp
19 | *.swo
20 |
--------------------------------------------------------------------------------
/src/components/MessageMenuBar/MessageMenuBar.css:
--------------------------------------------------------------------------------
1 | .MessageMenuBar {
2 | align-items: stretch;
3 | box-sizing: border-box;
4 | height: 50px;
5 | justify-content: space-around;
6 | padding: 8px;
7 | }
8 |
9 | .MessageMenuBar .Button {
10 | height: 100%;
11 | line-height: inherit;
12 | font-weight: 400;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/SearchBar/SearchBar.css:
--------------------------------------------------------------------------------
1 | .SearchBar {
2 | display: flex;
3 | width: 100%;
4 | height: 50px;
5 | min-height: 50px;
6 | max-height: 50px;
7 | }
8 |
9 | .SearchBar input {
10 | flex-grow: 1;
11 | border: 0;
12 | outline: 0;
13 | font-size: 14px;
14 | padding: 0.25rem 1rem;
15 | color: #555;
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/endpoints.js:
--------------------------------------------------------------------------------
1 | const ENDPOINTS = {};
2 |
3 | ENDPOINTS.operators = '/api/operators';
4 | ENDPOINTS.operator = '/api/operator';
5 |
6 | ENDPOINTS.clients = '/api/clients';
7 | ENDPOINTS.client = '/api/client';
8 |
9 | ENDPOINTS.chats = '/api/chats';
10 | ENDPOINTS.chat = '/api/chat';
11 |
12 | export default ENDPOINTS;
13 |
--------------------------------------------------------------------------------
/src/components/InputBar/InputBar.css:
--------------------------------------------------------------------------------
1 | .InputBar {
2 | display: flex;
3 | background: #fff;
4 | min-height: 50px;
5 | }
6 |
7 | .InputBar input {
8 | flex-grow: 1;
9 | outline: 0;
10 | display: flex;
11 | padding: 1rem;
12 | resize: none;
13 | border: none;
14 | }
15 |
16 | .InputBar .Button--send {
17 | margin: 4px;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/SearchBar/__snapshots__/SearchBar.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SearchBar matches snapshot 1`] = `
4 |
7 |
13 |
14 | `;
15 |
--------------------------------------------------------------------------------
/src/components/ClientsPanel/__snapshots__/ClientsPanel.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ClientsPanel matches snapshot 1`] = `
4 |
7 |
11 |
14 |
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/MessagePanel.css:
--------------------------------------------------------------------------------
1 | .MessagePanel {
2 | display: flex;
3 | flex-direction: column;
4 | flex-grow: 3;
5 | width: 50%; /* not sure if this is the best way*/
6 | }
7 |
8 | .MessagePanel__container {
9 | border: 0;
10 | box-shadow: inset 10px 0 50px -15px #efefef;
11 | display: flex;
12 | flex-direction: column;
13 | flex-grow: 1;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Stateless:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 |
5 | const Skeleton = props => (
6 |
7 | );
8 |
9 |
10 | const mapStateToProps = state => ({
11 | });
12 |
13 | const mapDispatchToProps = dispatch => ({
14 | });
15 |
16 |
17 | export default connect(mapStateToProps, mapDispatchToProps)(Skeleton);
18 |
--------------------------------------------------------------------------------
/src/components/Message/__snapshots__/Message.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MessageList matches snapshot 1`] = `
4 |
12 |
13 |
14 | -
17 | test message
18 |
19 |
20 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/test/jest.setup.js:
--------------------------------------------------------------------------------
1 | // Setup anything for tests (but know that this is skipped by eslinting)
2 | var enzyme = require('enzyme');
3 | var Adapter = require('enzyme-adapter-react-15');
4 |
5 | enzyme.configure({ adapter: new Adapter() });
6 |
7 | // Skip createElement warnings but fail tests on any other warning
8 | console.error = message => {
9 | if (!/(React.createElement: type should not be null)/.test(message)) {
10 | throw new Error(message);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:11
2 |
3 | RUN mkdir -p /tmp
4 | WORKDIR /tmp/operator
5 |
6 | ## Run these together otherwise we have to remember to run it with --no-cache
7 | # occasionally
8 | RUN dpkg --add-architecture i386 && \
9 | apt update && \
10 | apt install -y git build-essential wine wine32 libwine
11 |
12 |
13 | RUN apt autoremove -y
14 |
15 | COPY . .
16 |
17 |
18 | # Build the scripts
19 | RUN make clean dependencies
20 |
21 | CMD ["make", "compile"]
22 |
--------------------------------------------------------------------------------
/src/components/Application/Application.css:
--------------------------------------------------------------------------------
1 | :focus {
2 | outline-offset: 0;
3 | }
4 |
5 | input, button {
6 | font-family: Roboto, sans-serif;
7 | }
8 |
9 | .App {
10 | display: flex;
11 | font-family: Roboto, sans-serif;
12 | }
13 |
14 | .App__mainview {
15 | display: flex;
16 | flex-grow: 2;
17 | position: relative;
18 | }
19 |
20 | .App__settingsview {
21 | background: #e3eaf0;
22 | display: flex;
23 | flex-grow: 1;
24 | flex-direction: column;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/TitleBar/TitleBar.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import TitleBar from './TitleBar.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({ })),
11 | };
12 |
13 | describe('TitleBar', () => {
14 | it('matches snapshot', () => {
15 | const component = shallow();
16 |
17 | expect(component).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/OperatorPanel/OperatorPanel.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import OperatorPanel from './OperatorPanel.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({ })),
11 | };
12 |
13 | describe('OperatorPanel', () => {
14 | it('matches snapshot', () => {
15 | const component = shallow();
16 |
17 | expect(component).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/SearchBar/SearchBar.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 | import SearchBar from './SearchBar.jsx';
5 |
6 | const store = {
7 | subscribe: jest.fn(),
8 | dispatch: jest.fn(),
9 | getState: jest.fn(() => ({ })),
10 | };
11 |
12 | describe('SearchBar', () => {
13 | it('matches snapshot', () => {
14 | const component = shallow( {}} store={store} />);
15 |
16 | expect(component).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/MessagePanel.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import MessagePanel from './MessagePanel.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: { },
12 | })),
13 | };
14 |
15 | describe('MessagePanel', () => {
16 | it('matches snapshot', () => {
17 | const component = shallow();
18 |
19 | expect(component).toMatchSnapshot();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/ClientsPanel/ClientsPanel.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import ClientsPanel from './ClientsPanel.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: { chats: [] },
12 | })),
13 | };
14 |
15 | describe('ClientsPanel', () => {
16 | it('matches snapshot', () => {
17 | const component = shallow();
18 |
19 | expect(component).toMatchSnapshot();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/Message/Message.test.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { shallow } from 'enzyme';
4 | import Message from './Message.jsx';
5 |
6 | /*
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({})),
11 | };
12 | */
13 |
14 |
15 | describe('MessageList', () => {
16 | it('matches snapshot', () => {
17 | const component = shallow();
18 | expect(component).toMatchSnapshot();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/OperatorSettingsMenu/OperatorSettingsMenu.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import OperatorSettingsMenu from './OperatorSettingsMenu.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({ config: {} })),
11 | };
12 |
13 | describe('OperatorSettingsMenu', () => {
14 | it('matches snapshot', () => {
15 | const component = shallow();
16 |
17 | expect(component).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/MessageMenuBar/MessageMenuBar.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import MessageMenuBar from './MessageMenuBar.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: { activeId: '3r23fweefl' },
12 | })),
13 | };
14 |
15 | describe('MessageMenuBar', () => {
16 | it('matches snapshot', () => {
17 | const component = shallow();
18 |
19 | expect(component).toMatchSnapshot();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/WelcomeScreen/WelcomeScreen.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import WelcomeScreen from './WelcomeScreen.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | config: {
12 | apiServer: '',
13 | },
14 | })),
15 | };
16 |
17 | describe('WelcomeScreen', () => {
18 | it('matches snapshot', () => {
19 | const component = shallow();
20 |
21 | expect(component).toMatchSnapshot();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/components/Application/Application.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import Application from './Application.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | ui: {
12 | welcomeScreenOpen: false,
13 | settingsOpen: false,
14 | },
15 | })),
16 | };
17 |
18 | describe('Chat', () => {
19 | it('matches snapshot', () => {
20 | const component = shallow();
21 |
22 | expect(component).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/MessageList/MessageList.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import MessageList from './MessageList.jsx';
4 |
5 | const store = {
6 | subscribe: jest.fn(),
7 | dispatch: jest.fn(),
8 |
9 | getState: jest.fn(() => ({
10 | chat: {
11 | messages: [],
12 | typing: {},
13 | },
14 | config: {},
15 | })),
16 | };
17 |
18 |
19 | describe('MessageList', () => {
20 | it('matches snapshot', () => {
21 | const component = shallow();
22 | expect(component).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/InputBar/InputBar.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import InputBar from './InputBar.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: {
12 | activeId: 'TEST',
13 | },
14 | config: {
15 | operator: 'TEST',
16 | },
17 | })),
18 | };
19 |
20 | describe('InputBar', () => {
21 | it('matches snapshot', () => {
22 | const component = shallow();
23 |
24 | expect(component).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/SearchBar/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import './SearchBar.css';
4 |
5 | const SearchBar = (props) => {
6 | const { query, onQueryChange } = props;
7 |
8 | return (
9 |
10 |
16 |
17 | );
18 | };
19 |
20 | SearchBar.propTypes = {
21 | query: PropTypes.string.isRequired,
22 | onQueryChange: PropTypes.func.isRequired,
23 | };
24 |
25 | export default SearchBar;
26 |
--------------------------------------------------------------------------------
/src/components/OperatorClientMenu/OperatorClientMenu.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import OperatorClientMenu from './OperatorClientMenu.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: {
12 | chats: [],
13 | operatorFilter: 'all',
14 | },
15 | })),
16 | };
17 |
18 | describe('OperatorClientMenu', () => {
19 | it('matches snapshot', () => {
20 | const component = shallow();
21 |
22 | expect(component).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing guidelines
2 |
3 | ## How to become a contributor and submit your own code
4 |
5 | ### Contributing A Patch
6 |
7 | 1. Submit an issue describing your proposed change to the repo in question.
8 | 1. The [repo owners](OWNERS) will respond to your issue promptly.
9 | 1. If instructed by the repo owners provide a short design document in a PR.
10 | 1. Fork the desired repo, develop and test your code changes. Unit tests are required for most PRs.
11 | 1. Submit a pull request.
12 |
13 | ## Bug reporting
14 |
15 | If you think you found a bug, please open a [new issue](https://github.com/minimalchat/operator-app/issues/new)
16 |
--------------------------------------------------------------------------------
/src/components/OperatorSettingsMenu/OperatorSettingsStyles.css:
--------------------------------------------------------------------------------
1 | .SettingsMenu {
2 | width: 100%;
3 | }
4 |
5 | .Settings__box {
6 | display: flex;
7 | background: #F7F8FA;
8 | border-top: 1px solid #E8ECEE;
9 | border-bottom: 1px solid #E8ECEE;
10 | justify-content: flex-start;
11 | width: 100%;
12 | font-size: 12px;
13 | color: #8f8f8f;
14 | align-items: center;
15 | }
16 |
17 | .Settings__avatar {
18 | margin: 1rem;
19 | border-radius: 50%;
20 | border: 3px solid white;
21 | box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15);
22 | width: 58px;
23 | height: 58px;
24 | overflow: hidden;
25 | }
26 |
27 | .Settings__avatar img {
28 | width: 100%;
29 | }
30 |
31 | .Settings__operator {
32 | word-break: break-word;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ClientsPanel/ClientsPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import SearchBar from '../SearchBar/SearchBar.jsx';
4 | import ClientList from '../ClientList/ClientList.jsx';
5 | import './ClientsPanel.css';
6 |
7 |
8 | class ClientsPanel extends Component {
9 | state = { query: '' }
10 |
11 | onQueryChange = (event) => {
12 | const query = event.target.value;
13 | this.setState({ query });
14 | }
15 |
16 | render () {
17 | const { query } = this.state;
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default ClientsPanel;
29 |
--------------------------------------------------------------------------------
/src/components/OperatorPanel/__snapshots__/OperatorPanel.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`OperatorPanel matches snapshot 1`] = `
4 |
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/ClientList/ClientList.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { shallow } from 'enzyme';
4 |
5 | import ClientList from './ClientList.jsx';
6 |
7 | const store = {
8 | subscribe: jest.fn(),
9 | dispatch: jest.fn(),
10 | getState: jest.fn(() => ({
11 | chat: {
12 | operatorFilter: 'all',
13 | chats: {
14 | 'id-0': { client: { first_name: 'Robert', last_name: 'waffle' } },
15 | 'id-1': { client: { first_name: 'Lisa', last_name: 'pancake' } },
16 | },
17 | config: {},
18 | },
19 | })),
20 | };
21 |
22 |
23 | describe('ClientList', () => {
24 | const query = '';
25 | it('matches snapshot', () => {
26 | const component = shallow();
27 | expect(component).toMatchSnapshot();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/components/SVG/BrandLogo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const BrandLogo = props => (
4 |
15 | );
16 |
17 | export default BrandLogo;
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | - `node` version:
12 | - `npm` (or `yarn`) version:
13 |
14 | Relevant code or config
15 |
16 | ```javascript
17 |
18 | ```
19 |
20 | What you did:
21 |
22 |
23 |
24 | What happened:
25 |
26 |
27 |
28 | Reproduction repository:
29 |
30 |
34 |
35 | Problem description:
36 |
37 |
38 |
39 | Suggested solution:
40 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Operator
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const Electron = require('electron');
2 | const Window = require('./Window.js');
3 |
4 | // Module to control application life.
5 | const app = Electron.app;
6 |
7 | // This method will be called when Electron has finished
8 | // initialization and is ready to create browser windows.
9 | // Some APIs can only be used after this event occurs.
10 | app.on('ready', Window.onReady);
11 |
12 | // Quit when all windows are closed.
13 | app.on('window-all-closed', function () {
14 | // On OS X it is common for applications and their menu bar
15 | // to stay active until the user quits explicitly with Cmd + Q
16 | if (process.platform !== 'darwin') {
17 | app.quit();
18 | }
19 | });
20 |
21 | app.on('activate', function () {
22 | // On OS X it's common to re-create a window in the app when the
23 | // dock icon is clicked and there are no other windows open.
24 | if (Window.isNull()) {
25 | Window.create();
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "jest": true,
7 | },
8 | "rules": {
9 | "arrow-body-style": ["error", "as-needed"],
10 | "camelcase": ["warn"],
11 | "class-methods-use-this": ["warn"],
12 | "space-before-function-paren": ["error", "always"],
13 | "prefer-const": ["warn"],
14 | "import/no-extraneous-dependencies": [
15 | "error", { "devDependencies": ["**/*.test.js", "**/*.test.jsx", "**/*.spec.js", "**/*.spec.jsx"]}
16 | ],
17 | "import/extensions": ["error", { "jsx": "always" }],
18 | "no-unused-vars": ["warn"],
19 | "no-console": ["warn"],
20 | "no-prototype-builtins": ["off"],
21 | "no-multiple-empty-lines": ["off"],
22 | "no-use-before-define": ["warn"],
23 | "react/no-multi-comp": ["warn"],
24 | "react/jsx-no-bind": ["warn"],
25 | "react/forbid-prop-types": ["warn"],
26 | "react/button-has-type": ["warn"],
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/MessageList/MessageList.css:
--------------------------------------------------------------------------------
1 | .MessageList {
2 | background: #e3eaf0;
3 | flex-grow: 1;
4 | align-content: center;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | }
9 |
10 | .MessageList__empty {
11 | display: flex;
12 | flex-direction: column;
13 | flex-grow: 1;
14 | justify-content: center;
15 | align-self: center;
16 | text-align: center;
17 | align-items: center;
18 | color: rgba(0, 0, 0, 0.2);
19 | }
20 |
21 | .lil-ghost, .lil-mailbox {
22 | font-size: 40px;
23 | }
24 |
25 | .MessageList__box {
26 | flex-grow: 1;
27 | overflow-y: scroll;
28 | padding-left: 0;
29 | padding-right: 0.25rem;
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: flex-end;
33 | padding-bottom: 12px;
34 | }
35 |
36 | .MessageList__box--done {
37 | opacity: 0.5;
38 | }
39 |
40 | .MessageList__status {
41 | position: absolute;
42 | bottom: 60px;
43 | min-height: 14px; /* size of status message text */
44 | padding: 8px;
45 | margin-right: 16px;
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/OperatorClientMenu/OperatorClientMenu.css:
--------------------------------------------------------------------------------
1 | .OperatorClientMenu {
2 | display: flex;
3 | flex-direction: column;
4 | flex-grow: 1;
5 | padding: 0;
6 | margin: 0;
7 | }
8 |
9 | .OperatorClient__Menu__item {
10 | padding: 0;
11 | margin: 0;
12 | list-style-type: none;
13 | }
14 |
15 | .OperatorClient__Menu__item:hover {
16 | background: #fafafa;
17 | }
18 |
19 | .OperatorClient__selectedFilter {
20 | display: flex;
21 | background: #fafafa;
22 | border: none;
23 | outline: 0;
24 | height: 100%;
25 | width: 100%;
26 | padding: 15px 15px 15px 15px;
27 | border-left: 3px solid rgba(231, 76, 60, 1.0);
28 | }
29 |
30 | .OperatorClient__filter {
31 | display: flex;
32 | background: #fdfdfd;
33 | border: none;
34 | outline: 0;
35 | padding: 15px 15px 15px 15px;
36 | height: 100%;
37 | width: 100%;
38 | cursor: pointer;
39 | border-left: 3px solid #fdfdfd;
40 | }
41 |
42 | .OperatorClient__filter:hover {
43 | background: #fafafa;
44 | border-left: 3px solid rgba(231, 76, 60, 1.0);
45 | }
46 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | ## Description
19 |
20 |
21 | ### Motivation
22 |
23 |
24 | ### Changes
25 |
26 | -
27 | -
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/src/store/middleware.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_MESSAGE, setActiveChat } from './Chat/index';
2 |
3 |
4 | export const logger = store => next => (action) => {
5 | console.group(action.type);
6 | console.info('dispatching', action);
7 | console.log('next state', store.getState());
8 | console.groupEnd(action.type);
9 | return next(action);
10 | };
11 |
12 | export const notifications = store => next => (action) => {
13 | const messageLength = 80;
14 | if (action.type === RECEIVE_MESSAGE) {
15 | if (window.config.notificationsEnabled) {
16 | const newMessageNotification = new Notification('New Message', {
17 | body: `${action.payload.content.substring(0, messageLength)}${action.payload.content.length > messageLength ? '...' : ''}`,
18 | });
19 |
20 | newMessageNotification.onclick = () => {
21 | store.dispatch(setActiveChat({ id: action.payload.chat, open: true }));
22 | };
23 |
24 | try {
25 | newMessageNotification.show();
26 | } catch (e) {
27 | // TODO: Manage errors better?
28 | }
29 | }
30 | }
31 | return next(action);
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/__snapshots__/MessagePanel.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MessagePanel matches snapshot 1`] = `
4 |
45 | `;
46 |
--------------------------------------------------------------------------------
/src/components/OperatorSettingsMenu/__snapshots__/OperatorSettingsMenu.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`OperatorSettingsMenu matches snapshot 1`] = `
4 |
47 | `;
48 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/MessagePanel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | import MessageMenuBar from '../MessageMenuBar/MessageMenuBar.jsx';
6 | import MessageList from '../MessageList/MessageList.jsx';
7 | import InputBar from '../InputBar/InputBar.jsx';
8 | import './MessagePanel.css';
9 |
10 | const MessagePanel = (props) => {
11 | const { activeId } = props;
12 | const renderView = () => {
13 | if (!activeId) {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | return renderView();
31 | };
32 |
33 | MessageList.propTypes = {
34 | activeId: PropTypes.string,
35 | };
36 |
37 |
38 | const mapStateToProps = state => ({
39 | activeId: state.chat.activeId,
40 | });
41 |
42 | const mapDispatchToProps = dispatch => ({
43 | dispatch,
44 | });
45 |
46 |
47 | export default connect(
48 | mapStateToProps,
49 | mapDispatchToProps,
50 | )(MessagePanel);
51 |
--------------------------------------------------------------------------------
/src/components/OperatorSettingsMenu/OperatorSettingsMenu.jsx:
--------------------------------------------------------------------------------
1 | /** OperatorSettingsMenu
2 | * Displays informaiton about the operator.
3 | * Might eventually hold a menu settings button?
4 | */
5 |
6 | import React from 'react';
7 | import PropTypes from 'prop-types';
8 | import { connect } from 'react-redux';
9 | import './OperatorSettingsStyles.css';
10 |
11 | const OperatorSettingsMenu = ({ avatar, operatorName }) => (
12 |
13 |
14 |
15 |

19 |
20 |
{operatorName}
21 |
22 |
23 | );
24 |
25 | OperatorSettingsMenu.propTypes = {
26 | operatorName: PropTypes.string,
27 | avatar: PropTypes.string,
28 | };
29 |
30 | OperatorSettingsMenu.defaultProps = {
31 | operatorName: '',
32 | avatar: null,
33 | };
34 |
35 | const mapStateToProps = state => ({
36 | operatorName: state.config.operator,
37 | avatar: state.config.avatar,
38 | });
39 |
40 | export default connect(
41 | mapStateToProps,
42 | )(OperatorSettingsMenu);
43 |
--------------------------------------------------------------------------------
/src/components/WelcomeScreen/__snapshots__/WelcomeScreen.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`WelcomeScreen matches snapshot 1`] = `
4 |
49 | `;
50 |
--------------------------------------------------------------------------------
/src/components/WelcomeScreen/WelcomeScreen.css:
--------------------------------------------------------------------------------
1 | .WelcomeScreen.screen {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | background: #2c3e50;
9 | }
10 |
11 | .WelcomeScreen__header {
12 | width: 33%;
13 | min-width: 256px;
14 | margin: 18px 0 96px;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | font-family: Roboto, sans-serif;
19 | font-weight: 600;
20 | font-size: 13px;
21 | font-weight: 500;
22 | letter-spacing: 2px;
23 | text-transform: uppercase;
24 | color: white;
25 | }
26 |
27 | .WelcomeScreen__header .SVG__logo {
28 | height: 20px;
29 | margin-right: 10px;
30 | margin-bottom: 0;
31 | fill: rgba(255, 255, 255, 1.0);
32 | }
33 |
34 | .WelcomeScreen__body {
35 | width: 33%;
36 | min-width: 256px;
37 | max-width: 512px;
38 | }
39 |
40 | .WelcomeScreen__wrapper {
41 | position: relative;
42 | }
43 |
44 | .WelcomeScreen__wrapper input {
45 | width: 100%;
46 | height: 42px;
47 | box-sizing: border-box;
48 | padding: 14px;
49 | border-radius: 2px;
50 | border: 1px solid #ffffff;
51 | }
52 |
53 | .WelcomeScreen__wrapper input:focus {
54 | outline: none;
55 | }
56 |
57 | .WelcomeScreen__wrapper .Button {
58 | position: absolute;
59 | right: 3px;
60 | top: 3px;
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/MessageMenuBar/__snapshots__/MessageMenuBar.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MessageMenuBar matches snapshot 1`] = `
4 |
51 | `;
52 |
--------------------------------------------------------------------------------
/src/components/Application/__snapshots__/Application.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Chat matches snapshot 1`] = `
4 |
52 | `;
53 |
--------------------------------------------------------------------------------
/src/components/MessageMenuBar/MessageMenuBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Button from '../Button/Button.jsx';
5 | import { toggleChatOpen } from '../../store/Chat';
6 | import './MessageMenuBar.css';
7 |
8 |
9 | const MessageMenuBar = (props) => {
10 | const { activeChatId, activeChatIsOpen, toggleOpen } = props;
11 | const renderBtnMsg = () => {
12 | if (!activeChatId) return 'Select a chat';
13 | if (activeChatId && activeChatIsOpen) return 'Mark as Done';
14 | return 'Unarchive Chat';
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | };
23 |
24 |
25 | MessageMenuBar.propTypes = {
26 | activeChatId: PropTypes.string.isRequired,
27 | activeChatIsOpen: PropTypes.bool,
28 | toggleOpen: PropTypes.func.isRequired,
29 | };
30 |
31 | MessageMenuBar.defaultProps = {
32 | activeChatIsOpen: false,
33 | };
34 |
35 |
36 | const mapStateToProps = state => ({
37 | activeChatId: state.chat.activeId,
38 | activeChatIsOpen: state.chat.activeIsOpen,
39 | });
40 |
41 | const mapDispatchToProps = dispatch => ({
42 | toggleOpen: chatId => dispatch(toggleChatOpen(chatId)),
43 | });
44 |
45 |
46 | export default connect(
47 | mapStateToProps,
48 | mapDispatchToProps,
49 | )(MessageMenuBar);
50 |
--------------------------------------------------------------------------------
/src/components/OperatorClientMenu/__snapshots__/OperatorClientMenu.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`OperatorClientMenu matches snapshot 1`] = `
4 |
53 | `;
54 |
--------------------------------------------------------------------------------
/src/components/NotificationBar/NotificationBar.css:
--------------------------------------------------------------------------------
1 | .NotificationBar {
2 | z-index: 2;
3 | display: flex;
4 | position: absolute;
5 | top: -50px;
6 | left: 0;
7 | right: 0;
8 | height: 50px;
9 | width: 100%;
10 | transition: top ease 500ms;
11 | align-items: center;
12 | text-align: center;
13 | justify-content: center;
14 | color: rgba(255,255,255,1);
15 | }
16 |
17 | .NotificationBar--active {
18 | top: 0;
19 | }
20 |
21 | .NotificationBar--red {
22 | background: rgba(235, 110, 96, 1);
23 | }
24 |
25 | .NotificationBar--green {
26 | background: rgba(137, 220, 203, 1);
27 | }
28 |
29 | .NotificationBar--orange {
30 | background: rgba(235, 190, 117, 1);
31 | }
32 |
33 |
34 | .NotificationBar__status {
35 | flex-grow: 1;
36 | display: flex;
37 | align-items: center;
38 | font-size: 14px;
39 | }
40 |
41 | .NotificationBar__icon {
42 | padding: 11px 8px 8px 8px;
43 | box-sizing: border-box;
44 | height: 50px;
45 | width: 36px;
46 | font-size: 24px;
47 | float: left;
48 | margin: 0 8px;
49 | }
50 |
51 | .NotificationBar__close {
52 | position: absolute;
53 | text-align: right;
54 | box-sizing: border-box;
55 | padding: 8px;
56 | font-size: 32px;
57 | line-height: 12px;
58 | height: 100%;
59 | display: flex;
60 | align-items: center;
61 | justify-content: flex-end;
62 | padding-top: 16px;
63 | cursor: pointer;
64 | color: rgba(230, 126, 34,1.0);
65 | right: 0;
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import './Button.css';
4 |
5 | const iconRenderer = iconClassName => (
6 |
7 | );
8 |
9 | const Button = (props) => {
10 | const {
11 | icon, type, variant, children, onClick, disabled,
12 | } = props;
13 | const isIconButton = icon != null;
14 |
15 | const content = isIconButton ? iconRenderer(icon) : children;
16 |
17 | const buttonVariant = isIconButton ? 'icon' : Button.variants[variant];
18 | const buttonVariantClassName = buttonVariant ? `Button--${buttonVariant}` : '';
19 |
20 | return (
21 |
29 | );
30 | };
31 |
32 | Button.variants = {
33 | icon: 'icon',
34 | send: 'send',
35 | primary: 'primary',
36 | transparent: 'transparent',
37 | };
38 |
39 | Button.propTypes = {
40 | onClick: PropTypes.func,
41 | children: PropTypes.node,
42 | type: PropTypes.string,
43 | variant: PropTypes.string,
44 | icon: PropTypes.string,
45 | disabled: PropTypes.bool,
46 | };
47 |
48 | Button.defaultProps = {
49 | type: 'button',
50 | variant: '',
51 | disabled: false,
52 | children: null,
53 | onClick: null,
54 | icon: null,
55 | };
56 |
57 |
58 | export default Button;
59 |
--------------------------------------------------------------------------------
/src/components/Message/Message.css:
--------------------------------------------------------------------------------
1 | .Message__client {
2 | align-self: flex-end;
3 | background: #ffffff;
4 | border-radius: 10px 10px 0 10px;
5 | color: #000;
6 | float: left;
7 | font-size: 12px;
8 | letter-spacing: 1px;
9 | list-style: none;
10 | margin-top: 6px;
11 | margin-right: 6px;
12 | /* account for absolutely position time of messages below msg: */
13 | margin-bottom: 24px;
14 | max-width: 45%;
15 | min-width: 25%;
16 | padding: 10px;
17 | text-align: left;
18 | position: relative;
19 | }
20 |
21 | .Message__client ul li {
22 | text-align: right;
23 | }
24 |
25 | .Message__client.typing {
26 | padding: 3px 10px 3px 10px;
27 | min-width: auto;
28 | }
29 |
30 | .Message__time {
31 | position: absolute;
32 | bottom: -14px;
33 | right: 0px;
34 | color: rgba(149, 165, 166,1.0);
35 | font-size: 9px;
36 | font-style: italic;
37 | width: 94px;
38 | text-align: right;
39 | letter-spacing: 0.4px;
40 | }
41 |
42 | .Message__operator {
43 | align-self: flex-start;
44 | background: #0a6bef;
45 | border-radius: 10px 10px 10px 0;
46 | color: white;
47 | font-size: 12px;
48 | letter-spacing: 1px;
49 | list-style: none;
50 | padding: 0.5rem;
51 | margin: 0.5rem;
52 | max-width: 45%;
53 | min-width: 25%;
54 | }
55 |
56 | .Message__operator ul,
57 | .Message__client ul {
58 | list-style: none;
59 | padding: 0;
60 | margin: 0;
61 | }
62 |
63 | .Message__operator ul li {
64 | text-align: left;
65 | }
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Matthew Mihok
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/src/components/NotificationBar/NotificationBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | import './NotificationBar.css';
6 |
7 | const NotificationBar = (props) => {
8 | const {
9 | notification,
10 | notificationIcon,
11 | notificationColour,
12 | } = props;
13 |
14 | const classes = [
15 | 'NotificationBar',
16 | `NotificationBar--${notificationColour}`,
17 | notification !== '' ? 'NotificationBar--active' : '',
18 | ];
19 |
20 | return (
21 |
22 |
23 |
24 | {notificationIcon}
25 |
26 | {notification}
27 |
28 | {/* Close */}
29 |
30 | );
31 | };
32 |
33 | NotificationBar.defaultProps = {
34 | notificationIcon: 'info',
35 | notificationColour: 'green',
36 | };
37 |
38 | NotificationBar.propTypes = {
39 | notification: PropTypes.string.isRequired,
40 | notificationIcon: PropTypes.string,
41 | notificationColour: PropTypes.string,
42 | };
43 |
44 | const mapStateToProps = state => ({
45 | notification: state.ui.notification,
46 | notificationIcon: state.ui.notificationIcon,
47 | notificationColour: state.ui.notificationColour,
48 | });
49 |
50 | export default connect(mapStateToProps, null)(NotificationBar);
51 |
--------------------------------------------------------------------------------
/src/components/InputBar/__snapshots__/InputBar.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`InputBar matches snapshot 1`] = `
4 |
58 | `;
59 |
--------------------------------------------------------------------------------
/src/components/SettingsPanel/SettingsPanel.css:
--------------------------------------------------------------------------------
1 | .Settings {
2 | display: flex;
3 | flex-direction: column;
4 | flex-grow: 2;
5 | }
6 |
7 | .Settings__header {
8 | font-size: 16px;
9 | display: flex;
10 | align-items: center;
11 | padding: 0 2rem;
12 | height: 50px; /* helps align with minimal chat top left bar thing */
13 | color: #595959;
14 | border-bottom: 1px solid #D5D5D5;
15 | }
16 |
17 | .Settings__body {
18 | margin: 2rem;
19 | border-radius: 2px;
20 | display: flex;
21 | flex-direction: column;
22 | }
23 |
24 | .Settings__single {
25 | display: flex;
26 | margin: 1rem 0;
27 | align-content: center;
28 | justify-content: space-between;
29 | }
30 |
31 | .Settings__notification-label {
32 | font-size: 18px;
33 | color: #919191;
34 | align-self: center;
35 | }
36 |
37 | .Settings__disconnect-link .Button {
38 | font-size: 14px;
39 | font-weight: 400;
40 | align-self: center;
41 | padding: 0;
42 | margin: 0;
43 | border: 0;
44 | color: #c0392b;
45 | }
46 |
47 | .Settings__disconnect-link .Button:hover {
48 | border: 0;
49 | color: #c0392b;
50 | }
51 |
52 | .Settings__operator-label {
53 | font-size: 18px;
54 | color: #919191;
55 | align-self: center;
56 | padding-right: 1rem;
57 | flex-grow: 3;
58 | }
59 |
60 | .Settings__operator-name {
61 | outline: 0;
62 | display: flex;
63 | padding: 0.8rem;
64 | resize: none;
65 | border: none;
66 | border-radius: 2px;
67 | flex-grow: 2;
68 | }
69 |
70 | .Settings__operator-name:focus {
71 | outline-offset: 0;
72 | }
73 |
--------------------------------------------------------------------------------
/server/events.js:
--------------------------------------------------------------------------------
1 | // This file handles the functions to call when certain events are received by the ipc renderer.
2 | const path = require('path');
3 | const fs = require('fs')
4 |
5 | const configPath = path.join(__dirname, '../config.json');
6 |
7 | // creates config file if it doesn't already exist
8 | // InitConfig creates a config if it does not exist;
9 | // Config must be mirrored in the client app for passing config payloads from client<->server
10 | // TODO: move the initialConfig to another file / share between client / server
11 | // TODO: Convert *sync to asynchronous file ops (faster?)
12 | function initConfig (event) {
13 | let config = null;
14 |
15 | if (!fs.existsSync(configPath)) {
16 | const fd = fs.openSync(configPath, 'w');
17 |
18 | const initialConfig = {
19 | apiServer: '',
20 | operator: '',
21 | notificationsEnabled: true,
22 | };
23 |
24 | fs.writeSync(fd, JSON.stringify(initialConfig, null, ' '), 0, 'utf8');
25 | fs.closeSync(fd);
26 | }
27 |
28 | config = JSON.parse(fs.readFileSync(configPath).toString());
29 | event.sender.send('config', config);
30 | }
31 |
32 | // Handle changing the settings via front end `config.index` reducer
33 | // Writes to file and then sends payload to front end via ipc event.
34 | function updateSettings(event, payload) {
35 | fs.writeFile(configPath, JSON.stringify(payload, null, 2), function (err) {
36 | if (err) return console.log(err);
37 | event.sender.send('config', payload)
38 | });
39 | }
40 |
41 | module.exports = {
42 | initConfig,
43 | updateSettings
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/MessageList/__snapshots__/MessageList.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MessageList matches snapshot 1`] = `
4 |
59 | `;
60 |
--------------------------------------------------------------------------------
/src/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | .Button {
2 | user-select: none;
3 | cursor: pointer;
4 | padding: 3px 10px;
5 | margin: 0;
6 | outline: none;
7 | white-space: nowrap;
8 | line-height: 38px;
9 | font-weight: 500;
10 | color: #555;
11 | background: linear-gradient(-180deg, #fff 0%, #f6f7fa 90%);
12 | border: 1px solid #d7dce6;
13 | border-radius: 3px;
14 | }
15 |
16 | .Button:disabled {
17 | cursor: default;
18 | color: #bbb;
19 | }
20 |
21 | .Button:not(:disabled):hover{
22 | color: #222;
23 | border: 1px solid #c7cedc;
24 | background: #f6f7fa;
25 | }
26 |
27 | .Button:not(:disabled):active {
28 | background: #e5e6ea;
29 | }
30 |
31 |
32 | .Button--icon {
33 | background: transparent;
34 | width: 20px;
35 | height: 20px;
36 | font-size: 20px;
37 | line-height: 20px;
38 | border: none;
39 | padding: 0;
40 | }
41 | .Button--icon:not(:disabled):hover,
42 | .Button--icon:not(:disabled):active {
43 | color: #222;
44 | border: none;
45 | background: transparent;
46 | }
47 |
48 | .Button--send {
49 | background: #27ae60;
50 | color: #fff;
51 | line-height: 13px;
52 | height: 36px;
53 | }
54 | .Button--send:not(:disabled):hover,
55 | .Button--send:not(:disabled):active {
56 | background: #229955;
57 | color: #fff;
58 | }
59 |
60 | .Button--transparent {
61 | border-color: transparent;
62 | background: transparent;
63 | color: #2980b9;
64 | }
65 | .Button--transparent:not(:disabled):hover,
66 | .Button--transparent:not(:disabled):active {
67 | border-color: transparent;
68 | background: transparent;
69 | color: #2472a4;
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/Button/__snapshots__/Button.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Button matches basic button snapshot 1`] = `
4 |
12 | `;
13 |
14 | exports[`Button matches disabled button snapshot 1`] = `
15 |
21 | `;
22 |
23 | exports[`Button matches explicit icon button snapshot 1`] = `
24 |
34 | `;
35 |
36 | exports[`Button matches icon button snapshot 1`] = `
37 |
47 | `;
48 |
49 | exports[`Button matches send button snapshot 1`] = `
50 |
56 | `;
57 |
58 | exports[`Button matches submit button snapshot 1`] = `
59 |
65 | `;
66 |
67 | exports[`Button matches transparent button snapshot 1`] = `
68 |
74 | `;
75 |
--------------------------------------------------------------------------------
/src/components/Button/Button.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Button from './Button.jsx';
4 |
5 | describe('Button', () => {
6 | it('matches basic button snapshot', () => {
7 | const component = shallow();
8 |
9 | expect(component).toMatchSnapshot();
10 | });
11 |
12 | it('matches submit button snapshot', () => {
13 | const component = shallow(