├── .babelrc ├── .coveralls.yml ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── config ├── theme.json └── theme.json.template ├── docs └── socket-types.md ├── example ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html └── stockphoto-01.jpg ├── package-lock.json ├── package.json ├── src ├── components │ ├── App │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── functions.js │ │ ├── functions.test.js │ │ ├── index.js │ │ ├── index.test.js │ │ ├── storage.js │ │ └── styles.css │ ├── Chat │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── styles.css │ ├── ClosedState │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── chaticon.svg │ │ ├── index.js │ │ ├── index.test.js │ │ └── styles.css │ ├── Header │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── style.css │ ├── Input │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── styles.css │ ├── Message │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── styles.css │ ├── Notification │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── style.css │ ├── Skeleton │ │ ├── index.js │ │ └── styles.css │ └── ThemeProvider │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ └── styles.css └── index.js ├── test ├── client.load.yaml └── setup.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "@babel/preset-env" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-class-properties"], 8 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: nv9jFmvkNbGa5rdrXCWpGKlgdcOZXaXFL 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .github 4 | docs 5 | example 6 | .cmds 7 | *.swp 8 | *.swo 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.test.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb", 5 | "prettier" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "jest": true 11 | }, 12 | "settings": { 13 | "react": { 14 | "pragma": "h" 15 | } 16 | }, 17 | "rules": { 18 | "arrow-body-style": ["error", "as-needed"], 19 | "class-methods-use-this": ["warn"], 20 | "space-before-function-paren": ["error", "always"], 21 | "prefer-const": ["warn"], 22 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 23 | "no-unused-vars": ["warn"], 24 | "no-console": ["warn"], 25 | "no-prototype-builtins": ["off"], 26 | "no-multiple-empty-lines": ["off"], 27 | "no-use-before-define": ["warn"], 28 | "react/react-in-jsx-scope": 0, 29 | "react/no-multi-comp": ["warn"], 30 | "react/prefer-stateless-function": ["warn"], 31 | "react/no-array-index-key": ["warn"], 32 | "jsx-a11y/no-static-element-interactions": ["warn"], 33 | "import/extensions": ["warn", "always", { 34 | js: "never", 35 | mjs: "never", 36 | }], 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .DS_Store 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | yarn.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | build 31 | dist 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | 44 | # webpack output 45 | dist 46 | 47 | *.swo 48 | *.swp 49 | 50 | /node_modules 51 | /npm-debug.log 52 | /build 53 | .DS_Store 54 | /coverage 55 | /.idea 56 | yarn.lock 57 | /.vscode 58 | *.swp 59 | *.swo 60 | /dist/*.js 61 | /dist/*.js.map 62 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | before_script: 5 | - npm run lint 6 | script: make 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Minimal Chat Community Code of Conduct 2 | 3 | ## Contributor Code of Conduct 4 | 5 | ### Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, gender identity and expression, level of experience, 11 | nationality, personal appearance, race, religion, or sexual identity and 12 | orientation. 13 | 14 | ### Our Standards 15 | 16 | Examples of behavior that contributes to creating a positive environment 17 | include: 18 | 19 | * Using welcoming and inclusive language 20 | * Being respectful of differing viewpoints and experiences 21 | * Gracefully accepting constructive criticism 22 | * Focusing on what is best for the community 23 | * Showing empathy towards other community members 24 | 25 | Examples of unacceptable behavior by participants include: 26 | 27 | * The use of sexualized language or imagery and unwelcome sexual attention or 28 | advances 29 | * Trolling, insulting/derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or electronic 32 | address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ### Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ### Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ### Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at contact@minimal.chat. All 61 | complaints will be reviewed and investigated and will result in a response that 62 | is deemed necessary and appropriate to the circumstances. The project team is 63 | obligated to maintain confidentiality with regard to the reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ### Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at [http://contributor-covenant.org/version/1/4][version] 74 | 75 | [homepage]: http://contributor-covenant.org 76 | [version]: http://contributor-covenant.org/version/1/4/ 77 | -------------------------------------------------------------------------------- /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/client/issues/new) 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11 2 | 3 | RUN mkdir -p /tmp 4 | WORKDIR /tmp/client 5 | 6 | ## Run these together otherwise we have to remember to run it with --no-cache 7 | # occasionally 8 | RUN apt update && \ 9 | apt install -y git build-essential 10 | 11 | 12 | RUN apt autoremove -y 13 | 14 | # RUN git clone https://github.com/minimalchat/client.git /tmp/client 15 | COPY . . 16 | 17 | ENV REMOTE_HOST "localhost:8000" 18 | 19 | # ENV DIGITAL_OCEAN_SPACES_KEY 20 | # ENV CLIENT_KEY 21 | 22 | 23 | # TODO: Is this the best way to go about supplying the theme details? 24 | # ENV CLIENT_THEME_PRIMARY_COLOUR 25 | 26 | # Build the scripts 27 | RUN make dependencies 28 | 29 | CMD ["make", "compile"] 30 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Commands 2 | NPM_CMD ?= npm 3 | 4 | .PHONY: coverage test 5 | 6 | default: test lint coverage compile 7 | 8 | run: test format lint coverage compile start 9 | 10 | load: 11 | $(NPM_CMD) run load 12 | 13 | lint: 14 | $(NPM_CMD) run lint 15 | 16 | format: 17 | $(NPM_CMD) run format 18 | 19 | compile: 20 | mkdir -p dist 21 | $(NPM_CMD) run build:production 22 | 23 | coverage: 24 | $(NPM_CMD) run coverage 25 | 26 | test: 27 | $(NPM_CMD) test 28 | 29 | dependencies: 30 | $(NPM_CMD) install 31 | 32 | start: 33 | $(NPM_CMD) start 34 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | mihok 2 | teesloane 3 | broneks 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal Chat client script 2 | 3 | [![Build Status](https://travis-ci.org/minimalchat/client.svg?branch=master)](https://travis-ci.org/minimalchat/client) 4 | [![Coverage Status](https://coveralls.io/repos/github/minimalchat/client/badge.svg?branch=master)](https://coveralls.io/github/minimalchat/client?branch=master) 5 | 6 | --- 7 | 8 | Minimal Chat is an open source live chat system providing live one on one messaging to a website visitor and an operator. 9 | 10 | Minimal Chat is: 11 | - **minimal**: simple, lightweight, accessible 12 | - **extensible**: modular, pluggable, hookable, composable 13 | 14 | We're glad you're interested in contributing, feel free to create an [issue](https://github.com/minimalchat/client/issues/new) or pick one up but first check out our [contributing doc](https://github.com/minimalchat/client/blob/master/CONTRIBUTING.md) and [code of conduct](https://github.com/minimalchat/client/blob/master/CODE_OF_CONDUCT.md). Check out our [design documentation](https://github.com/minimalchat/client/wiki/Design-Documentation) as well. 15 | 16 | Screenshot 17 | --- 18 | ![client-screenshot-2](https://user-images.githubusercontent.com/563301/32126537-35036eb6-bb3f-11e7-9c33-a1f9fa602601.png) 19 | 20 | 21 | 22 | --- 23 | 24 | The client script is embedded into a html page just before the `` tag. 25 | 26 | ### Usage 27 | 28 | ```javascript 29 | 30 | 37 | 38 | ``` 39 | 40 | ### Development 41 | 42 | Developing for the client is fairly straight forward. All of the Minimal Chat repositories are run through `make`. To get the code running: 43 | 44 | 1. Clone the repository 45 | 2. `make dependencies` 46 | 3. `make run` 47 | 4. Browse to http://localhost:3000 48 | -------------------------------------------------------------------------------- /config/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "primary_colour": "#34495e" 3 | } 4 | -------------------------------------------------------------------------------- /config/theme.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "primaryColour": "PRIMARY_COLOUR" 3 | } 4 | -------------------------------------------------------------------------------- /docs/socket-types.md: -------------------------------------------------------------------------------- 1 | this.socket.on('connect', this.onSocketConnected()); 2 | this.socket.on('connect_error', this.onSocketConnectionError); 3 | this.socket.on('connect_timeout', this.onSocketTimeout); 4 | this.socket.on('disconnect', this.onSocketDisconnected()); 5 | this.socket.on('reconnect', this.onSocketReconnected()); 6 | this.socket.on('reconnecting', this.onSocketReconnecting); 7 | // this.socket.on('reconnect_error', socketConnectionError); 8 | this.socket.on('reconnect_failed', this.onSocketReconnectionFailed); 9 | this.socket.on('reconnect_timeout', this.onSocketTimeout); 10 | this.socket.on('ping', this.onPing); 11 | this.socket.on('pong', this.onPong); 12 | 13 | // Mnml specific socket messages 14 | this.socket.on('operator:message', this.handleOperatorMessage()); 15 | this.socket.on('chat:new', this.handleChatNew()); 16 | this.socket.on('chat:existing', this.handleChatExisting()); -------------------------------------------------------------------------------- /example/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/example/favicon-16x16.png -------------------------------------------------------------------------------- /example/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/example/favicon-32x32.png -------------------------------------------------------------------------------- /example/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/example/favicon-96x96.png -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/example/favicon.ico -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 64 | 65 | 66 |
67 |

Star Wars — Classic Collection

68 |

A New Hope

69 |

Your friend is quite a mercenary. I wonder if he really cares about anything...or anyone. I care! So...what do you think of her, Han? I'm trying not to, kid! Good... Still, she's got a lot of spirit. I don't know, what do you think? Do you think a princess and a guy like me... No!

70 |

I can't abide these Jawas. Disgusting creatures. Go on, go on. I can't understand how we got by those troopers. I thought we were dead. The Force can have a strong influence on the weak-minded. You will find it a powerful ally. Do you really think we're going to find a pilot here that'll take us to Alderaan? Well, most of the best freighter pilots can be found here. Only watch your step. This place can be a little rough. I'm ready for anything. Come along, Artoo.

71 |

They've coming in! Three marks at two ten. I'll take them myself! Cover me! Yes, sir. I can't maneuver! Stay on target. We're too close. Stay on target! Loosen up! Gold Five to Red Leader... Lost Tiree, lost Dutch. I copy, Gold Five. They came from behind.... We've analyzed their attack, sir, and there is a danger. Should I have your ship standing by? Evacuate? In out moment of triumph? I think you overestimate their chances! Rebel base, three minutes and closing. Red Group, this is Red Leader.

72 |

What? Yahoo! Look out! You're all clear, kid. Now let's blow this thing and go home! Stand by to fire at Rebel base. Standing by. Great shot, kid. That was one in a million. Remember, the Force will be with you...always.

73 |

Help me, Obi-Wan Kenobi. You're my only hope. What's this? What is what?!? He asked you a question... What is that? Help me, Obi-Wan Kenobi. You're my only hope. Help me, Obi-Wan Kenobi. You're my only hope. Oh, he says it's nothing, sir. Merely a malfunction. Old data. Pay it no mind. Who is she? She's beautiful. I'm afraid I'm not quite sure, sir. Help me, Obi-Wan Kenobi... I think she was a passenger on our last voyage. A person of some importance, sir -- I believe. Our captain was attached to... Is there more to this recording? Behave yourself, Artoo. You're going to get us in trouble. It's all right, you can trust him. He's our new master.

74 |

What is it? I'm afraid I'm not quite sure, sir. He says I found her, and keeps repeating, She's here. Well, who...who has he found? Princess Leia. The princess? She's here? Princess? What's going on? Level five. Detention block A A-twenty-three. I'm afraid she's scheduled to be terminated. Oh, no! We've got to do something. What are you talking about? The droid belongs to her. She's the one in the message.. We've got to help her. Now, look, don't get any funny ideas. The old man wants us to wait right here. But he didn't know she was here. Look, will you just find a way back into the detention block?

75 |

Let him go! Stay on the leader! Hurry, Luke, they're coming in much faster this time. I can't hold them! Artoo, try and increase the power! Hurry up, Luke! Wait! I'm on the leader. Hang on, Artoo! Use the Force, Luke. Let go, Luke. The Force is strong with this one! Luke, trust me. His computer's off. Luke, you switched off your targeting computer. What's wrong? Nothing. I'm all right. I've lost Artoo! You may fire when ready. I have you now.

76 |

The battle station is heavily shielded and carries a firepower greater than half the star fleet. It's defenses are designed around a direct large-scale assault. A small one-man fighter should be able to penetrate the outer defense. Pardon me for asking, sir, but what good are snub fighters going to be against that? Well, the Empire doesn't consider a small one-man fighter to be any threat, or they'd have a tighter defense. An analysis of the plans provided by Princess Leia has demonstrated a weakness in the battle station.

77 |

To your stations! Come with me. Close all outboard shields! Close all outboard shields! Yes. We've captured a freighter entering the remains of the Alderaan system. It's markings match those of a ship that blasted its way out of Mos Eisley. They must be trying to return the stolen plans to the princess. She may yet be of some use to us. Unlock one-five-seven and nine. Release charges. There's no one on board, sir. According to the log, the crew abandoned ship right after takeoff. It must be a decoy, sir. Several of the escape pods have been jettisoned.

78 |

Chewie! Get behind me! Get behind me! Can't get out that way. Looks like you managed to cut off our only escape route. Maybe you'd like it back in your cell, Your Highness. See-Threepio! See-Threepio! Yes sir? We've been cut off! Are there any other ways out of the cell bay?...What was that? I didn't copy! I said, all systems have been alerted to your presence, sir. The main entrance seems to be the only way in or out, all other information on your level is restricted.

79 |

The Empire Strikes Back

80 |

Well done. Hold them in the security tower - and keep it quiet. Move. What do you think you're doing? We're getting out of here. I knew all along it had to be a mistake. Do you think that after what you did to Han we're going to trust you? I had no choice... What are you doing? Trust him, trust him! Oh, so we understand, don't we, Chewie? He had no choice. I'm just trying to help... We don't need any of your help. H-a-a-a... What? It sounds like Han. There's still a chance to save Han...I mean, at the East Platform... Chewie. I'm terribly sorry about all this. After all, he's only a Wookiee.

81 |

You said you wanted to be around when I made a mistake. well, this could be it, sweetheart. I take it back. We're going to get pulverized if we stay out here much longer. I'm not going to argue with that. Pulverized? I'm going in closer to one of the big ones. Closer? Closer?! h, this is suicide! There. That looks pretty good. What looks pretty good? Yeah. That'll do nicely. Excuse me, ma'am, but where are we going? I hope you know what you're doing. Yeah, me too.

82 |

I can't abide these Jawas. Disgusting creatures.

83 |

This ground sure feels strange. It doesn't feel like rock at all. There's an awful lot of moisture in here. I don't know. I have a bad feeling about this. Yeah. Watch out! Yeah, that's what I thought. Mynock. Chewie, check the rest of the ship, make sure there aren't any more attached. They're chewing on the power cables. Mynocks? Go on inside. We'll clean them off if there are any more. Ohhh! Go away! Go away! Beastly thing. Shoo! Shoo! Wait a minute... All right, Chewie, let's get out of here!

84 |

What do you want? Well, it's Princess Leia, sir. She's been trying to get you on the communicator. I turned it off. I don't want to talk to her. Oh. Well, Princess Leia is wondering about Master Luke. He hasn't come back yet. She doesn't know where he is. I don't know where he is. Nobody knows where he is. What do you mean, nobody knows? Well, uh, you see... Deck Officer. Deck Officer!

85 |

Lord Vader, the fleet has moved out of light-speed, and we're preparing to...Aaagh! You have failed me for the last time, Admiral. Captain Piett. Yes, my lord. Make ready to land out troops beyond the energy shield and deploy the fleet so that nothing gets off that system. You are in command now, Admiral Piett. Thank you, Lord Vader.

86 |

Return of the Jedi

87 |

You can see here the Death Star orbiting the forest Moon of Endor. Although the weapon systems on this Death Star are not yet operational, the Death Star does have a strong defense mechanism. It is protected by an energy shield, which is generated from the nearby forest Moon of Endor. The shield must be deactivated if any attack is to be attempted. Once the shield is down, our cruisers will create a perimeter, while the fighters fly into the superstructure and attempt to knock out the main reactor. General Calrissian has volunteered to lead the fighter attack

88 |

Look. I want you to take her. I mean it. Take her. You need all the help you can get. She's the fastest ship in the fleet. All right, old buddy. You know, I know what she means to you. I'll take good care of her. She-she won't get a scratch. All right? Right. I got your promise now. Not a scratch. Look, would you get going, you pirate. Good luck. You, too.

89 |

Luke...Luke...Do not...Do not underestimate the powers of the Emperor, or suffer your father's fate, you will. Luke, when gone am I, the last of the Jedi will you be. Luke, the Force runs strong in your family. Pass on what you have learned, Luke... There is...another...Sky...Sky...walker.

90 |

Of course I'm worried. And you should be, too. Lando Calrissian and poor Chewbacca never returned from this awful place. Artoo whistles timidly. Don't be so sure. If I told you half the things I've heard about this Jabba the Hutt, you'd probably short-circuit. Artoo, are you sure this is the right place? I better knock, I suppose. There doesn't seem to be anyone there. Let's go back and tell Master Luke.

91 |

I told you to remain on the command ship. A small Rebel force has penetrated the shield and landed on Endor. Yes, I know. My son is with them. Are you sure? I have felt him, my Master. Strange, that I have not. I wonder if your feelings on this matter are clear, Lord Vader. They are clear, my Master. Then you must go to the Sanctuary Moon and wait for them. He will come to me? I have foreseen it. His compassion for you will be his undoing. He will come to you andthen you will bring him before me. As you wish.

92 |
93 | 94 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /example/stockphoto-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/example/stockphoto-01.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mnml-client", 3 | "version": "0.2.0", 4 | "description": "Client library for Minimal Chat", 5 | "author": "Tyler Sloane ", 6 | "repository": "minimalchat/client.git", 7 | "license": "BSD-3-Clause", 8 | "scripts": { 9 | "test": "jest", 10 | "test:watch": "npm run test -- --watchAll", 11 | "build": "npm run build:dev", 12 | "build:dev": "cross-env NODE_ENV=development webpack --progress --colors", 13 | "build:production": "cross-env NODE_ENV=production webpack -p --progress --colors --display-modules", 14 | "build:watch": "npm run build:dev -- --watch", 15 | "format": "prettier-eslint --trailing-comma es5 --single-quote --print-width 100 --write \"src/**/*.js\"", 16 | "lint": "eslint src test", 17 | "coverage": "npm run test -- --coverage && cat coverage/lcov.info | coveralls", 18 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress", 19 | "start": "npm run dev" 20 | }, 21 | "jest": { 22 | "setupFiles": [ 23 | "./test/setup.js" 24 | ], 25 | "moduleNameMapper": { 26 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 27 | "\\.(css|less)$": "identity-obj-proxy", 28 | "^react$": "preact-compat", 29 | "^react-dom$": "preact-compat" 30 | }, 31 | "moduleFileExtensions": [ 32 | "js", 33 | "jsx" 34 | ], 35 | "moduleDirectories": [ 36 | "node_modules" 37 | ], 38 | "testRegex": "((test|spec))\\.(js|jsx)$", 39 | "testURL": "http://localhost:8080", 40 | "coverageReporters": [ 41 | "lcov", 42 | "text" 43 | ], 44 | "coverageDirectory": "coverage", 45 | "collectCoverageFrom": [ 46 | "src/**/*.{js,jsx}", 47 | "!src/components/Skeleton/*.{js,jsx}" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.4.4", 52 | "@babel/plugin-proposal-class-properties": "^7.4.4", 53 | "@babel/plugin-transform-react-jsx": "^7.3.0", 54 | "@babel/preset-env": "^7.4.4", 55 | "babel-eslint": "^7.2.2", 56 | "babel-loader": "^8.0.5", 57 | "chai": "^4.0.2", 58 | "coveralls": "^3.0.3", 59 | "cross-env": "^4.0.0", 60 | "css-loader": "^2.1.1", 61 | "eslint": "^4.8.0", 62 | "eslint-config-airbnb": "^16.0.0", 63 | "eslint-config-prettier": "^2.6.0", 64 | "eslint-plugin-import": "^2.7.0", 65 | "eslint-plugin-jest": "^21.2.0", 66 | "eslint-plugin-jsx-a11y": "^6.0.2", 67 | "eslint-plugin-react": "^7.4.0", 68 | "identity-obj-proxy": "^3.0.0", 69 | "jest": "^24.1.0", 70 | "jest-localstorage-mock": "^2.4.0", 71 | "preact-jsx-chai": "^2.2.1", 72 | "preact-render-to-string": "^3.7.0", 73 | "prettier-eslint": "^8.8.2", 74 | "prettier-eslint-cli": "^4.7.1", 75 | "replace-bundle-webpack-plugin": "^1.0.0", 76 | "style-loader": "^0.18.2", 77 | "webpack": "^4.30.0", 78 | "webpack-cli": "^3.3.2", 79 | "webpack-dev-server": "^3.3.1" 80 | }, 81 | "dependencies": { 82 | "preact": "^8.1.0", 83 | "preact-compat": "^3.15.0", 84 | "prop-types": "^15.5.10", 85 | "socket.io-client": "^2.0.3" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/App/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = `"
Chat with John
"`; 4 | -------------------------------------------------------------------------------- /src/components/App/functions.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | 3 | const remoteHost = process.env.REMOTE_HOST || 'localhost:8000'; 4 | const socketPath = `http${process.env.NODE_ENV === 'development' ? '' : 's'}://${remoteHost}`; 5 | 6 | const sessionStorageKey = 'minimalchat-session'; 7 | 8 | // Socket Functions 9 | // 10 | 11 | export function createSocket (app) { 12 | const { localStorage } = window; 13 | 14 | let storedSessionId = localStorage.getItem(sessionStorageKey); 15 | 16 | const socket = io.connect( 17 | socketPath, 18 | { 19 | reconnectionAttempts: 3, 20 | query: `type=client&sessionId=${storedSessionId}`, 21 | } 22 | ); 23 | 24 | socket.on('operator:message', app.receiveMessage.bind(app)); 25 | socket.on('operator:typing', app.operatorTyping.bind(app)); 26 | socket.on('chat:existing', data => { 27 | const session = JSON.parse(data); 28 | 29 | // TODO: Check if local storage ID different from chat ID 30 | 31 | return app.handleExistingConnection(session); 32 | }); 33 | socket.on('chat:new', data => { 34 | const session = JSON.parse(data); 35 | 36 | // Set the session in local storage incase they refresh 37 | localStorage.setItem(sessionStorageKey, session.id); 38 | storedSessionId = session.id; 39 | 40 | return app.handleNewConnection(session); 41 | }); 42 | socket.on('reconnect', app.handleReconnected.bind(app)); 43 | socket.on('disconnect', app.handleDisconnected.bind(app)); 44 | socket.on('reconnecting', app.handleReconnecting.bind(app)); 45 | 46 | return socket; 47 | } 48 | 49 | // Fetch past messages from the API 50 | export function fetchMessages (app) { 51 | // TODO: Query past messages 52 | const { session } = app.state; 53 | 54 | // TODO: Decide whether we should be hitting http or https somehow, somewhere 55 | return fetch( 56 | `http${process.env.NODE_ENV === 'development' ? '' : 's'}://${remoteHost}/api/chat/${ 57 | session.id 58 | }/messages` 59 | ) 60 | .then(res => res.json()) 61 | .then(data => app.loadMessages(data.messages || [])) 62 | .catch(err => { 63 | console.error('Failed to load past messages', err); 64 | }); 65 | } 66 | 67 | // Message Functions 68 | // 69 | 70 | export function canCombineLastMessage (msg, messages) { 71 | const lastMsg = messages[messages.length - 1]; 72 | 73 | // If this is the first message in the conversation 74 | if (lastMsg === undefined) return false; 75 | 76 | // If theres no author field, return false 77 | if (!msg.hasOwnProperty('author') || !lastMsg.hasOwnProperty('author')) return false; 78 | 79 | // If last message was not from the same author 80 | if (msg.author !== lastMsg.author) return false; 81 | 82 | return true; 83 | } 84 | 85 | export function combineLastMessage (msg, messages) { 86 | const lastMsg = messages[messages.length - 1]; 87 | const newMsg = msg; 88 | 89 | newMsg.content = newMsg.content || []; 90 | 91 | // Is the content of the new message an array? 92 | if (!(newMsg.content.push instanceof Function)) { 93 | newMsg.content = [newMsg.content]; 94 | } 95 | 96 | // Can we combine the last message? 97 | if (canCombineLastMessage(msg, messages)) { 98 | const combinedMessages = [...messages]; 99 | 100 | // If there is no content in the last message make sure we can add some 101 | lastMsg.content = lastMsg.content || []; 102 | 103 | // Is the content of the last message an array? 104 | if (!(lastMsg.content.push instanceof Function)) { 105 | lastMsg.content = [lastMsg.content]; 106 | } 107 | 108 | // Push contents to last message 109 | lastMsg.content.push(...newMsg.content); 110 | 111 | // Splice in new last message 112 | // TODO: This may not be necessary if lastMsg is a reference to messages? 113 | combinedMessages.splice(combinedMessages.length - 1, 1, lastMsg); 114 | 115 | return combinedMessages; 116 | } 117 | 118 | return [...messages, newMsg]; 119 | } 120 | 121 | export function formatMessage (content, clientID, sessionID) { 122 | return { 123 | timestamp: new Date().toISOString(), 124 | author: `client.${clientID}`, 125 | content, 126 | chat: sessionID, 127 | }; 128 | } 129 | 130 | export function formatMessageForClient (msg, clientID, sessionID) { 131 | return formatMessage([msg], clientID, sessionID); 132 | } 133 | 134 | export function formatMessageForServer (msg, clientID, sessionID) { 135 | return formatMessage(msg, clientID, sessionID); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/App/functions.test.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | 3 | import { 4 | createSocket, 5 | canCombineLastMessage, 6 | combineLastMessage, 7 | } from './functions.js'; 8 | 9 | describe('createSocket', () => { 10 | let app; 11 | let socket; 12 | 13 | beforeEach(() => { 14 | global.localStorage = { 15 | setItem: jest.fn(), 16 | getItem: jest.fn(), 17 | }; 18 | 19 | app = { 20 | receiveMessage: { bind: jest.fn() }, 21 | operatorTyping: { bind: jest.fn() }, 22 | handleNewConnection: { bind: jest.fn() }, 23 | handleDisconnected: { bind: jest.fn() }, 24 | handleReconnecting: { bind: jest.fn() }, 25 | handleReconnected: { bind: jest.fn() }, 26 | }; 27 | 28 | socket = { 29 | on: jest.fn() 30 | }; 31 | 32 | // Mock the socket library 33 | io.connect = jest.fn(() => socket); 34 | }); 35 | 36 | it('initiates a socket connection', () => { 37 | createSocket(app); 38 | 39 | expect(io.connect).toHaveBeenCalled(); 40 | }); 41 | 42 | it('listens for events', () => { 43 | createSocket(app); 44 | 45 | expect(socket.on).toHaveBeenCalledWith('operator:message', undefined); 46 | expect(socket.on).toHaveBeenCalledWith('operator:typing', undefined); 47 | expect(socket.on).toHaveBeenCalledWith('chat:new', expect.any(Function)); 48 | expect(socket.on).toHaveBeenCalledWith('disconnect', undefined); 49 | expect(socket.on).toHaveBeenCalledWith('reconnecting', undefined); 50 | }); 51 | 52 | it('gets stored sessionId', () => { 53 | createSocket(app); 54 | 55 | const sessionStorageKey = 'minimalchat-session'; 56 | 57 | expect(global.localStorage.getItem).toHaveBeenCalledWith(sessionStorageKey); 58 | }); 59 | }); 60 | 61 | describe('canCombineLastMessage', () => { 62 | it('returns false if there are no messages', () => { 63 | const messages = []; 64 | const msg = {}; 65 | 66 | expect(canCombineLastMessage(msg, messages)).toBe(false); 67 | }); 68 | 69 | it('returns false if either message or last message has no author property', () => { 70 | const messages = [{}]; 71 | const msg = {}; 72 | 73 | expect(canCombineLastMessage(msg, messages)).toBe(false); 74 | }); 75 | 76 | it('returns false if message author and last message author are not equal', () => { 77 | const messages = [{author: 'TEST2'}]; 78 | const msg = {author: 'TEST1'}; 79 | 80 | expect(canCombineLastMessage(msg, messages)).toBe(false); 81 | }); 82 | 83 | it('returns true if message author and last message author are equal', () => { 84 | const messages = [{author: 'TEST'}]; 85 | const msg = {author: 'TEST'}; 86 | 87 | expect(canCombineLastMessage(msg, messages)).toBe(true); 88 | }); 89 | }); 90 | 91 | describe('combineLastMessage', () => { 92 | it('returns an array', () => { 93 | const messages = [{}]; 94 | const msg = {}; 95 | 96 | expect(combineLastMessage(msg, messages).push instanceof Function).toBe(true); 97 | }); 98 | 99 | it('converts malformed messages to proper format', () => { 100 | const messages = [{ 101 | author: 'TEST', 102 | content: 'TEST', 103 | }]; 104 | const msg = { 105 | author: 'TEST', 106 | content: 'TEST', 107 | }; 108 | 109 | expect(combineLastMessage(msg, messages)[0].content.push instanceof Function).toBe(true); 110 | }); 111 | 112 | it('combines messages when appropriate', () => { 113 | const messages = [{author: 'TEST'}]; 114 | const msg = {author: 'TEST'}; 115 | 116 | expect(combineLastMessage(msg, messages).length).toBe(1); 117 | }); 118 | 119 | it('does not combine messages when inappropriate', () => { 120 | const messages = [{author: 'TEST1'}]; 121 | const msg = {author: 'TEST2'}; 122 | 123 | expect(combineLastMessage(msg, messages).length).toBe(2); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Chat from '../Chat'; 5 | import ClosedState from '../ClosedState'; 6 | import { ThemeProvider } from '../ThemeProvider'; 7 | import { 8 | formatMessageForClient, 9 | formatMessageForServer, 10 | combineLastMessage, 11 | createSocket, 12 | fetchMessages, 13 | } from './functions'; 14 | 15 | import './styles.css'; 16 | 17 | const MESSENGER = 'messenger'; 18 | const FLOAT = 'float'; 19 | const SIDEPANEL = 'side'; 20 | 21 | const TYPING_TIMEOUT = 3000; 22 | 23 | class App extends Component { 24 | propTypes = { 25 | theme: PropTypes.objectOf(PropTypes.string), 26 | }; 27 | 28 | state = { 29 | chatOpen: false, 30 | messages: [], 31 | textBox: '', 32 | network: '', 33 | style: MESSENGER, // wrapped with theme provider + HOC 34 | }; 35 | 36 | componentDidMount () { 37 | this.socket = createSocket(this); 38 | this.typing = null; 39 | } 40 | 41 | componentWillUnmount () { 42 | window.clearTimeout(this.typing); 43 | this.typing = null; 44 | } 45 | 46 | // --- UI / Event Handlers 47 | 48 | toggleChat = bool => { 49 | this.setState({ chatOpen: bool }); 50 | }; 51 | 52 | handleInput = e => { 53 | this.setState({ textBox: e.target.value }); 54 | }; 55 | 56 | // --- Socket Methods 57 | 58 | handleKeyDown = () => { 59 | const payload = JSON.stringify( 60 | formatMessageForServer(null, this.state.session.client.id, this.state.session.id) 61 | ); 62 | 63 | this.socket.emit('client:typing', payload); 64 | }; 65 | 66 | /** 67 | * @description On connecting to the socket server save a session object into the state 68 | */ 69 | handleNewConnection = session => { 70 | this.setState({ 71 | session, 72 | }); 73 | }; 74 | 75 | handleExistingConnection = session => { 76 | this.setState({ 77 | session, 78 | }); 79 | 80 | fetchMessages(this); 81 | }; 82 | 83 | handleDisconnected = () => { 84 | this.setState({ 85 | network: 'disconnected', 86 | }); 87 | }; 88 | 89 | handleReconnecting = attempts => { 90 | // eslint-disable-next-line 91 | const attemptLimit = this.socket.io._reconnectionAttempts; 92 | if (attempts < attemptLimit) { 93 | this.setState({ 94 | network: 'reconnecting', 95 | }); 96 | } else { 97 | this.handleDisconnected(); 98 | } 99 | }; 100 | 101 | /** 102 | * HandleReconnected changes our network state based on the socket state. 103 | * It uses a set timeout to move from a state of "reconnected" back to the standard state. 104 | * This is primarily for the notification component for displaying network issues. 105 | */ 106 | handleReconnected = () => { 107 | // prettier-ignore 108 | this.setState({ 109 | network: 'reconnected', 110 | }, setTimeout(() => { 111 | this.setState({network: ""}) 112 | }, 2000)) 113 | }; 114 | 115 | // --- Message Methods 116 | 117 | loadMessages = msgs => { 118 | let messages = []; 119 | 120 | // Since these are all 'new' we have to run though each and combine accordingly 121 | for (let i = 0; i < msgs.length; i += 1) { 122 | messages = combineLastMessage( 123 | Object.assign( 124 | {}, 125 | { 126 | ...msgs[i], 127 | content: [msgs[i].content], 128 | } 129 | ), 130 | messages 131 | ); 132 | } 133 | 134 | this.setState({ 135 | messages, 136 | }); 137 | }; 138 | 139 | // Save the message to local state 140 | saveMessageToState = msg => { 141 | const formattedMsg = formatMessageForClient( 142 | msg, 143 | this.state.session.client.id, 144 | this.state.session.id 145 | ); 146 | 147 | this.setState({ 148 | messages: combineLastMessage(formattedMsg, this.state.messages), 149 | textBox: '', 150 | }); 151 | }; 152 | 153 | saveMessageToServer = msg => { 154 | const formattedMsg = formatMessageForServer( 155 | msg, 156 | this.state.session.client.id, 157 | this.state.session.id 158 | ); 159 | 160 | this.socket.emit('client:message', JSON.stringify(formattedMsg)); 161 | }; 162 | 163 | /** Send Message 164 | * @summary - Allow a user to send a message. 165 | * @description - Passes the message through some functions to save it locally and remotely 166 | * @param {object} e - event object; used to prevent refreshing the page 167 | */ 168 | sendMessage = e => { 169 | e.preventDefault(); // Must prevent default behavior first 170 | if (this.state.textBox === '') return; 171 | const msg = this.state.textBox; 172 | this.saveMessageToState(msg); 173 | this.saveMessageToServer(msg); 174 | }; 175 | 176 | /** Recieve Message 177 | * @summary - Takes data sent to user from socket/operator and consumes it 178 | * @description - Takes data from websocket clears typing timeout and combines the data 179 | * with the last message if necessary 180 | * @param {string} data - JSON string of data being sent back 181 | */ 182 | receiveMessage = data => { 183 | const msg = JSON.parse(data); // Data comes in as a string 184 | 185 | // Clear the typing timeout if we receive a message 186 | window.clearTimeout(this.typing); 187 | this.typing = null; 188 | 189 | this.setState({ 190 | typing: false, 191 | messages: combineLastMessage(msg, this.state.messages), 192 | }); 193 | }; 194 | 195 | /** Operator Typing 196 | * @summary - Handles debouncing operator typing chat bubble 197 | * @description - Sets a timeout to remove chat bubble after receiving indication from 198 | * the daemon that the operator is typing, resetting the timeout each time and updating 199 | * the state to reflect the typing 200 | */ 201 | operatorTyping = () => { 202 | window.clearTimeout(this.typing); 203 | 204 | this.typing = window.setTimeout(() => { 205 | // Clear the typing variable 206 | this.setState({ 207 | typing: false, 208 | }); 209 | }, TYPING_TIMEOUT); 210 | 211 | this.setState({ 212 | typing: true, 213 | }); 214 | }; 215 | 216 | // --- Render + Render methods 217 | 218 | renderClosedChat = () => ( 219 | 220 | ); 221 | 222 | renderOpenChat = () => ( 223 | 234 | ); 235 | 236 | renderChat = () => (this.state.chatOpen ? this.renderOpenChat() : this.renderClosedChat()); 237 | 238 | render () { 239 | const { theme } = this.props; 240 | const { style, chatOpen } = this.state; 241 | const visibility = chatOpen ? 'open' : 'closed'; 242 | 243 | return ( 244 | 245 |
{this.renderChat()}
246 |
247 | ); 248 | } 249 | } 250 | 251 | export default App; 252 | -------------------------------------------------------------------------------- /src/components/App/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | import io from 'socket.io-client'; 4 | 5 | import App from '.'; 6 | 7 | describe('', () => { 8 | it('matches snapshot', () => { 9 | const theme = { 10 | primary_colour: 'test', 11 | }; 12 | const tree = render(); 13 | expect(tree).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/App/storage.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minimalchat/client/111dcca5d63d68bdd460435923adb67c51628f33/src/components/App/storage.js -------------------------------------------------------------------------------- /src/components/App/styles.css: -------------------------------------------------------------------------------- 1 | /* Application Component */ 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700'); 4 | 5 | #mnml-chat { 6 | font-size: 12px; 7 | font-family: 'Open Sans', sans-serif; 8 | } 9 | 10 | 11 | /* Messenger Theme */ 12 | 13 | .mnml--messenger { 14 | position: fixed; 15 | display: flex; 16 | flex-direction: column; 17 | bottom: 0; 18 | right: 100px; 19 | width: 280px; 20 | height: 360px; 21 | box-shadow: rgba(0, 0, 0, 0.12) 0px 0px 2px 0px, rgba(0, 0, 0, 0.24) 0px 2px 4px 0px; 22 | } 23 | 24 | .mnml--messenger.closed { 25 | position: fixed; 26 | display: flex; 27 | flex-direction: column; 28 | bottom: 0; 29 | right: 100px; 30 | width: 280px; 31 | height: 32px; 32 | } 33 | 34 | /* Float Theme */ 35 | 36 | .mnml--float { 37 | position: fixed; 38 | bottom: 0; 39 | right: 0; 40 | } 41 | 42 | .mnml--float.closed { 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Chat/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = `"
Chat with John
    \\"Operator\\"
"`; 4 | -------------------------------------------------------------------------------- /src/components/Chat/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Header from '../Header'; 5 | import Message from '../Message'; 6 | import Input from '../Input'; 7 | import { applyTheme } from '../ThemeProvider'; 8 | 9 | import './styles.css'; 10 | import Notification from '../Notification/index'; 11 | 12 | /** 13 | * Main chat component handles displaying chat messages and passes 14 | * functions to the input to send messages 15 | * Has scroll to bottom functionality for keeping window scoll at bottom of the chat. 16 | * Needs to be a class based component in order to have that functionality ^ 17 | */ 18 | class Chat extends Component { 19 | propTypes = { 20 | toggleChat: PropTypes.func, 21 | typing: PropTypes.boolean, 22 | handleInput: PropTypes.func, 23 | handleKeyDown: PropTypes.func, 24 | sendMessage: PropTypes.func, 25 | chatOpen: PropTypes.boolean, 26 | network: PropTypes.string, 27 | style: PropTypes.string, 28 | textBox: PropTypes.string, 29 | messages: PropTypes.arrayOf({ 30 | timestamp: PropTypes.string, 31 | author: PropTypes.string, 32 | content: PropTypes.arrayOf(PropTypes.string), 33 | chat: PropTypes.string, 34 | }), 35 | }; 36 | 37 | componentDidMount () { 38 | this.scrollToBottom(); 39 | } 40 | 41 | componentDidUpdate () { 42 | this.scrollToBottom(); 43 | } 44 | 45 | scrollToBottom = () => { 46 | this.container.scrollTop = this.container.scrollHeight; 47 | }; 48 | 49 | // msg.content is an array! 50 | renderMessages = () => 51 | this.props.messages.map(msg => ); 52 | 53 | renderTyping = () => ( 54 |
55 |
    56 |
  • 57 | 58 | 59 | 72 | 73 | 85 | 86 | 98 | 99 |
  • 100 |
101 | Operator 106 |
107 | ); 108 | 109 | render () { 110 | const { 111 | toggleChat, 112 | textBox, 113 | typing, 114 | handleInput, 115 | handleKeyDown, 116 | sendMessage, 117 | style, 118 | chatOpen, 119 | } = this.props; 120 | 121 | return ( 122 |
123 |
toggleChat(false)} chatOpen={chatOpen} /> 124 | 125 | 126 | {/* Container for text input and reading messages */} 127 |
    { 130 | this.container = c; 131 | return c; 132 | }} 133 | > 134 | {this.renderMessages()} 135 |
  • 139 | {this.renderTyping()} 140 |
  • 141 |
142 | 143 | 149 |
150 | ); 151 | } 152 | } 153 | 154 | export default applyTheme(Chat); 155 | -------------------------------------------------------------------------------- /src/components/Chat/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import Chat from '.'; 5 | import { ThemeProvider } from '../ThemeProvider'; 6 | 7 | describe('', () => { 8 | it('matches snapshot', () => { 9 | const props = { 10 | messages: [], 11 | theme: { 12 | primary_colour: 'test', 13 | }, 14 | }; 15 | const tree = render( 16 | 17 | ); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Chat/styles.css: -------------------------------------------------------------------------------- 1 | /* Chat Component */ 2 | 3 | 4 | /* Messenger Theme */ 5 | 6 | .Chat--messenger { 7 | display: flex; 8 | flex-direction: column; 9 | background: rgba(248,248,248,1); 10 | flex-grow: 1; 11 | box-shadow: #00000013 1px 1px 8px 0; 12 | border: 0; 13 | border-radius: 3px 3px 0 0; 14 | } 15 | 16 | .Chat__body--messenger { 17 | list-style: none; 18 | display: flex; 19 | flex-grow: 1; 20 | padding: 6px 4px; 21 | flex-direction: column; 22 | overflow-y: scroll; 23 | margin: 0; 24 | border-right: 1px solid rgba(0,0,0,0.1); 25 | height: 100%; 26 | } 27 | 28 | 29 | /* Float Theme */ 30 | 31 | .Chat--float { 32 | position: relative; 33 | right: 10px; 34 | bottom: 30px; 35 | background-color: transparent; 36 | } 37 | 38 | .Chat__body--float { 39 | width: 310px; 40 | bottom: 110px; 41 | right: 0; 42 | margin: 0; 43 | padding: 0; 44 | overflow-y: auto; 45 | overflow-x: hidden; 46 | height: auto; 47 | position: absolute; 48 | display: flex; 49 | flex-direction: column; 50 | list-style: none; 51 | flex-grow: 1; 52 | justify-content: flex-end; 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/ClosedState/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = `"
Chat with John
"`; 4 | -------------------------------------------------------------------------------- /src/components/ClosedState/chaticon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/ClosedState/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Header from '../Header'; 5 | 6 | import './styles.css'; 7 | 8 | const ClosedState = props => ( 9 |
props.toggleChat(true)} /> 10 | ); 11 | 12 | ClosedState.defaultProps = { 13 | chatOpen: false, 14 | }; 15 | 16 | ClosedState.propTypes = { 17 | toggleChat: PropTypes.func.isRequired, 18 | chatOpen: PropTypes.bool, 19 | }; 20 | 21 | export default ClosedState; 22 | -------------------------------------------------------------------------------- /src/components/ClosedState/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import ClosedState from '.'; 5 | import { ThemeProvider } from '../ThemeProvider'; 6 | 7 | describe('', () => { 8 | it('matches snapshot', () => { 9 | const theme = { 10 | primary_colour: 'test', 11 | }; 12 | const tree = render( 13 | 14 | ); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/ClosedState/styles.css: -------------------------------------------------------------------------------- 1 | .ClosedState { 2 | width: 60px; 3 | height: 60px; 4 | position: fixed; 5 | bottom: 0; 6 | right: 0; 7 | border: 1px solid #efefef; 8 | margin: 40px; 9 | border-radius: 50%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | color: white; 14 | cursor: pointer; 15 | transition: all, 0.15s ease; 16 | background: white; 17 | animation-name: breathe; 18 | animation-duration: 10.4s; 19 | animation-direction: alternate-reverse; 20 | animation-iteration-count: infinite; 21 | &:hover { 22 | box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.09); 23 | } 24 | &:active { 25 | box-shadow: 2px 2px 1px rgba(0, 0, 0, 0); 26 | } 27 | } 28 | 29 | @keyframes breathe { 30 | 0% { 31 | box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.14); 32 | } 33 | 50% { 34 | box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.08); 35 | } 36 | 37 | 100% { 38 | box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.08); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Header/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`
should render 1`] = `"
Chat with John
"`; 4 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { applyTheme } from '../ThemeProvider/'; 5 | 6 | import './style.css'; 7 | 8 | /** 9 | * Header 10 | * 11 | */ 12 | class Header extends Component { 13 | propTypes = { 14 | toggleChat: PropTypes.func, 15 | style: PropTypes.string, 16 | theme: PropTypes.shape({ 17 | primary_colour: PropTypes.string.isRequired, 18 | }), 19 | chatOpen: PropTypes.boolean, 20 | }; 21 | 22 | renderToggleChatButton = () => (this.props.chatOpen ? × : ...); 23 | 24 | render () { 25 | const { toggleChat, style, theme } = this.props; 26 | 27 | return ( 28 |
toggleChat(true)} 34 | onKeyPress={event => { 35 | // Ensure event is not null 36 | const e = event || window.event; 37 | 38 | if ((e.which === 72 || e.keyCode === 72) && e.ctrlKey) { 39 | toggleChat(true); 40 | } 41 | }} 42 | > 43 | Chat with John 44 | 47 |
48 | ); 49 | } 50 | } 51 | 52 | export default applyTheme(Header); 53 | -------------------------------------------------------------------------------- /src/components/Header/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import Header from '.'; 5 | import { ThemeProvider } from '../ThemeProvider'; 6 | 7 | describe('
', () => { 8 | it('should render ', () => { 9 | const theme = { 10 | primary_colour: 'test', 11 | }; 12 | const tree = render( 13 |
14 | ); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Header/style.css: -------------------------------------------------------------------------------- 1 | /* Header Component */ 2 | 3 | 4 | /* Messenger Theme */ 5 | 6 | .Header--messenger { 7 | background: #72a0e5; 8 | box-sizing: border-box; 9 | color: #efefef; 10 | font-weight: 700; 11 | padding: 12px 4px 12px 12px; 12 | display: flex; 13 | justify-content: space-between; 14 | height: 32px; 15 | border-radius: 3px 3px 0 0; 16 | border-right: 1px solid rgba(0,0,0,0.1); 17 | cursor: pointer; 18 | } 19 | 20 | .Header__title--messenger { 21 | align-self: center; 22 | color: white; 23 | } 24 | 25 | .Header__closeBtn--messenger { 26 | font-weight: 800; 27 | background: none; 28 | border: 0; 29 | color: white; 30 | font-size: 18px; 31 | align-self: center; 32 | outline: none; 33 | cursor: pointer; 34 | width: 24px; 35 | height: 24px; 36 | border-radius: 50%; 37 | line-height: 1; 38 | padding-top: 0; 39 | } 40 | 41 | .Header__closeBtn--messanger:active { 42 | box-shadow: 1px 1px 4px rgba(0, 0, 0, 0); 43 | } 44 | 45 | 46 | /* Float Theme */ 47 | 48 | .Header--float { } 49 | 50 | .closed .Header--float { 51 | position: relative; 52 | right: 10px; 53 | bottom: 30px; 54 | } 55 | 56 | .Header__title--float { 57 | display: none; 58 | } 59 | 60 | .Header__closeBtn--float { 61 | position: absolute; 62 | bottom: 0; 63 | right: 0; 64 | display: block; 65 | width: 42px; 66 | height: 42px; 67 | padding: 4px 6px; 68 | box-sizing: border-box; 69 | box-shadow: #dddddd 1px 1px 8px 0; 70 | border-radius: 21px; 71 | font-size: 32px; 72 | font-family: sans-serif; 73 | color: #ffffff; 74 | cursor: pointer; 75 | outline: none; 76 | background-color: #72a0e5; 77 | border: 0; 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Input/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render 1`] = `"
"`; 4 | -------------------------------------------------------------------------------- /src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { applyTheme } from '../ThemeProvider'; 5 | 6 | import './styles.css'; 7 | 8 | class Input extends Component { 9 | componentDidMount () { 10 | this.focusInput(); 11 | } 12 | 13 | focusInput () { 14 | if (this.input != null) { 15 | this.input.focus(); 16 | } 17 | } 18 | 19 | render () { 20 | const { sendMessage, style, textBox, handleInput, handleKeyDown } = this.props; 21 | 22 | return ( 23 |
24 | { 26 | this.input = input; 27 | }} 28 | className={`Input--${style}`} 29 | placeholder="Type Here" 30 | onChange={e => handleInput(e)} 31 | onKeyDown={e => handleKeyDown(e)} 32 | name="messages" 33 | value={textBox} 34 | /> 35 |
36 | ); 37 | } 38 | } 39 | 40 | Input.propTypes = { 41 | handleInput: PropTypes.func.isRequired, 42 | handleKeyDown: PropTypes.func.isRequired, 43 | sendMessage: PropTypes.func.isRequired, 44 | 45 | style: PropTypes.string.isRequired, 46 | textBox: PropTypes.string.isRequired, 47 | }; 48 | 49 | export default applyTheme(Input); 50 | -------------------------------------------------------------------------------- /src/components/Input/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import Input from '.'; 5 | 6 | describe('', () => { 7 | it('should render ', () => { 8 | const tree = render(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/Input/styles.css: -------------------------------------------------------------------------------- 1 | /* Input Component */ 2 | 3 | 4 | /* Messenger Theme */ 5 | 6 | .Input__form--messenger { 7 | display: flex !important; 8 | min-height: 40px; 9 | background: white; 10 | border-right: 1px solid rgba(0,0,0,0.1); 11 | } 12 | 13 | .Input--messenger { 14 | padding: 12px; 15 | outline: 0; 16 | border: 0px; 17 | flex-grow: 1; 18 | } 19 | 20 | 21 | /* Float Theme */ 22 | 23 | .Input--float { 24 | position: absolute; 25 | bottom: 50px; 26 | right: 50px; 27 | width: 260px; 28 | height: 48px; 29 | box-sizing: border-box; 30 | box-shadow: #dddddd 1px 1px 8px 0; 31 | border: 0; 32 | background-color: #ffffff; 33 | border-radius: 10px; 34 | padding: 16px; 35 | color: #222222; 36 | font-size: 13px; 37 | resize: none; 38 | outline: 0; 39 | } 40 | 41 | .Input--float:disabled { 42 | background-color: #e1e1e1; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Message/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = `"
    • "`; 4 | -------------------------------------------------------------------------------- /src/components/Message/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { applyTheme } from '../ThemeProvider'; 5 | 6 | import './styles.css'; 7 | 8 | const Message = props => { 9 | // our message's content 10 | const content = props.content.map((msg, i) =>
    • {msg}
    • ); 11 | 12 | // our default message is a client message 13 | let message = ( 14 |
      15 |
        {content}
      16 | 17 | Operator 22 |
      23 | ); 24 | 25 | // Ff the iterated message is an operator; override `message` 26 | if (props.type.indexOf('client') >= 0) { 27 | message =
        {content}
      ; 28 | } 29 | 30 | // Incoming props, mesage.content is an array. 31 | return
    • {message}
    • ; 32 | }; 33 | 34 | Message.propTypes = { 35 | style: PropTypes.string.isRequired, 36 | type: PropTypes.string.isRequired, 37 | content: PropTypes.arrayOf(PropTypes.string).isRequired, 38 | }; 39 | 40 | export default applyTheme(Message); 41 | -------------------------------------------------------------------------------- /src/components/Message/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import Message from '.'; 5 | 6 | describe('', () => { 7 | it('matches snapshot', () => { 8 | const props = { 9 | content: [], // message content 10 | type: 'client-TEST', 11 | }; 12 | 13 | const tree = render(); 14 | expect(tree).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/Message/styles.css: -------------------------------------------------------------------------------- 1 | /* Message Component */ 2 | 3 | 4 | /* Messenger Theme */ 5 | 6 | .Message__client--messenger { 7 | align-self: flex-end; 8 | background: rgb(10, 107, 239); 9 | border-radius: 8px 8px 8px 0; 10 | color: white; 11 | display: inline-block; 12 | font-size: 12px; 13 | margin: 0; 14 | max-width: 95%; 15 | min-width: 160px; 16 | padding: 5px 5px 5px 10px; 17 | list-style-type: none; 18 | hyphens: auto; 19 | } 20 | 21 | 22 | /* Container box for Avatar-op + Operator__Messenger */ 23 | .Message__operatorWrapper--messenger { 24 | align-items: flex-start; 25 | display: flex; 26 | justify-content: flex-end; 27 | margin: 0.5rem 0; 28 | } 29 | 30 | .Message__operator--messenger { 31 | align-self: flex-start; 32 | background: #e1e1e1; 33 | border-radius: 8px 0px 8px 8px; 34 | color: #333; 35 | display: inline-block; 36 | font-size: 12px; 37 | list-style: none; 38 | max-width: 70%; 39 | min-width: 120px; 40 | padding: 0.5rem; 41 | text-align: left; 42 | } 43 | 44 | .Message__box-typing--messenger 45 | .Message__operator--messenger { 46 | min-width: auto; 47 | padding: 2px 8px; 48 | } 49 | 50 | .Message__avatar--messenger { 51 | width: 40px; 52 | height: 40px; 53 | padding-left: 4px; 54 | } 55 | 56 | 57 | /* Float Theme */ 58 | 59 | /* Top level container around an
    • message */ 60 | .Message__box--float{ 61 | margin: 7px 0; 62 | } 63 | 64 | .Message__avatar--float { 65 | width: 40px; 66 | height: 40px; 67 | border-radius: 20px; 68 | margin-top: -24px; 69 | } 70 | 71 | .Message__operatorWrapper--float { 72 | align-items: flex-start; 73 | display: flex; 74 | justify-content: flex-end; 75 | } 76 | 77 | .Message__operator--float { 78 | margin: 0; 79 | list-style: none; 80 | width: 160px; 81 | float: left; 82 | text-align: left; 83 | background: #ffffff; 84 | color: #000000; 85 | margin-right: 12px; 86 | box-shadow: #dddddd 1px 1px 8px 0; 87 | border-radius: 10px 0px 10px 0px; 88 | padding: 16px; 89 | } 90 | 91 | .Message__client--float { 92 | margin: 0; 93 | padding: 16px; 94 | list-style: none; 95 | width: 160px; 96 | margin-top: 4px; 97 | margin-bottom: 4px; 98 | box-shadow: #dddddd 1px 1px 8px 0; 99 | border-radius: 10px 10px 10px 0px; 100 | background: #0a6bef; 101 | float: left; 102 | text-align: left; 103 | color: #ffffff; 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Notification/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`
      should render 1`] = `"
      "`; 4 | -------------------------------------------------------------------------------- /src/components/Notification/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { applyTheme } from '../ThemeProvider/'; 5 | 6 | import './style.css'; 7 | 8 | /** 9 | * Notification 10 | * NOTE: this notificatin is tailored towards displaying network notifications; it has 11 | * not been built with other types of notifications in mind (ex: '6 new messages!') 12 | * Possible future feature refactor 13 | */ 14 | class Notification extends Component { 15 | propTypes = { 16 | style: PropTypes.string, 17 | network: PropTypes.string, 18 | }; 19 | 20 | render () { 21 | const { style, network } = this.props; 22 | if (network === '' || network === 'connected') return null; 23 | 24 | const renderNotification = () => { 25 | switch (network) { 26 | case 'disconnected': 27 | return 'Chat Disconnected'; 28 | case 'reconnecting': 29 | return 'Disconnected; trying to reconnect...'; 30 | case 'reconnected': 31 | return 'Back in Business!'; 32 | default: 33 | return ''; 34 | } 35 | }; 36 | 37 | return ( 38 |
      {renderNotification()}
      39 | ); 40 | } 41 | } 42 | 43 | export default applyTheme(Notification); 44 | -------------------------------------------------------------------------------- /src/components/Notification/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import Header from '.'; 5 | 6 | describe('
      ', () => { 7 | it('should render ', () => { 8 | const tree = render(
      ); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/Notification/style.css: -------------------------------------------------------------------------------- 1 | /* Messenger Theme */ 2 | 3 | .Notification__messenger { 4 | display: flex; 5 | background: rgba(243, 156, 18, 0.5); 6 | padding: 8px; 7 | } 8 | 9 | 10 | .Notification__messenger-disconnected { 11 | display: flex; 12 | background: rgba(235, 110, 96, 1); 13 | color: rgba(255, 255, 255, 1); 14 | padding: 8px 12px; 15 | border-right: 1px solid rgba(0,0,0,0.1); 16 | } 17 | 18 | .Notification__messenger-reconnected { 19 | display: flex; 20 | background: rgba(137, 220, 203, 1); 21 | color: rgba(255, 255, 255, 1); 22 | padding: 8px 12px; 23 | border-right: 1px solid rgba(0,0,0,0.1); 24 | } 25 | 26 | .Notification__messenger-reconnecting { 27 | display: flex; 28 | background: rgba(235, 190, 117, 1); 29 | color: rgba(255, 255, 255, 1); 30 | padding: 8px 12px; 31 | border-right: 1px solid rgba(0,0,0,0.1); 32 | } 33 | 34 | 35 | /* Float Theme */ 36 | 37 | /* .Notification--float { 38 | } 39 | 40 | .closed .Notification--float { 41 | } 42 | 43 | .Notification__title--float { 44 | } 45 | 46 | .Notification__closeBtn--float { 47 | } */ 48 | -------------------------------------------------------------------------------- /src/components/Skeleton/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | import './styles.css'; 4 | 5 | const Skeleton = () =>
      ; 6 | 7 | export default Skeleton; 8 | -------------------------------------------------------------------------------- /src/components/Skeleton/styles.css: -------------------------------------------------------------------------------- 1 | 2 | .Skeleton { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render 1`] = `"Hello World"`; 4 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './styles.css'; 5 | 6 | export class ThemeProvider extends Component { 7 | propTypes = { 8 | children: PropTypes.element, 9 | }; 10 | 11 | getChildContext () { 12 | const { ...context } = this.props; 13 | return context; 14 | } 15 | render ({ children }) { 16 | return (children && children[0]) || null; 17 | } 18 | } 19 | 20 | export const applyTheme = ComponentToWrap => 21 | class ThemeComponent extends Component { 22 | render () { 23 | const { theme, style } = this.context; 24 | return ; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/index.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import render from 'preact-render-to-string'; 3 | 4 | import ThemeProvider from '.'; 5 | 6 | 7 | describe('', () => { 8 | it('should render ', () => { 9 | const theme = {}; 10 | const style = 'messenger'; 11 | const tree = render(Hello World); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/styles.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .Skeleton { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { render, h } from 'preact'; 2 | import App from './components/App'; 3 | import theme from '../config/theme.json'; 4 | 5 | // Testing to see if this works with the Theme Editor 6 | // TODO: Figure out a better way to deal with this, but for now, hack it up! 7 | window._mnmlThemeObject = theme; // eslint-disable-line no-underscore-dangle 8 | 9 | // Render the app 10 | const div = document.createElement('div'); 11 | document.body.appendChild(div); 12 | 13 | div.id = 'mnml-chat'; 14 | 15 | render(, div); 16 | -------------------------------------------------------------------------------- /test/client.load.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "http://minimal.chat:8888?type=client" 3 | phases: 4 | - 5 | duration: 120 6 | arrivalRate: 5 7 | name: "Warm up" 8 | - 9 | pause: 10 10 | - 11 | duration: 60 12 | arrivalRate: 30 13 | name: "High load phase" 14 | scenarios: 15 | - 16 | engine: "socketio" 17 | flow: 18 | - 19 | emit: 20 | channel: "client:message" 21 | data: "{\"timestamp\":\"2017-07-09T01:51:37.539Z\",\"author\":\"client.FwbvVrxfGjw13HTNmv4a\",\"content\":\"Cool beans!\",\"chat\":\"FwbvVrxfGjw13HTNmv4a\"}" 22 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | // import 'regenerator-runtime/runtime'; 2 | // 3 | import 'jest-localstorage-mock'; 4 | import chai from 'chai'; 5 | import assertJsx, { options } from 'preact-jsx-chai'; 6 | 7 | // when checking VDOM assertions, don't compare functions, just nodes and attributes: 8 | options.functions = false; 9 | 10 | // activate the JSX assertion extension: 11 | chai.use(assertJsx); 12 | 13 | global.sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const ReplacePlugin = require('replace-bundle-webpack-plugin'); 5 | 6 | const config = require('./package.json'); 7 | 8 | const ENV = process.env.NODE_ENV || 'development'; 9 | const development = ENV !== 'production'; 10 | 11 | const PATHS = { 12 | BUILD: path.join(__dirname, '/dist'), 13 | SRC: path.join(__dirname, '/src'), 14 | MODULES: path.join(__dirname, '/node_modules'), 15 | }; 16 | 17 | let plugins = [ 18 | // TODO: What does this do? 19 | new webpack.NoEmitOnErrorsPlugin(), 20 | new webpack.EnvironmentPlugin(['NODE_ENV', 'PORT', 'REMOTE_HOST']) 21 | ]; 22 | 23 | if (!development) { 24 | // Minify source on production only 25 | // plugins.push(new webpack.optimize.UglifyJsPlugin()); 26 | 27 | // Strip out babel-helper invariant checks 28 | plugins.push(new ReplacePlugin([ 29 | { 30 | // This is actually the property name https://github.com/kimhou/replace-bundle-webpack-plugin/issues/1 31 | partten: /throw\s+(new\s+)?[a-zA-Z]+Error\s*\(/g, 32 | replacement: () => 'return;(', 33 | }, 34 | ])); 35 | } 36 | 37 | module.exports = { 38 | context: PATHS.SRC, 39 | entry: PATHS.SRC + '/index.js', 40 | output: { 41 | filename: `mnml${!development ? `-${config.version}` : ''}${!development ? '.min' : ''}.js`, 42 | path: PATHS.BUILD, 43 | publicPath: '/', 44 | }, 45 | optimization: { 46 | minimize: !development, 47 | }, 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.jsx?$/, 52 | include: [ PATHS.SRC ], 53 | exclude: [ PATHS.MODULES ], 54 | loader: 'babel-loader', 55 | options: { 56 | presets: [ '@babel/preset-env' ], 57 | plugins: [ 58 | [ '@babel/plugin-proposal-class-properties' ], 59 | [ '@babel/plugin-transform-react-jsx', { 'pragma': 'h' } ], 60 | ], 61 | }, 62 | }, 63 | { 64 | test: /\.css$/, 65 | include: [ PATHS.SRC ], 66 | exclude: [ PATHS.MODULES ], 67 | loader: 'style-loader!css-loader', 68 | }, 69 | ], 70 | }, 71 | plugins: plugins, 72 | resolve: { 73 | extensions: ['.jsx', '.js'], 74 | modules: [ 75 | // path.resolve(__dirname, "src/lib"), 76 | path.resolve(__dirname, 'node_modules'), 77 | 'node_modules', 78 | ], 79 | alias: { 80 | 'components': path.resolve(__dirname, 'src/components'), // used for tests 81 | 'react-dom/server': 'preact-render-to-string', 82 | 'react': 'preact-compat', 83 | 'react-dom': 'preact-compat' 84 | }, 85 | }, 86 | 87 | // TODO: What is cheap-module-eval-source-map? 88 | devtool: development ? 'source-map' : false, 89 | devServer: { 90 | port: process.env.PORT || 3000, 91 | host: process.env.HOST || 'localhost', 92 | publicPath: '/', 93 | contentBase: './example', 94 | // TODO: What his history API fallback? 95 | historyApiFallback: true, 96 | } 97 | }; 98 | --------------------------------------------------------------------------------