├── CNAME ├── components ├── header │ ├── style.css │ ├── logo.png │ ├── close.png │ ├── outline.png │ └── index.js ├── home │ ├── style.css │ └── index.js ├── input │ ├── send.png │ └── index.js ├── intro │ ├── close.png │ ├── facebook.svg │ ├── github.svg │ ├── twitter.svg │ └── index.js ├── messages │ ├── read.png │ └── index.js └── spinner │ └── index.js ├── assets ├── favicon.ico └── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── robots.txt ├── deploy.sh ├── .gitignore ├── utility └── index.js ├── manifest.json ├── index.js ├── package.json ├── LICENSE ├── README.md └── style └── index.css /CNAME: -------------------------------------------------------------------------------- 1 | anonymouschat.in 2 | -------------------------------------------------------------------------------- /components/header/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /components/header/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/header/logo.png -------------------------------------------------------------------------------- /components/home/style.css: -------------------------------------------------------------------------------- 1 | .home { 2 | padding: 56px 20px; 3 | min-height: 100%; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /components/input/send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/input/send.png -------------------------------------------------------------------------------- /components/intro/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/intro/close.png -------------------------------------------------------------------------------- /components/header/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/header/close.png -------------------------------------------------------------------------------- /components/messages/read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/messages/read.png -------------------------------------------------------------------------------- /assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /components/header/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/components/header/outline.png -------------------------------------------------------------------------------- /assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gokulkrishh/anonymous-web/HEAD/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | git commit -am "Save local changes" 2 | git checkout -B gh-pages 3 | git add -f build 4 | git commit -am "Rebuild website" 5 | git filter-branch -f --prune-empty --subdirectory-filter build 6 | git push -f origin gh-pages 7 | git checkout - 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | *.zip 13 | config.json 14 | dummy.html 15 | -------------------------------------------------------------------------------- /utility/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utility = { 4 | showIntroScreen() { 5 | return !localStorage.getItem("visited"); 6 | }, 7 | 8 | getChatHash(otherUserId, currentUserId) { 9 | if (otherUserId > currentUserId) { 10 | return "chat_" + otherUserId + "_" + currentUserId; 11 | } 12 | else { 13 | return "chat_" + currentUserId + "_" + otherUserId; 14 | } 15 | } 16 | }; 17 | 18 | module.exports = utility; 19 | -------------------------------------------------------------------------------- /components/spinner/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from "preact"; 2 | 3 | export default class Spinner extends Component { 4 | static defaultProps = { 5 | showSpinner: false, 6 | spinnerText: "Looking for user..." 7 | } 8 | 9 | render() { 10 | const {showSpinner, spinnerText} = this.props; 11 | return ( 12 |
13 |

{spinnerText}

14 |
15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Anonymous Chat", 3 | "short_name": "Anonymous Chat", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#333", 9 | "icons": [{ 10 | "src": "/assets/icons/android-chrome-192x192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "/assets/icons/android-chrome-512x512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import { h } from 'preact'; 3 | import Home from './components/home'; 4 | import firebase from 'firebase/app'; 5 | 6 | const testEnv = { 7 | "apiKey": "AIzaSyB3X0VnbRACigiD1G1VcO0F8GFImzFIzdc", 8 | "authDomain": "test-anonymous-bcba0.firebaseapp.com", 9 | "databaseURL": "https://test-anonymous-bcba0.firebaseio.com", 10 | "projectId": "test-anonymous-bcba0", 11 | "storageBucket": "" 12 | }; 13 | 14 | firebase.initializeApp(testEnv); 15 | 16 | export default () => ( 17 |
18 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /components/intro/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anonymous-chat", 3 | "version": "2.0.0", 4 | "description": "Chat with strangers randomly.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "if-env NODE_ENV=production && npm run -s serve || npm run -s dev", 8 | "build": "preact build --no-prerender", 9 | "serve": "preact build --no-prerender && preact serve", 10 | "dev": "preact watch", 11 | "test": "eslint src && preact test" 12 | }, 13 | "author": "gokulkrishh", 14 | "license": "MIT", 15 | "eslintConfig": { 16 | "extends": "eslint-config-synacor" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^6.8.0", 20 | "if-env": "^1.0.0", 21 | "preact-cli": "^1.1.1" 22 | }, 23 | "dependencies": { 24 | "dotenv": "^4.0.0", 25 | "firebase": "^4.1.1", 26 | "preact": "^8.1.0", 27 | "preact-compat": "^3.16.0", 28 | "react-autobind": "^1.0.6", 29 | "react-mixin": "^3.0.5", 30 | "reactfire": "^1.0.0", 31 | "timeago.js": "^3.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Gokulakrishnan Kalaikovan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/intro/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/intro/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from "preact"; 2 | import style from "./style"; 3 | import logo from "./logo.png"; 4 | import outlineInfo from "./outline.png"; 5 | import closeBtn from "./close.png"; 6 | 7 | export default class Header extends Component { 8 | static defaultProps = { 9 | status: "connecting..." 10 | } 11 | 12 | constructor(props) { 13 | super(props); 14 | } 15 | 16 | htmlCloseBtn() { 17 | const {closeChatCallback, showCloseBtn} = this.props; 18 | if (!showCloseBtn) return
; 19 | return ( 20 |
21 | 24 |
25 | ); 26 | } 27 | 28 | render() { 29 | const {status} = this.props; 30 | return ( 31 |
32 | logo 33 |
34 | Anonymous Chat 35 | {status} 36 |
37 | 38 |
39 | 40 |
41 | 44 | {this.htmlCloseBtn()} 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /components/messages/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from "preact"; 2 | import database from "firebase/database"; 3 | import reactMixin from "react-mixin"; 4 | import reactFire from "reactfire"; 5 | import timeago from "timeago.js"; 6 | import read from "./read.png"; 7 | 8 | export default class Messages extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | chats: [] 13 | }; 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if (nextProps.otherUserId && nextProps.chatURL) { 18 | this.firebaseRef = database().ref(`chats/${nextProps.chatURL}/messages`); 19 | if (typeof this.firebaseRefs["chats"] === "undefined") { 20 | this.bindAsArray(this.firebaseRef, "chats"); 21 | } 22 | } 23 | } 24 | 25 | render() { 26 | const {chats} = this.state; 27 | const {userId} = this.props; 28 | const chatMessages = chats.map((chat, index) => { 29 | console.log(chat.timestamp) 30 | return( 31 |
32 |

33 | 34 | {chat.message} 35 | 36 | {timeago().format(chat.timestamp)} 37 | read 38 | 39 | 40 |

41 |
42 | ); 43 | }); 44 | return( 45 |
46 | {chatMessages} 47 |
48 | ); 49 | } 50 | } 51 | 52 | reactMixin(Messages.prototype, reactFire); 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | # [Anonymous Chat](https://anonymouschat.in) 4 | 5 | *A PreactJS powered progressive web (chat) application using Firebase Realtime Database.* 6 | 7 | ### [Live](https://anonymouschat.in) 8 | 9 | ## Features 10 | 11 | - Chat instantly with strangers (no login is required). 12 | 13 | - Messages sent, typing status. 14 | 15 | - One click to close and chat with another stranger. 16 | 17 | - Native app like experience. 18 | 19 | - Supported platform **Android**, **iOS** & **Windows**. 20 | 21 | - Supported browsers **Chrome**, **Firefox**, **Opera**, **Safari** & **Edge**. 22 | 23 | 24 | ## Build Tools 25 | 26 | - preact-cli 27 | 28 | - Material Icons 29 | 30 | - Firebase Realtime Database 31 | 32 | - Icons Generator 33 | 34 | *few other utility libraries, check my package.json file* 35 | 36 | ### Installation 37 | 38 | ````sh 39 | npm install 40 | ```` 41 | 42 | ### Run 43 | 44 | ````sh 45 | npm run start 46 | ```` 47 | 48 | ### Build 49 | 50 | ````sh 51 | npm run build 52 | ```` 53 | 54 | ### Deploy 55 | 56 | After running `npm run build`, use below command to deploy in gh-pages 57 | 58 | ````sh 59 | npm run deploy 60 | ```` 61 | 62 | ### Contributions & Feature Request 63 | 64 | If you find a bug or nice to have feature, please feel free to create an issue and PR is most welcome :) 65 | 66 | #### MIT Licensed 67 | -------------------------------------------------------------------------------- /components/intro/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from "preact"; 2 | import logo from "../header/logo.png"; 3 | import close from "./close.png"; 4 | import facebook from "./facebook.svg"; 5 | import twitter from "./twitter.svg"; 6 | import github from "./github.svg"; 7 | 8 | 9 | export default class Intro extends Component { 10 | render() { 11 | const {show} = this.props; 12 | if (!show) return
; 13 | return( 14 |
15 |
16 | 17 |
18 |
19 |
20 | logo 21 |

Anonymous Chat

22 |

Chat with strangers randomly.

23 |
24 | 25 |
26 | 27 | twitter 28 | 29 | 30 | github 31 | 32 |
33 |
34 | 35 |
36 |

Built with by Gokul

37 |
38 |
39 | ); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /components/input/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import database from 'firebase/database'; 3 | import autoBind from "react-autobind"; 4 | import send from './send.png'; 5 | 6 | export default class Input extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.firebaseDB = database(); 10 | this.currentChat = null; 11 | this.timeoutRef = null; 12 | this.userInput = null; 13 | this.chatMessageRef = null; 14 | autoBind(this); 15 | } 16 | 17 | componentDidMount() { 18 | this.userInput = document.querySelector("input"); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | const {chatURL, userId} = this.props; 23 | if (nextProps.chatURL !== chatURL) { 24 | this.chatMessageRef = this.firebaseDB.ref(`chats/${nextProps.chatURL}/messages`); 25 | } 26 | } 27 | 28 | clearTimeOut() { 29 | clearTimeout(this.timeoutRef); 30 | } 31 | 32 | handleKeyPress() { 33 | this.clearTimeOut(); 34 | const {chatURL, userId} = this.props; 35 | this.currentChat = this.firebaseDB.ref(`chats/${chatURL}/${userId}`); 36 | if (event.keyCode === 13) { 37 | this.currentChat.update({ 38 | typing: false 39 | }); 40 | this.sendMsg(); 41 | } 42 | else { 43 | this.currentChat.update({ 44 | typing: true 45 | }); 46 | } 47 | } 48 | 49 | handleKeyUp() { 50 | this.clearTimeOut(); 51 | this.timeoutRef = setTimeout(() => { 52 | this.currentChat.update({ 53 | typing: false 54 | }); 55 | }, 300); 56 | } 57 | 58 | sendMsg() { 59 | if (!this.userInput.value.replace(/^\s+|\s+$/g, "") || !this.chatMessageRef) { 60 | return false; 61 | } 62 | const timestamp = new Date(); 63 | this.chatMessageRef.push({ 64 | id: this.props.userId, 65 | message: this.userInput.value, 66 | timestamp: timestamp.toString() 67 | }); 68 | this.userInput.value = ""; 69 | } 70 | 71 | render() { 72 | return( 73 |
74 |
75 | 76 | 79 |
80 |
81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /components/home/index.js: -------------------------------------------------------------------------------- 1 | import style from "./style"; 2 | import { h, Component } from "preact"; 3 | import autoBind from "react-autobind"; 4 | import database from "firebase/database"; 5 | import utility from "../../utility"; 6 | import Header from "../header"; 7 | import Spinner from "../spinner"; 8 | import Input from "../input"; 9 | import Intro from "../intro"; 10 | import Messages from "../messages"; 11 | 12 | export default class Home extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | chatURL: null, 17 | otherUserId: null, 18 | showCloseBtn: false, 19 | showIntroScreen: utility.showIntroScreen(), 20 | showSpinner: true 21 | }; 22 | this.firebaseDB = database(); 23 | this.firebaseChatRef = this.firebaseDB.ref("chats"); 24 | this.firebaseUserRef = null; 25 | this.firebaseDBTyping = null; 26 | this.firebaseChatDisconnection = null; 27 | this.userId = window.navigator.userAgent.replace(/\D+/g, ""); 28 | autoBind(this); 29 | } 30 | 31 | componentWillMount() { 32 | this.checkFirebaseConnection(); 33 | this.checkForDisconnection(); 34 | } 35 | 36 | componentDidMount() { 37 | this.initializeChat(); 38 | } 39 | 40 | checkFirebaseConnection() { 41 | this.firebaseConnection = this.firebaseDB.ref(".info/connected"); 42 | this.firebaseConnection.on("value", (snap) => { 43 | if (snap.val() === true) { 44 | console.log("Came ---->"); 45 | if (this.currentChat) { 46 | this.currentChat.onDisconnect().remove(); 47 | } 48 | this.firebaseUserRef.onDisconnect().remove(); 49 | } 50 | }); 51 | } 52 | 53 | checkForDisconnection() { 54 | window.addEventListener("beforeunload", () => { 55 | this.removeConnection(); 56 | }); 57 | 58 | window.addEventListener("offline", () => { 59 | this.setState({ 60 | headerStatus: "no internet", 61 | spinnerText: "No internet connection" 62 | }); 63 | }); 64 | 65 | window.addEventListener("online", () => { 66 | this.setState({ 67 | headerStatus: "connected", 68 | spinnerText: "Looking for user..." 69 | }); 70 | }); 71 | } 72 | 73 | createUser() { 74 | this.firebaseUserRef = this.firebaseDB.ref(`chats/user_${this.userId}`); 75 | this.firebaseUserRef.update({ 76 | userId: this.userId, 77 | queued: true 78 | }); 79 | } 80 | 81 | initializeChat() { 82 | this.setState({ 83 | showCloseBtn: false, 84 | headerStatus: "connecting...", 85 | showSpinner: true, 86 | spinnerText: "Looking for user..." 87 | }, () => { 88 | this.createUser(); 89 | this.lookForUser(); 90 | }); 91 | } 92 | 93 | lookForUser() { 94 | this.firebaseChatRef.on("value", (snapshot) => { 95 | snapshot.forEach((data) => { 96 | const user = data.val(); 97 | if (user.userId !== this.userId && user.queued) { 98 | const chatURL = utility.getChatHash(user.userId, this.userId); 99 | const isChatAlreadyExist = snapshot.child(chatURL).exists(); 100 | if (!isChatAlreadyExist) { 101 | this.addChatConnection(chatURL, user.userId); 102 | } 103 | } 104 | }); 105 | }); 106 | } 107 | 108 | addChatConnection(chatURL, otherUserId) { 109 | this.firebaseUserRef.update({ 110 | queued: false 111 | }); 112 | 113 | this.currentChat = this.firebaseDB.ref(`chats/${chatURL}`); 114 | this.currentChat.update({ 115 | connection: true 116 | }); 117 | 118 | this.setState({ 119 | headerStatus: "connected", 120 | showCloseBtn: true, 121 | showSpinner: false, 122 | otherUserId, 123 | chatURL 124 | }, () => { 125 | this.listenToTyping(); 126 | this.checkUserDisconnection(); 127 | }); 128 | } 129 | 130 | listenToTyping() { 131 | const {chatURL, otherUserId} = this.state; 132 | this.firebaseDBTyping = this.firebaseDB.ref(`chats/${chatURL}/${otherUserId}`).on("value", (snapshot) => { 133 | var snapshotData = snapshot.val(); 134 | if (snapshotData && snapshotData.typing) { 135 | this.setState({ 136 | headerStatus: "typing..." 137 | }); 138 | } 139 | else { 140 | this.setState({ 141 | headerStatus: "online" 142 | }); 143 | } 144 | }); 145 | } 146 | 147 | checkUserDisconnection() { 148 | const {chatURL, otherUserId} = this.state; 149 | var counter = 0; 150 | this.firebaseChatRef.orderByChild("chat_").on("child_removed", (oldSnapshot) => { 151 | if (chatURL === oldSnapshot.key || otherUserId === oldSnapshot.key) { 152 | this.removeConnection(); 153 | } 154 | }); 155 | } 156 | 157 | removeConnection() { 158 | if (this.firebaseUserRef) { 159 | this.firebaseUserRef.remove(); 160 | } 161 | if (this.firebaseChatRef) { 162 | this.firebaseChatRef.remove((error) => { 163 | if (!error) { 164 | this.initializeChat(); 165 | } 166 | }); 167 | } 168 | } 169 | 170 | hideIntroCallback() { 171 | this.setState({showIntroScreen: false}, () => { 172 | localStorage.setItem("visited", true); 173 | }); 174 | } 175 | 176 | showIntroCallback() { 177 | this.setState({showIntroScreen: true}); 178 | } 179 | 180 | render() { 181 | const {firebaseRef} = this.props; 182 | const {chatURL, headerStatus, otherUserId, showCloseBtn, showIntroScreen, showSpinner, spinnerText} = this.state; 183 | 184 | return ( 185 |
186 |
187 | 188 | 189 |
190 | 191 |
192 | 193 |
194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | * { 52 | box-sizing: border-box; 53 | } 54 | 55 | html, 56 | body, 57 | #root { 58 | height: 100%; 59 | overflow: hidden; 60 | } 61 | 62 | body { 63 | background: #ebe4db; 64 | font-family: 'Roboto', Helvetica, Arial, sans-serif; 65 | -webkit-font-smoothing: antialiased; 66 | -webkit-tap-highlight-color: transparent; 67 | } 68 | 69 | .app__layout { 70 | display: flex; 71 | flex-direction: column; 72 | height: 100%; 73 | } 74 | 75 | header { 76 | width: 100%; 77 | height: 56px; 78 | background-color: #333; 79 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); 80 | color: #fff; 81 | display: flex; 82 | align-items: center; 83 | padding-left: 15px; 84 | padding-right: 10px; 85 | } 86 | 87 | .header__content { 88 | display: flex; 89 | flex-direction: column; 90 | } 91 | 92 | .header__title { 93 | font-size: 16px; 94 | font-weight: 600; 95 | } 96 | 97 | .header__logo { 98 | width: 50px; 99 | height: 50px; 100 | margin-right: 10px; 101 | } 102 | 103 | .header__status { 104 | font-size: 12px; 105 | margin-top: 6px; 106 | color: #fff; 107 | font-weight: 500; 108 | } 109 | 110 | .header__spacer { 111 | flex-grow: 1; 112 | } 113 | 114 | .header__icons { 115 | display: flex; 116 | flex-direction: row; 117 | justify-content: space-between; 118 | align-items: center; 119 | } 120 | 121 | .header__icons label { 122 | height: 45px; 123 | width: 45px; 124 | display: block; 125 | line-height: 60px; 126 | text-align: center; 127 | cursor: pointer; 128 | border-radius: 50%; 129 | } 130 | 131 | .header__icons label:active { 132 | background: rgba(252, 252, 252, 0.08); 133 | } 134 | 135 | .header__icons label:not(:last-child) { 136 | margin-right: 5px; 137 | } 138 | 139 | .header__icon--close { 140 | margin-left: 10px; 141 | } 142 | 143 | .header__close-icon { 144 | width: 29px; 145 | height: 29px; 146 | margin-top: 9px; 147 | } 148 | 149 | .header__icon { 150 | width: 27px; 151 | height: 27px; 152 | margin-top: 10px; 153 | } 154 | 155 | .app__content { 156 | flex: 1; 157 | overflow-y: auto; 158 | overflow-x: hidden; 159 | } 160 | 161 | .app__input-container { 162 | width: 100%; 163 | height: 50px; 164 | position: relative; 165 | margin-bottom: 8px; 166 | } 167 | 168 | .app__input { 169 | height: inherit; 170 | } 171 | 172 | .app__input input { 173 | position: relative; 174 | border: transparent; 175 | background: #fff; 176 | font-size: 17px; 177 | resize: none; 178 | width: calc(100% - 80px); 179 | height: inherit; 180 | padding-left: 15px; 181 | outline: none; 182 | border-radius: 4px; 183 | margin-left: 8px; 184 | } 185 | 186 | .app__input img { 187 | margin-left: 4px; 188 | margin-top: 2px; 189 | width: 30px; 190 | height: 30px; 191 | } 192 | 193 | .app__input button { 194 | position: absolute; 195 | right: 10px; 196 | top: 0; 197 | bottom: 0; 198 | background: #EC407A; 199 | color: #fff; 200 | width: 50px; 201 | height: 50px; 202 | border: 0; 203 | outline: none; 204 | border-radius: 50%; 205 | cursor: pointer; 206 | box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.4); 207 | -webkit-tap-highlight-color: transparent; 208 | } 209 | 210 | .app__input button:active { 211 | opacity: 0.9; 212 | } 213 | 214 | .spinner__container { 215 | background: #fff; 216 | } 217 | 218 | .spinner__container.none { 219 | display: none; 220 | } 221 | 222 | .spinner__container { 223 | position: absolute; 224 | z-index: 9999; 225 | top: 57px; 226 | right: 0; 227 | bottom: 0; 228 | left: 0; 229 | margin: auto; 230 | } 231 | 232 | .spinner__container p { 233 | position: absolute; 234 | top: -30px; 235 | right: 0; 236 | bottom: 0; 237 | left: 0; 238 | width: 80%; 239 | height: 0px; 240 | margin: auto; 241 | text-align: center; 242 | font-family: 'Open Sans',sans-serif; 243 | font-size: 20px; 244 | font-weight: 400; 245 | } 246 | 247 | .message__container p::after { 248 | position: absolute; 249 | top: 0; 250 | left: -9px; 251 | width: 0; 252 | height: 0; 253 | content: ""; 254 | border-width: 10px; 255 | border-style: solid; 256 | border-color: #fff transparent transparent; 257 | } 258 | 259 | .message__container p.user { 260 | float: right; 261 | background: #e1ffc7; 262 | } 263 | 264 | .message__container p.self { 265 | float: left; 266 | } 267 | 268 | .message__container p.user::after { 269 | top: 0; 270 | right: -8px; 271 | left: initial; 272 | border-color: #e1ffc7 transparent transparent; 273 | } 274 | 275 | .message__container .msg { 276 | font-size: 14px; 277 | line-height: 19px; 278 | word-wrap: break-word; 279 | color: rgb(38, 38, 38); 280 | } 281 | 282 | .message__container img { 283 | position: relative; 284 | top: 4px; 285 | right: -5px; 286 | width: 14px; 287 | height: 14px; 288 | } 289 | 290 | .message__container p { 291 | position: relative; 292 | clear: both; 293 | min-width: 40px; 294 | max-width: 85vw; 295 | margin: 15px 20px 0 20px; 296 | padding: 4px 10px; 297 | border-radius: 5px; 298 | background: #fff; 299 | box-shadow: 0 2px 0 -1px rgba(0, 0, 0, 0.12), 0 2px 1px 0px rgba(0, 0, 0, 0.12); 300 | } 301 | 302 | .message__container div:last-of-type p { 303 | margin-bottom: 10px; 304 | } 305 | 306 | .message__container .timestamp { 307 | display: flex; 308 | height: 20px; 309 | color: #757575; 310 | font-size: 10px; 311 | flex-direction: row; 312 | align-items: flex-start; 313 | float: right; 314 | padding-left: 7px; 315 | padding-top: 4px; 316 | font-weight: 500; 317 | } 318 | 319 | .intro__screen { 320 | position: fixed; 321 | top: 0; 322 | bottom: 0; 323 | right: 0; 324 | left: 0; 325 | background: #ffffff; 326 | z-index: 9999; 327 | } 328 | 329 | .intro__screen-close { 330 | float: right; 331 | margin-top: 10px; 332 | margin-right: 10px; 333 | width: 50px; 334 | height: 50px; 335 | text-align: center; 336 | line-height: 70px; 337 | border-radius: 50%; 338 | } 339 | 340 | .intro__screen-close img { 341 | width: 28px; 342 | height: 28px; 343 | cursor: pointer; 344 | } 345 | 346 | .intro__screen-close:active { 347 | opacity: 0.9; 348 | background-color: #ececec; 349 | } 350 | 351 | .intro__screen-container img { 352 | width: 130px; 353 | height: 130px; 354 | display: block; 355 | margin: 0 auto; 356 | } 357 | 358 | .intro__screen-container { 359 | position: absolute; 360 | left: 0; 361 | right: 0; 362 | bottom: 0; 363 | top: -55px; 364 | margin: auto; 365 | width: 300px; 366 | height: 300px; 367 | } 368 | 369 | .intro__screen p { 370 | text-align: center; 371 | font-size: 15px; 372 | margin-top: 18px; 373 | font-style: italic; 374 | } 375 | 376 | .intro__screen h2 { 377 | font-size: 24px; 378 | text-align: center; 379 | margin-top: 10px; 380 | } 381 | 382 | .intro__screen-footer { 383 | position: absolute; 384 | bottom: 20px; 385 | left: 0; 386 | right: 0; 387 | margin: auto; 388 | user-select: none; 389 | } 390 | 391 | .intro__screen-footer span { 392 | font-size: 17px; 393 | color: #f30b0b; 394 | } 395 | 396 | .intro__screen-footer p { 397 | word-spacing: 1px; 398 | font-size: 16px 399 | } 400 | 401 | .intro__screen-footer a { 402 | color: #000; 403 | text-decoration: none; 404 | } 405 | 406 | .intro__screen-social { 407 | display: flex; 408 | flex-direction: row; 409 | margin: 25px auto; 410 | width: 120px; 411 | justify-content: space-around; 412 | user-select: none; 413 | } 414 | 415 | .intro__screen-social img { 416 | width: 40px; 417 | height: 40px; 418 | cursor: pointer; 419 | } 420 | 421 | .intro__screen-social img.github-logo { 422 | width: 38px; 423 | height: 38px; 424 | margin-top: 3px; 425 | margin-left: 1px; 426 | } 427 | 428 | .intro__screen-footer p { 429 | font-style: normal; 430 | } 431 | --------------------------------------------------------------------------------