├── 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 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/views/layout.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SimpleChat
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------