├── 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 |
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 |
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 |
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 |

21 |
Anonymous Chat
22 |
Chat with strangers randomly.
23 |
24 |
25 |
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 |
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 |
--------------------------------------------------------------------------------