├── Procfile ├── .gitignore ├── package.json ├── assets ├── css │ ├── mixins.styl │ └── style.styl └── js │ └── app.js ├── views ├── index.ejs └── layout.ejs ├── uglify-middleware.js ├── user-handler.js ├── static ├── js │ └── app.js └── css │ └── style.css ├── README.md └── app.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.sublime-project 3 | *.sublime-workspace 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "version": "0.0.1", 4 | "description": "A simple chat", 5 | "main": "app.js", 6 | "author": "Gabriele Cirulli", 7 | "license": "BSD", 8 | "dependencies": { 9 | "socket.io": "~0.9.16", 10 | "express": "~3.3.1", 11 | "ejs-locals": "~1.0.2", 12 | "stylus": "~0.32.1", 13 | "uglify-js": "~2.3.6", 14 | "mime": "~1.2.9" 15 | }, 16 | "engines": { 17 | "node": "0.10.12", 18 | "npm": "1.2.30" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/css/mixins.styl: -------------------------------------------------------------------------------- 1 | transition() 2 | -webkit-transition arguments 3 | -moz-transition arguments 4 | transition arguments 5 | 6 | animation() 7 | -webkit-animation arguments 8 | -moz-animation arguments 9 | animation arguments 10 | 11 | animation-delay() 12 | -webkit-animation-delay arguments 13 | -moz-animation-delay arguments 14 | animation-delay arguments 15 | 16 | animation-fill-mode() 17 | -webkit-animation-fill-mode arguments 18 | -moz-animation-fill-mode arguments 19 | animation-fill-mode arguments 20 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | <% layout('layout') %> 2 |
3 |

Welcome.

4 |

Please insert your nickname to begin:

5 |
6 | 7 |
8 |
9 | 18 | -------------------------------------------------------------------------------- /views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SimpleChat 6 | 7 | 8 | 9 |
10 |
11 |

SimpleChat

12 |

A simple chat service. Made by Gabriele Cirulli.

13 |
14 |
15 | <%- body %> 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /uglify-middleware.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs") 2 | UglifyJS = require("uglify-js"), 3 | path = require("path"), 4 | mime = require('mime'); 5 | 6 | exports.middleware = function (options) { 7 | return function (req, res, next) { 8 | var requestFile = req.url; 9 | var fileMime = mime.lookup(requestFile); 10 | 11 | if (fileMime === "application/javascript") { 12 | var sourcePath = path.join(options.src, req.url), 13 | srcExists = fs.existsSync(sourcePath); 14 | 15 | if (srcExists) { 16 | // Find dest path, check modification date 17 | var sourceStat = fs.statSync(sourcePath), 18 | destPath = path.join(options.dest, req.url), 19 | destExists = fs.existsSync(destPath), 20 | generate = function () { 21 | var minified = UglifyJS.minify(sourcePath); 22 | fs.writeFileSync(destPath, minified.code); 23 | } 24 | 25 | if (destExists) { 26 | var destStat = fs.statSync(destPath), 27 | sourceModificationTime = sourceStat.mtime.getTime(), 28 | destModificationTime = destStat.mtime.getTime(); 29 | 30 | if (sourceModificationTime > destModificationTime) { 31 | generate(); 32 | } 33 | } else { 34 | generate(); 35 | } 36 | } 37 | } 38 | 39 | next(); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /user-handler.js: -------------------------------------------------------------------------------- 1 | function UserHandler () { 2 | this.users = {}; 3 | } 4 | 5 | UserHandler.prototype.addUser = function (nickname) { 6 | var userId = this.generateUserId(), 7 | userKeys = Object.keys(this.users), 8 | self = this, 9 | notTaken; 10 | 11 | notTaken = userKeys.every(function (key) { 12 | return nickname !== self.users[key].nickname 13 | }); 14 | 15 | if (notTaken) { 16 | this.users[userId] = { 17 | nickname: nickname, 18 | color: this.generateUserColor(), 19 | lastMessage: null 20 | } 21 | 22 | return userId; 23 | } else { 24 | return false; 25 | } 26 | }; 27 | 28 | UserHandler.prototype.getUserById = function (userId) { 29 | return this.users[userId] || null; 30 | }; 31 | 32 | UserHandler.prototype.removeUser = function (userId) { 33 | delete this.users[userId]; 34 | }; 35 | 36 | UserHandler.prototype.generateUserId = function () { 37 | var id; 38 | do { 39 | id = ""; 40 | for (var i = 0; i < 10; i++) { 41 | id += Math.floor(Math.random() * 16).toString(16); 42 | } 43 | } while (id in this.users); 44 | 45 | return id; 46 | }; 47 | 48 | UserHandler.prototype.generateUserColor = function () { 49 | var allowedColors = ["#c0392b", "#16a085", "#27ae60", "#2980b9", "#2c3e50", "#8e44ad", "#d35400", "#f39c12", "#34495e", "#9b59b6", "#1abc9c"]; 50 | 51 | return allowedColors[Math.floor(Math.random() * allowedColors.length)]; 52 | }; 53 | 54 | exports.UserHandler = UserHandler; 55 | -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){function e(e,n){var a=$(document.createElement("li")).addClass("message "+(n?"external":"internal")),o=$(document.createElement("div")).text(e.message).css({backgroundColor:e.color}).attr("data-color",e.color),r=$(document.createElement("div")).addClass("tip").css({borderColor:e.color}),s=$(document.createElement("div")).text(e.nickname);o.addClass("text"),o.append(r),s.addClass("nickname"),n?a.append(o,s):a.append(s,o),c.append(a),t()}function n(e){var n=$(document.createElement("li")).addClass("info");n.text(e.message),c.append(n),t()}function t(){c.scrollTop(c[0].scrollHeight)}var a=io.connect(),o=$(".chatBody .welcomeBlock"),r=o.find(".nicknameForm"),s=r.find(".nicknameInput"),d=$(".chatBody .chatBlock"),c=d.find(".messageList"),i=d.find(".messageForm"),m=i.find(".indicator"),l=i.find(".messageInput"),u=400,f=!1,p=!1;$(".nicknameForm").submit(function(e){var n=s.val().trim();s.attr("disabled",!0),s.blur(),e.preventDefault(),n&&!f&&(f=!0,a.emit("enter",{nickname:n}),a.on("enter response",function(e){if(e.accepted)p=!0,r.find(".error").addClass("hidden"),o.addClass("hidden"),setTimeout(function(){o.remove(),d.removeClass("hidden"),l.focus()},500);else{r.find(".error").remove();var n=$(document.createElement("p")).addClass("error").text(e.message);r.append(n),s.removeAttr("disabled"),s.focus(),f=!1}}))}),l.keyup(function(){var e=$(this).val(),n=e.length,t=u-n;0===n?m.empty():(m.text(t),m.attr("class","indicator "+(0>t?"bad":"")))}),i.submit(function(e){var n=l.val().trim();e.preventDefault(),n&&(a.emit("message",{message:n}),l.val(""),m.empty(),i.find(".error").remove())}),a.on("message",function(n){p&&e(n,n.external)}),a.on("message response",function(e){var n=$(document.createElement("p")).addClass("error").text(e.message);i.append(n)}),a.on("info",function(e){p&&n(e)})}); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleChat 2 | This is a quick app I wrote in an evening two days before my final oral exam, back in school. It was meant as an example for what you can accomplish using Node and Socket.IO. 3 | 4 | Feel free to do whatever you want with it, but keep in mind the code is a bit rough, unoptimized, probably buggy and also insecure (it's quite easy to crash the server by sending the wrong data). This app was really only meant as a proof of concept, but I could expand on it in the future *(or merge your changes if you do!)*. 5 | 6 | ## Installing 7 | To install, clone this repo on your PC, `cd` into the folder and then go: ```npm install```. That should install all of the dependencies. 8 | To start the app, run: ```node app.js``` 9 | 10 | ## License 11 | Simplified BSD: 12 | 13 | ``` 14 | Copyright (c) 2013, Gabriele Cirulli 15 | All rights reserved. 16 | 17 | Redistribution and use in source and binary forms, with or without 18 | modification, are permitted provided that the following conditions are met: 19 | 20 | 1. Redistributions of source code must retain the above copyright notice, this 21 | list of conditions and the following disclaimer. 22 | 2. Redistributions in binary form must reproduce the above copyright notice, 23 | this list of conditions and the following disclaimer in the documentation 24 | and/or other materials provided with the distribution. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 27 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 30 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 32 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 33 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 35 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | 37 | The views and conclusions contained in the software and documentation are those 38 | of the authors and should not be interpreted as representing official policies, 39 | either expressed or implied, of the FreeBSD Project. 40 | ``` 41 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | express = require('express'), 3 | socket = require('socket.io'), 4 | ejs = require('ejs-locals'), 5 | stylus = require('stylus'), 6 | uglify = require("./uglify-middleware.js"), 7 | UserHandler = require("./user-handler.js").UserHandler, 8 | app = express(), 9 | server = http.createServer(app), 10 | io = socket.listen(server); 11 | 12 | // Configuration 13 | app.engine('ejs', ejs); 14 | app.set('view engine', 'ejs'); 15 | app.use(stylus.middleware({ 16 | src: __dirname + "/assets", 17 | dest: __dirname + "/static" 18 | })); 19 | app.use(uglify.middleware({ 20 | src: __dirname + "/assets", 21 | dest: __dirname + "/static" 22 | })); 23 | app.use(express.static(__dirname + "/static")); 24 | 25 | // Routes 26 | app.get("*", function (req, res) { 27 | res.render("index"); 28 | }); 29 | 30 | // Log level 31 | io.set('log level', 1); 32 | 33 | // Socket.IO 34 | var userHandler = new UserHandler(); 35 | 36 | io.sockets.on("connection", function (socket) { 37 | socket.on("enter", function (data) { 38 | data.nickname = data.nickname.trim(); 39 | 40 | if (data.nickname && data.nickname.length <= 35) { 41 | var userId = userHandler.addUser(data.nickname); 42 | 43 | if (userId) { 44 | socket.set("userId", userId, function () { 45 | socket.broadcast.emit("info", { message: data.nickname + " has joined." }); 46 | socket.emit("enter response", { accepted: true }); 47 | }); 48 | } else { 49 | socket.emit("enter response", { accepted: false, message: "The nickname is already taken." }); 50 | } 51 | } else { 52 | socket.emit("enter response", { accepted: false, message: "The nickname is invalid (max. 35 characters)." }); 53 | } 54 | }); 55 | 56 | socket.on("message", function (data) { 57 | data.message = data.message.trim(); 58 | 59 | if (data.message.length <= 400) { 60 | socket.get("userId", function (err, userId) { 61 | if (!err && userId) { 62 | var user = userHandler.getUserById(userId), 63 | messageObject = { 64 | message: data.message, 65 | nickname: user.nickname, 66 | color: user.color, 67 | external: true 68 | }; 69 | 70 | if (Date.now() - user.lastMessage < 1500) { 71 | socket.emit("message response", { accepted: false, message: "Slow down! You're posting too fast." }); 72 | } else { 73 | user.lastMessage = Date.now(); 74 | 75 | socket.broadcast.emit("message", messageObject); 76 | 77 | messageObject.external = false; 78 | 79 | socket.emit("message", messageObject); 80 | } 81 | 82 | 83 | } 84 | }); 85 | } else { 86 | socket.emit("message response", { accepted: false, message: "The message is too long." }) 87 | } 88 | }); 89 | 90 | socket.on("disconnect", function () { 91 | socket.get("userId", function (err, userId) { 92 | if (!err && userId) { 93 | var user = userHandler.getUserById(userId); 94 | if (user) { 95 | userHandler.removeUser(userId); 96 | socket.broadcast.emit("info", { message: user.nickname + " has left." }); 97 | } 98 | } 99 | }); 100 | }); 101 | }); 102 | 103 | // Start listening 104 | var port = process.env.PORT || 7000, 105 | host = process.env.HOST || "0.0.0.0"; 106 | 107 | server.listen(port, host, function() { 108 | console.log("Listening on: " + host + ":" + port); 109 | }); 110 | -------------------------------------------------------------------------------- /assets/css/style.styl: -------------------------------------------------------------------------------- 1 | @import "mixins" 2 | 3 | main-color = #333 4 | 5 | @keyframes enter 6 | from 7 | top -100px 8 | opacity 0 9 | to 10 | top 0 11 | opacity 1 12 | 13 | @keyframes enter-bottom 14 | from 15 | top 200px 16 | opacity 0 17 | to 18 | top 0 19 | opacity 1 20 | 21 | @keyframes fade-in 22 | from 23 | opacity 0 24 | to 25 | opacity 1 26 | 27 | 28 | html, body 29 | margin 0 30 | padding 0 31 | font 18px/1.5 Helvetica Neue, Arial, sans-serif 32 | color main-color 33 | background #fafafa 34 | 35 | a 36 | text-decoration none 37 | color #2980b9 38 | &:hover 39 | color #3498db 40 | 41 | p 42 | margin 0 43 | margin-bottom 10px 44 | 45 | &.error 46 | margin-top 10px 47 | color #c0392b 48 | animation fade-in 350ms ease 49 | animation-fill-mode backwards 50 | 51 | &.hidden 52 | transition opacity 350ms ease 53 | opacity 0 54 | 55 | 56 | input 57 | font inherit 58 | color inherit 59 | width 100% 60 | display block 61 | box-sizing border-box 62 | padding 5px 10px 63 | -webkit-appearance none 64 | border 1px solid lighten(main-color, 80%) 65 | background #fff 66 | color lighten(main-color, 20%) 67 | 68 | &:focus 69 | border 1px solid #3498db 70 | outline none 71 | 72 | &[disabled] 73 | opacity 0.5 74 | 75 | section.intro 76 | margin-bottom 30px 77 | 78 | h1 79 | margin 0 80 | margin-bottom 5px 81 | font-size 20px 82 | 83 | p.introText 84 | margin 0 85 | color: lighten(main-color, 50%) 86 | 87 | .content 88 | width 400px 89 | margin 0 auto 90 | padding-top 50px 91 | padding-bottom 30px 92 | 93 | .chatBlock 94 | position relative 95 | 96 | &:not(.hidden) 97 | animation enter 700ms ease 98 | animation-delay 200ms 99 | animation-fill-mode backwards 100 | 101 | &.hidden 102 | opacity 0 103 | display none 104 | 105 | .messageForm 106 | &:after 107 | display block 108 | content '' 109 | clear both 110 | width 0 111 | height 0 112 | 113 | .indicator 114 | margin-top 10px 115 | color lighten(main-color, 50%) 116 | float right 117 | 118 | &.bad 119 | color #c0392b 120 | 121 | .messageList 122 | padding 0 123 | margin 0 124 | display block 125 | width 100% 126 | height 600px 127 | overflow hidden 128 | overflow-y scroll 129 | margin-bottom 30px 130 | //background #ccc 131 | 132 | .message, .info 133 | display block 134 | margin 0 135 | margin-bottom 20px 136 | padding 0 137 | width 100% 138 | clear both 139 | 140 | position relative 141 | animation enter 400ms ease 142 | animation-fill-mode backwards 143 | 144 | .info 145 | color lighten(main-color, 50%) 146 | 147 | .message 148 | 149 | div 150 | box-sizing border-box 151 | float right 152 | word-wrap break-word 153 | 154 | .nickname 155 | width 35% 156 | padding 10px 15px 157 | color lighten(main-color, 50%) 158 | 159 | .text 160 | width 65% 161 | padding 10px 24px 162 | min-height 48px 163 | color #fafafa 164 | border-radius 24px 165 | position relative 166 | 167 | .tip 168 | border-top 10px solid transparent !important 169 | border-bottom 10px solid transparent !important 170 | border-left 15px solid 171 | position absolute 172 | right -12px 173 | top 14px 174 | 175 | &.external 176 | .nickname 177 | text-align right 178 | padding-right 25px 179 | 180 | .tip 181 | border-left none 182 | border-right 15px solid 183 | right auto 184 | left -12px 185 | 186 | &.internal 187 | .nickname 188 | padding-left 25px 189 | 190 | &:after 191 | content '' 192 | display block 193 | width 0 194 | height 0 195 | clear both 196 | 197 | .welcomeBlock 198 | margin-top 200px 199 | position relative 200 | animation enter 500ms ease 201 | animation-delay 600ms 202 | animation-fill-mode backwards 203 | 204 | p:first-child 205 | font-weight bold 206 | margin-bottom 5px 207 | 208 | &.hidden 209 | top 200px 210 | opacity 0 211 | transition all 500ms cubic-bezier(0.715, 0.005, 0.790, 0.085); 212 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var socket = io.connect(), 3 | welcomeBlock = $(".chatBody .welcomeBlock"), 4 | nicknameForm = welcomeBlock.find(".nicknameForm"), 5 | nicknameInput = nicknameForm.find(".nicknameInput"), 6 | chatBlock = $(".chatBody .chatBlock"), 7 | messageList = chatBlock.find(".messageList"), 8 | messageForm = chatBlock.find(".messageForm"), 9 | lenIndicator = messageForm.find(".indicator"), 10 | messageInput = messageForm.find(".messageInput"), 11 | maxLength = 400, 12 | connecting = false, 13 | entered = false; 14 | 15 | $(".nicknameForm").submit(function (e) { 16 | var nickname = nicknameInput.val().trim(); 17 | 18 | nicknameInput.attr("disabled", true); 19 | nicknameInput.blur(); 20 | 21 | e.preventDefault(); 22 | 23 | if (nickname && !connecting) { 24 | connecting = true; 25 | socket.emit("enter", { nickname: nickname }); 26 | 27 | socket.on("enter response", function (data) { 28 | if (data.accepted) { 29 | // console.log("ACCEPTED"); 30 | 31 | entered = true; 32 | 33 | nicknameForm.find(".error").addClass("hidden"); 34 | welcomeBlock.addClass("hidden"); 35 | 36 | setTimeout(function () { 37 | welcomeBlock.remove(); 38 | chatBlock.removeClass("hidden"); 39 | messageInput.focus(); 40 | }, 500); 41 | } else { 42 | // console.log("REJECTED: " + data.message); 43 | nicknameForm.find(".error").remove(); 44 | var errorParagraph = $(document.createElement("p")).addClass("error").text(data.message); 45 | nicknameForm.append(errorParagraph); 46 | nicknameInput.removeAttr("disabled"); 47 | nicknameInput.focus(); 48 | 49 | connecting = false; 50 | } 51 | }); 52 | } 53 | }); 54 | 55 | // Send messages 56 | messageInput.keyup(function () { 57 | var text = $(this).val(), 58 | textLength = text.length, 59 | remaining = maxLength - textLength; 60 | 61 | if (textLength === 0) { 62 | lenIndicator.empty(); 63 | } else { 64 | lenIndicator.text(remaining); 65 | lenIndicator.attr("class", "indicator " + (remaining < 0 ? "bad" : "")); 66 | } 67 | }); 68 | 69 | messageForm.submit(function (e) { 70 | var message = messageInput.val().trim(); 71 | 72 | e.preventDefault(); 73 | 74 | if (message) { 75 | socket.emit("message", { message: message }); 76 | messageInput.val(""); 77 | lenIndicator.empty(); 78 | messageForm.find(".error").remove(); 79 | } 80 | }); 81 | 82 | // Receive messages 83 | socket.on("message", function (data) { 84 | if (entered) { 85 | addMessage(data, data.external); 86 | } 87 | }); 88 | 89 | // Message error 90 | socket.on("message response", function (data) { 91 | var errorParagraph = $(document.createElement("p")).addClass("error").text(data.message); 92 | messageForm.append(errorParagraph); 93 | }); 94 | 95 | // Receive information 96 | socket.on("info", function (data) { 97 | // console.log(data); 98 | if (entered) { 99 | addInfo(data); 100 | } 101 | }); 102 | 103 | // Other functions 104 | function addMessage (data, external) { 105 | var message = $(document.createElement("li")) 106 | .addClass("message " + (external ? "external" : "internal")), 107 | textContainer = $(document.createElement("div")) 108 | .text(data.message) 109 | .css({ backgroundColor: data.color }) 110 | .attr("data-color", data.color), 111 | textTip = $(document.createElement("div")) 112 | .addClass("tip") 113 | .css({ borderColor: data.color }), 114 | userContainer = $(document.createElement("div")) 115 | .text(data.nickname); 116 | 117 | textContainer.addClass("text"); 118 | textContainer.append(textTip); 119 | userContainer.addClass("nickname"); 120 | 121 | if (external) { 122 | message.append(textContainer, userContainer); 123 | } else { 124 | message.append(userContainer, textContainer); 125 | } 126 | 127 | messageList.append(message); 128 | 129 | scrollList(); 130 | } 131 | 132 | function addInfo (data) { 133 | // console.log(data); 134 | var infoBox = $(document.createElement("li")).addClass("info"); 135 | 136 | infoBox.text(data.message); 137 | 138 | messageList.append(infoBox); 139 | 140 | scrollList(); 141 | } 142 | 143 | function scrollList () { 144 | messageList.scrollTop(messageList[0].scrollHeight); 145 | } 146 | }); 147 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font: 18px/1.5 Helvetica Neue, Arial, sans-serif; 6 | color: #333; 7 | background: #fafafa; 8 | } 9 | a { 10 | text-decoration: none; 11 | color: #2980b9; 12 | } 13 | a:hover { 14 | color: #3498db; 15 | } 16 | p { 17 | margin: 0; 18 | margin-bottom: 10px; 19 | } 20 | p.error { 21 | margin-top: 10px; 22 | color: #c0392b; 23 | -webkit-animation: fade-in 350ms ease; 24 | -moz-animation: fade-in 350ms ease; 25 | animation: fade-in 350ms ease; 26 | -webkit-animation-fill-mode: backwards; 27 | -moz-animation-fill-mode: backwards; 28 | animation-fill-mode: backwards; 29 | } 30 | p.error.hidden { 31 | -webkit-transition: opacity 350ms ease; 32 | -moz-transition: opacity 350ms ease; 33 | transition: opacity 350ms ease; 34 | opacity: 0; 35 | } 36 | input { 37 | font: inherit; 38 | color: inherit; 39 | width: 100%; 40 | display: block; 41 | box-sizing: border-box; 42 | padding: 5px 10px; 43 | -webkit-appearance: none; 44 | border: 1px solid #d6d6d6; 45 | background: #fff; 46 | color: #5c5c5c; 47 | } 48 | input:focus { 49 | border: 1px solid #3498db; 50 | outline: none; 51 | } 52 | input[disabled] { 53 | opacity: 0.5; 54 | } 55 | section.intro { 56 | margin-bottom: 30px; 57 | } 58 | h1 { 59 | margin: 0; 60 | margin-bottom: 5px; 61 | font-size: 20px; 62 | } 63 | p.introText { 64 | margin: 0; 65 | color: #999; 66 | } 67 | .content { 68 | width: 400px; 69 | margin: 0 auto; 70 | padding-top: 50px; 71 | padding-bottom: 30px; 72 | } 73 | .chatBlock { 74 | position: relative; 75 | } 76 | .chatBlock:not(.hidden) { 77 | -webkit-animation: enter 700ms ease; 78 | -moz-animation: enter 700ms ease; 79 | animation: enter 700ms ease; 80 | -webkit-animation-delay: 200ms; 81 | -moz-animation-delay: 200ms; 82 | animation-delay: 200ms; 83 | -webkit-animation-fill-mode: backwards; 84 | -moz-animation-fill-mode: backwards; 85 | animation-fill-mode: backwards; 86 | } 87 | .chatBlock.hidden { 88 | opacity: 0; 89 | display: none; 90 | } 91 | .chatBlock .messageForm:after { 92 | display: block; 93 | content: ''; 94 | clear: both; 95 | width: 0; 96 | height: 0; 97 | } 98 | .chatBlock .messageForm .indicator { 99 | margin-top: 10px; 100 | color: #999; 101 | float: right; 102 | } 103 | .chatBlock .messageForm .indicator.bad { 104 | color: #c0392b; 105 | } 106 | .chatBlock .messageList { 107 | padding: 0; 108 | margin: 0; 109 | display: block; 110 | width: 100%; 111 | height: 600px; 112 | overflow: hidden; 113 | overflow-y: scroll; 114 | margin-bottom: 30px; 115 | } 116 | .chatBlock .messageList .message, 117 | .chatBlock .messageList .info { 118 | display: block; 119 | margin: 0; 120 | margin-bottom: 20px; 121 | padding: 0; 122 | width: 100%; 123 | clear: both; 124 | position: relative; 125 | -webkit-animation: enter 400ms ease; 126 | -moz-animation: enter 400ms ease; 127 | animation: enter 400ms ease; 128 | -webkit-animation-fill-mode: backwards; 129 | -moz-animation-fill-mode: backwards; 130 | animation-fill-mode: backwards; 131 | } 132 | .chatBlock .messageList .info { 133 | color: #999; 134 | } 135 | .chatBlock .messageList .message div { 136 | box-sizing: border-box; 137 | float: right; 138 | word-wrap: break-word; 139 | } 140 | .chatBlock .messageList .message .nickname { 141 | width: 35%; 142 | padding: 10px 15px; 143 | color: #999; 144 | } 145 | .chatBlock .messageList .message .text { 146 | width: 65%; 147 | padding: 10px 24px; 148 | min-height: 48px; 149 | color: #fafafa; 150 | border-radius: 24px; 151 | position: relative; 152 | } 153 | .chatBlock .messageList .message .text .tip { 154 | border-top: 10px solid transparent !important; 155 | border-bottom: 10px solid transparent !important; 156 | border-left: 15px solid; 157 | position: absolute; 158 | right: -12px; 159 | top: 14px; 160 | } 161 | .chatBlock .messageList .message.external .nickname { 162 | text-align: right; 163 | padding-right: 25px; 164 | } 165 | .chatBlock .messageList .message.external .tip { 166 | border-left: none; 167 | border-right: 15px solid; 168 | right: auto; 169 | left: -12px; 170 | } 171 | .chatBlock .messageList .message.internal .nickname { 172 | padding-left: 25px; 173 | } 174 | .chatBlock .messageList .message:after { 175 | content: ''; 176 | display: block; 177 | width: 0; 178 | height: 0; 179 | clear: both; 180 | } 181 | .welcomeBlock { 182 | margin-top: 200px; 183 | position: relative; 184 | -webkit-animation: enter 500ms ease; 185 | -moz-animation: enter 500ms ease; 186 | animation: enter 500ms ease; 187 | -webkit-animation-delay: 600ms; 188 | -moz-animation-delay: 600ms; 189 | animation-delay: 600ms; 190 | -webkit-animation-fill-mode: backwards; 191 | -moz-animation-fill-mode: backwards; 192 | animation-fill-mode: backwards; 193 | } 194 | .welcomeBlock p:first-child { 195 | font-weight: bold; 196 | margin-bottom: 5px; 197 | } 198 | .welcomeBlock.hidden { 199 | top: 200px; 200 | opacity: 0; 201 | -webkit-transition: all 500ms cubic-bezier(0.715, 0.005, 0.79, 0.085); 202 | -moz-transition: all 500ms cubic-bezier(0.715, 0.005, 0.79, 0.085); 203 | transition: all 500ms cubic-bezier(0.715, 0.005, 0.79, 0.085); 204 | } 205 | @-moz-keyframes enter { 206 | 0% { 207 | top: -100px; 208 | opacity: 0; 209 | } 210 | 211 | 100% { 212 | top: 0; 213 | opacity: 1; 214 | } 215 | } 216 | @-webkit-keyframes enter { 217 | 0% { 218 | top: -100px; 219 | opacity: 0; 220 | } 221 | 222 | 100% { 223 | top: 0; 224 | opacity: 1; 225 | } 226 | } 227 | @-o-keyframes enter { 228 | 0% { 229 | top: -100px; 230 | opacity: 0; 231 | } 232 | 233 | 100% { 234 | top: 0; 235 | opacity: 1; 236 | } 237 | } 238 | @-ms-keyframes enter { 239 | 0% { 240 | top: -100px; 241 | opacity: 0; 242 | } 243 | 244 | 100% { 245 | top: 0; 246 | opacity: 1; 247 | } 248 | } 249 | @keyframes enter { 250 | 0% { 251 | top: -100px; 252 | opacity: 0; 253 | } 254 | 255 | 100% { 256 | top: 0; 257 | opacity: 1; 258 | } 259 | } 260 | @-moz-keyframes enter-bottom { 261 | 0% { 262 | top: 200px; 263 | opacity: 0; 264 | } 265 | 266 | 100% { 267 | top: 0; 268 | opacity: 1; 269 | } 270 | } 271 | @-webkit-keyframes enter-bottom { 272 | 0% { 273 | top: 200px; 274 | opacity: 0; 275 | } 276 | 277 | 100% { 278 | top: 0; 279 | opacity: 1; 280 | } 281 | } 282 | @-o-keyframes enter-bottom { 283 | 0% { 284 | top: 200px; 285 | opacity: 0; 286 | } 287 | 288 | 100% { 289 | top: 0; 290 | opacity: 1; 291 | } 292 | } 293 | @-ms-keyframes enter-bottom { 294 | 0% { 295 | top: 200px; 296 | opacity: 0; 297 | } 298 | 299 | 100% { 300 | top: 0; 301 | opacity: 1; 302 | } 303 | } 304 | @keyframes enter-bottom { 305 | 0% { 306 | top: 200px; 307 | opacity: 0; 308 | } 309 | 310 | 100% { 311 | top: 0; 312 | opacity: 1; 313 | } 314 | } 315 | @-moz-keyframes fade-in { 316 | 0% { 317 | opacity: 0; 318 | } 319 | 320 | 100% { 321 | opacity: 1; 322 | } 323 | } 324 | @-webkit-keyframes fade-in { 325 | 0% { 326 | opacity: 0; 327 | } 328 | 329 | 100% { 330 | opacity: 1; 331 | } 332 | } 333 | @-o-keyframes fade-in { 334 | 0% { 335 | opacity: 0; 336 | } 337 | 338 | 100% { 339 | opacity: 1; 340 | } 341 | } 342 | @-ms-keyframes fade-in { 343 | 0% { 344 | opacity: 0; 345 | } 346 | 347 | 100% { 348 | opacity: 1; 349 | } 350 | } 351 | @keyframes fade-in { 352 | 0% { 353 | opacity: 0; 354 | } 355 | 356 | 100% { 357 | opacity: 1; 358 | } 359 | } 360 | --------------------------------------------------------------------------------