├── Procfile
├── .gitignore
├── Public
├── images
│ └── vapor-logo.png
├── .sass-cache
│ └── 32330c74f84c325718a9f705439c3d456ca7267f
│ │ └── style.sassc
├── styles
│ ├── normalize.css
│ ├── style.css.map
│ └── style.css
├── scripts
│ └── chat.js
└── scss
│ └── style.sass
├── README.md
├── Sources
├── App
│ ├── WebSocket+JSON.swift
│ ├── Config+Setup.swift
│ ├── String+Truncate.swift
│ ├── Room.swift
│ └── Droplet+Setup.swift
└── Run
│ └── main.swift
├── Package.swift
├── Resources
└── Views
│ └── welcome.html
└── Package.pins
/Procfile:
--------------------------------------------------------------------------------
1 | web: .build/release/Run --env=production
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Packages
2 | Build
3 | .build
4 | xcuserdata
5 | *.xcodeproj
6 | Config/*.json
7 |
--------------------------------------------------------------------------------
/Public/images/vapor-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor-community/chat-example/HEAD/Public/images/vapor-logo.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Realtime chat example hosted by Vapor
2 | Original design by SupahFunk: http://codepen.io/supah/pen/jqOBqp?utm_source=bypeople
3 |
--------------------------------------------------------------------------------
/Public/.sass-cache/32330c74f84c325718a9f705439c3d456ca7267f/style.sassc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor-community/chat-example/HEAD/Public/.sass-cache/32330c74f84c325718a9f705439c3d456ca7267f/style.sassc
--------------------------------------------------------------------------------
/Sources/App/WebSocket+JSON.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | extension WebSocket {
4 | func send(_ json: JSON) throws {
5 | let js = try json.makeBytes()
6 | try send(js.makeString())
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/App/Config+Setup.swift:
--------------------------------------------------------------------------------
1 | @_exported import Vapor
2 |
3 | extension Config {
4 | public func setup() throws {
5 | // allow fuzzy conversions for these types
6 | // (add your own types here)
7 | Node.fuzzy = [JSON.self, Node.self]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/App/String+Truncate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension String {
4 | func truncated(to max: Int) -> String {
5 | if characters.count > max {
6 | return substring(
7 | to: index(
8 | startIndex,
9 | offsetBy: max
10 | )
11 | )
12 | }
13 |
14 | return self
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "VaporChat",
5 | targets: [
6 | Target(name: "App"),
7 | Target(name: "Run", dependencies: ["App"])
8 | ],
9 | dependencies: [
10 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
11 | ],
12 | exclude: [
13 | "Config",
14 | "Database",
15 | "Public",
16 | "Resources",
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/Sources/Run/main.swift:
--------------------------------------------------------------------------------
1 | import App
2 |
3 | /// We have isolated all of our App's logic into
4 | /// the App module because it makes our app
5 | /// more testable.
6 | ///
7 | /// In general, the executable portion of our App
8 | /// shouldn't include much more code than is presented
9 | /// here.
10 | ///
11 | /// We simply initialize our Droplet, optionally
12 | /// passing in values if necessary
13 | /// Then, we pass it to our App's setup function
14 | /// this should setup all the routes and special
15 | /// features of our app
16 | ///
17 | /// .run() runs the Droplet's commands,
18 | /// if no command is given, it will default to "serve"
19 | let config = try Config()
20 | try config.setup()
21 |
22 | let drop = try Droplet(config)
23 | try drop.setup()
24 |
25 | try drop.run()
26 |
--------------------------------------------------------------------------------
/Sources/App/Room.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | class Room {
4 | var connections: [String: WebSocket]
5 |
6 | func bot(_ message: String) {
7 | send(name: "Bot", message: message)
8 | }
9 |
10 | func send(name: String, message: String) {
11 | let message = message.truncated(to: 256)
12 |
13 | let messageNode: [String: NodeRepresentable] = [
14 | "username": name,
15 | "message": message
16 | ]
17 |
18 | guard let json = try? JSON(node: messageNode) else {
19 | return
20 | }
21 |
22 | for (username, socket) in connections {
23 | guard username != name else {
24 | continue
25 | }
26 |
27 | try? socket.send(json)
28 | }
29 | }
30 |
31 | init() {
32 | connections = [:]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/App/Droplet+Setup.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Foundation
3 |
4 | let room = Room()
5 |
6 | extension Droplet {
7 | public func setup() throws {
8 | get("/") { _ in
9 | try self.view.make("welcome.html")
10 | }
11 |
12 | socket("chat") { req, ws in
13 | var pingTimer: DispatchSourceTimer? = nil
14 | var username: String? = nil
15 |
16 | pingTimer = DispatchSource.makeTimerSource()
17 | pingTimer?.scheduleRepeating(deadline: .now(), interval: .seconds(25))
18 | pingTimer?.setEventHandler { try? ws.ping() }
19 | pingTimer?.resume()
20 |
21 | ws.onText = { ws, text in
22 | let json = try JSON(bytes: text.makeBytes())
23 |
24 | if let u = json.object?["username"]?.string {
25 | username = u
26 | room.connections[u] = ws
27 | room.bot("\(u) has joined. 👋")
28 | }
29 |
30 | if let u = username, let m = json.object?["message"]?.string {
31 | room.send(name: u, message: m)
32 | }
33 | }
34 |
35 | ws.onClose = { ws, _, _, _ in
36 | pingTimer?.cancel()
37 | pingTimer = nil
38 |
39 | guard let u = username else {
40 | return
41 | }
42 |
43 | room.bot("\(u) has left")
44 | room.connections.removeValue(forKey: u)
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Resources/Views/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vapor Chat
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
28 |
29 |
30 |
31 |
Vapor Chat
32 |
Realtime WebSocket chat powered by Vapor 🚀
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Public/styles/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v4.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input,select{overflow:visible}button,select{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{cursor:pointer}[disabled]{cursor:default}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button:-moz-focusring,input:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
2 |
--------------------------------------------------------------------------------
/Package.pins:
--------------------------------------------------------------------------------
1 | {
2 | "autoPin": true,
3 | "pins": [
4 | {
5 | "package": "BCrypt",
6 | "reason": null,
7 | "repositoryURL": "https://github.com/vapor/bcrypt.git",
8 | "version": "1.0.0"
9 | },
10 | {
11 | "package": "Bits",
12 | "reason": null,
13 | "repositoryURL": "https://github.com/vapor/bits.git",
14 | "version": "1.0.0"
15 | },
16 | {
17 | "package": "CTLS",
18 | "reason": null,
19 | "repositoryURL": "https://github.com/vapor/ctls.git",
20 | "version": "1.0.0"
21 | },
22 | {
23 | "package": "Console",
24 | "reason": null,
25 | "repositoryURL": "https://github.com/vapor/console.git",
26 | "version": "2.1.0"
27 | },
28 | {
29 | "package": "Core",
30 | "reason": null,
31 | "repositoryURL": "https://github.com/vapor/core.git",
32 | "version": "2.0.2"
33 | },
34 | {
35 | "package": "Crypto",
36 | "reason": null,
37 | "repositoryURL": "https://github.com/vapor/crypto.git",
38 | "version": "2.0.0"
39 | },
40 | {
41 | "package": "Debugging",
42 | "reason": null,
43 | "repositoryURL": "https://github.com/vapor/debugging.git",
44 | "version": "1.0.0"
45 | },
46 | {
47 | "package": "Engine",
48 | "reason": null,
49 | "repositoryURL": "https://github.com/vapor/engine.git",
50 | "version": "2.1.0"
51 | },
52 | {
53 | "package": "JSON",
54 | "reason": null,
55 | "repositoryURL": "https://github.com/vapor/json.git",
56 | "version": "2.0.2"
57 | },
58 | {
59 | "package": "Multipart",
60 | "reason": null,
61 | "repositoryURL": "https://github.com/vapor/multipart.git",
62 | "version": "2.0.0"
63 | },
64 | {
65 | "package": "Node",
66 | "reason": null,
67 | "repositoryURL": "https://github.com/vapor/node.git",
68 | "version": "2.0.3"
69 | },
70 | {
71 | "package": "Random",
72 | "reason": null,
73 | "repositoryURL": "https://github.com/vapor/random.git",
74 | "version": "1.0.0"
75 | },
76 | {
77 | "package": "Routing",
78 | "reason": null,
79 | "repositoryURL": "https://github.com/vapor/routing.git",
80 | "version": "2.0.0"
81 | },
82 | {
83 | "package": "Sockets",
84 | "reason": null,
85 | "repositoryURL": "https://github.com/vapor/sockets.git",
86 | "version": "2.0.1"
87 | },
88 | {
89 | "package": "TLS",
90 | "reason": null,
91 | "repositoryURL": "https://github.com/vapor/tls.git",
92 | "version": "2.0.4"
93 | },
94 | {
95 | "package": "Vapor",
96 | "reason": null,
97 | "repositoryURL": "https://github.com/vapor/vapor.git",
98 | "version": "2.1.0"
99 | }
100 | ],
101 | "version": 1
102 | }
--------------------------------------------------------------------------------
/Public/scripts/chat.js:
--------------------------------------------------------------------------------
1 | function Chat(host) {
2 | var chat = this;
3 |
4 | chat.ws = new WebSocket('ws://' + host);
5 | chat.ws.onopen = function() {
6 | chat.askUsername();
7 | };
8 |
9 | chat.askUsername = function() {
10 | var name = prompt('What is your GitHub username?');
11 |
12 | $.get('https://api.github.com/users/' + name, function(data) {
13 | chat.join(name);
14 | }).fail(function() {
15 | alert('Invalid username');
16 | chat.askUsername();
17 | });
18 | }
19 |
20 | chat.imageCache = {};
21 |
22 | $('form').on('submit', function(e) {
23 | e.preventDefault();
24 |
25 | var message = $('.message-input').val();
26 |
27 | if (message.length == 0 || message.length >= 256) {
28 | return;
29 | }
30 |
31 | chat.send(message);
32 | $('.message-input').val('');
33 | });
34 |
35 | chat.ws.onmessage = function(event) {
36 | var message = JSON.parse(event.data);
37 | console.log('[' + name + '] ' + message);
38 | chat.bubble(message.message, message.username);
39 | }
40 |
41 | chat.send = function(message) {
42 | chat.ws.send(JSON.stringify({
43 | 'message': message
44 | }));
45 |
46 | chat.bubble(message);
47 | }
48 |
49 | chat.bubble = function(message, username) {
50 | var bubble = $('')
51 | .addClass('message')
52 | .addClass('new');
53 |
54 | if (username) {
55 | var lookup = username;
56 |
57 | if (lookup == 'Bot') {
58 | lookup = 'qutheory';
59 | }
60 | bubble.attr('data-username', lookup);
61 |
62 | var imageUrl = chat.imageCache[lookup];
63 |
64 | if (!imageUrl) {
65 | // async fetch and update the image
66 | $.get('https://api.github.com/users/' + lookup, function(data) {
67 | if (data.avatar_url) {
68 | imageUrl = data.avatar_url;
69 | } else {
70 | imageUrl = 'https://avatars3.githubusercontent.com/u/17364220?v=3&s=200';
71 | }
72 |
73 | $('div.message[data-username=' + lookup + ']')
74 | .find('img')
75 | .attr('src', imageUrl);
76 |
77 | chat.imageCache[lookup] = imageUrl;
78 | });
79 | }
80 |
81 | var image = $('
![]()
')
82 | .addClass('avatar')
83 | .attr('src', imageUrl);
84 |
85 | bubble.append(image);
86 | }
87 |
88 |
89 | var text = $('
')
90 | .addClass('text');
91 |
92 | if (username) {
93 | text.text(username + ': ' + message);
94 | } else {
95 | bubble.addClass('personal');
96 | text.text(message);
97 | }
98 |
99 |
100 | bubble.append(text);
101 |
102 | var d = new Date()
103 | var m = '00'
104 | if (m != d.getMinutes()) {
105 | m = d.getMinutes();
106 | }
107 |
108 | if (m < 10) {
109 | m = '0' + m;
110 | }
111 |
112 | var time = $('' + d.getHours() + ':' + m + ' ');
113 | bubble.append(time);
114 |
115 | $('.messages').append(bubble);
116 |
117 | var objDiv = $('.messages')[0];
118 | objDiv.scrollTop = objDiv.scrollHeight;
119 | }
120 |
121 | chat.join = function(name) {
122 | chat.ws.send(JSON.stringify({
123 | 'username': name
124 | }));
125 | }
126 | };
127 |
--------------------------------------------------------------------------------
/Public/styles/style.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "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",
4 | "sources": ["../scss/style.sass"],
5 | "names": [],
6 | "file": "style.css"
7 | }
--------------------------------------------------------------------------------
/Public/styles/style.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box; }
5 |
6 | html,
7 | body {
8 | height: 100%; }
9 |
10 | body {
11 | background: linear-gradient(135deg, #F7CAC9, #92A8D1);
12 | background-size: cover;
13 | font-family: "Open Sans", sans-serif;
14 | font-size: 12px;
15 | line-height: 1.3;
16 | overflow: hidden; }
17 |
18 | .chat {
19 | position: absolute;
20 | top: 50%;
21 | left: 50%;
22 | transform: translate(-50%, -50%);
23 | max-width: 500px;
24 | width: 100%;
25 | height: 80%;
26 | position: relative;
27 | padding-top: 44px;
28 | padding-bottom: 40px;
29 | z-index: 2;
30 | overflow: hidden;
31 | box-shadow: 0 5px 30px rgba(0, 0, 0, 0.2);
32 | background: rgba(0, 0, 0, 0.5);
33 | border-radius: 20px; }
34 |
35 | .chat-title {
36 | position: absolute;
37 | left: 0;
38 | top: 0;
39 | right: 0;
40 | height: 44px;
41 | z-index: 2;
42 | background: rgba(0, 0, 0, 0.2);
43 | color: #fff;
44 | text-transform: uppercase;
45 | text-align: left;
46 | padding: 10px 10px 10px 50px; }
47 | .chat-title h1, .chat-title h2 {
48 | font-weight: normal;
49 | font-size: 10px;
50 | margin: 0;
51 | padding: 0; }
52 | .chat-title h2 {
53 | color: rgba(255, 255, 255, 0.5);
54 | font-size: 8px;
55 | letter-spacing: 1px; }
56 | .chat-title .avatar {
57 | position: absolute;
58 | z-index: 1;
59 | top: 8px;
60 | left: 9px;
61 | border-radius: 30px;
62 | width: 30px;
63 | height: 30px;
64 | overflow: hidden;
65 | margin: 0;
66 | padding: 0;
67 | border: 2px solid rgba(255, 255, 255, 0.24); }
68 | .chat-title .avatar img {
69 | width: 100%;
70 | height: auto; }
71 |
72 | .messages {
73 | padding: 10px;
74 | color: rgba(255, 255, 255, 0.5);
75 | width: 100%;
76 | height: 100%;
77 | overflow: hidden;
78 | overflow-y: scroll;
79 | -webkit-overflow-scrolling: touch; }
80 | .messages .message {
81 | color: #fff;
82 | clear: both;
83 | float: left;
84 | padding: 6px 10px 7px;
85 | border-radius: 10px 10px 10px 0;
86 | background: rgba(0, 0, 0, 0.3);
87 | margin: 8px 0;
88 | font-size: 11px;
89 | line-height: 1.4;
90 | margin-left: 35px;
91 | position: relative;
92 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
93 | word-break: break-all; }
94 | .messages .message .timestamp {
95 | position: absolute;
96 | bottom: -15px;
97 | left: 10px;
98 | font-size: 9px;
99 | color: rgba(255, 255, 255, 0.3); }
100 | .messages .message::before {
101 | content: "";
102 | position: absolute;
103 | bottom: -6px;
104 | border-top: 6px solid rgba(0, 0, 0, 0.3);
105 | left: 0;
106 | border-right: 7px solid transparent; }
107 | .messages .message .avatar {
108 | position: absolute;
109 | z-index: 1;
110 | bottom: -12px;
111 | left: -35px;
112 | border-radius: 30px;
113 | width: 30px;
114 | height: 30px;
115 | overflow: hidden;
116 | margin: 0;
117 | padding: 0;
118 | border: 2px solid rgba(255, 255, 255, 0.24); }
119 | .messages .message .avatar img {
120 | width: 100%;
121 | height: auto; }
122 | .messages .message.personal {
123 | float: right;
124 | color: #fff;
125 | text-align: right;
126 | background: rgba(0, 0, 0, 0.3);
127 | border-radius: 10px 10px 0 10px; }
128 | .messages .message.personal .timestamp {
129 | right: 5px;
130 | left: auto; }
131 | .messages .message.personal::before {
132 | left: auto;
133 | right: 0;
134 | border-right: none;
135 | border-left: 5px solid transparent;
136 | border-top: 4px solid rgba(0, 0, 0, 0.3);
137 | bottom: -4px; }
138 | .messages .message.new {
139 | transform: scale(0);
140 | transform-origin: 0 0;
141 | animation: bounce 500ms linear both; }
142 | .messages .message.loading::before {
143 | position: absolute;
144 | top: 50%;
145 | left: 50%;
146 | transform: translate(-50%, -50%);
147 | content: "";
148 | display: block;
149 | width: 3px;
150 | height: 3px;
151 | border-radius: 50%;
152 | background: rgba(255, 255, 255, 0.5);
153 | z-index: 2;
154 | margin-top: 4px;
155 | animation: ball 0.45s cubic-bezier(0, 0, 0.15, 1) alternate infinite;
156 | border: none;
157 | animation-delay: 0.15s; }
158 | .messages .message.loading span {
159 | display: block;
160 | font-size: 0;
161 | width: 20px;
162 | height: 10px;
163 | position: relative; }
164 | .messages .message.loading span::before {
165 | position: absolute;
166 | top: 50%;
167 | left: 50%;
168 | transform: translate(-50%, -50%);
169 | content: "";
170 | display: block;
171 | width: 3px;
172 | height: 3px;
173 | border-radius: 50%;
174 | background: rgba(255, 255, 255, 0.5);
175 | z-index: 2;
176 | margin-top: 4px;
177 | animation: ball 0.45s cubic-bezier(0, 0, 0.15, 1) alternate infinite;
178 | margin-left: -7px; }
179 | .messages .message.loading span::after {
180 | position: absolute;
181 | top: 50%;
182 | left: 50%;
183 | transform: translate(-50%, -50%);
184 | content: "";
185 | display: block;
186 | width: 3px;
187 | height: 3px;
188 | border-radius: 50%;
189 | background: rgba(255, 255, 255, 0.5);
190 | z-index: 2;
191 | margin-top: 4px;
192 | animation: ball 0.45s cubic-bezier(0, 0, 0.15, 1) alternate infinite;
193 | margin-left: 7px;
194 | animation-delay: 0.3s; }
195 |
196 | .message-box {
197 | bottom: 0;
198 | left: 0;
199 | right: 0;
200 | height: 40px;
201 | width: 100%;
202 | background: rgba(0, 0, 0, 0.3);
203 | position: absolute; }
204 | .message-box form {
205 | width: 100%;
206 | height: 100%; }
207 | .message-box .message-input {
208 | padding-left: 15px;
209 | padding-right: 65px;
210 | background: none;
211 | border: none;
212 | outline: none !important;
213 | color: rgba(255, 255, 255, 0.7);
214 | font-size: 11px;
215 | margin: 0;
216 | width: 100%;
217 | height: 100%; }
218 | .message-box textarea:focus:-webkit-placeholder {
219 | color: transparent; }
220 | .message-box .message-submit {
221 | position: absolute;
222 | z-index: 1;
223 | top: 9px;
224 | right: 10px;
225 | color: #fff;
226 | border: none;
227 | background: #92A8D1;
228 | font-size: 10px;
229 | text-transform: uppercase;
230 | line-height: 1;
231 | padding: 6px 10px;
232 | border-radius: 10px;
233 | outline: none !important;
234 | transition: background 0.2s ease; }
235 | .message-box .message-submit:hover {
236 | background: #F7CAC9; }
237 |
238 | @keyframes bounce {
239 | 0% {
240 | transform: matrix3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
241 | 4.7% {
242 | transform: matrix3d(0.45, 0, 0, 0, 0, 0.45, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
243 | 9.41% {
244 | transform: matrix3d(0.883, 0, 0, 0, 0, 0.883, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
245 | 14.11% {
246 | transform: matrix3d(1.141, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
247 | 18.72% {
248 | transform: matrix3d(1.212, 0, 0, 0, 0, 1.212, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
249 | 24.32% {
250 | transform: matrix3d(1.151, 0, 0, 0, 0, 1.151, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
251 | 29.93% {
252 | transform: matrix3d(1.048, 0, 0, 0, 0, 1.048, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
253 | 35.54% {
254 | transform: matrix3d(0.979, 0, 0, 0, 0, 0.979, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
255 | 41.04% {
256 | transform: matrix3d(0.961, 0, 0, 0, 0, 0.961, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
257 | 52.15% {
258 | transform: matrix3d(0.991, 0, 0, 0, 0, 0.991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
259 | 63.26% {
260 | transform: matrix3d(1.007, 0, 0, 0, 0, 1.007, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
261 | 85.49% {
262 | transform: matrix3d(0.999, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
263 | 100% {
264 | transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } }
265 | @keyframes ball {
266 | from {
267 | transform: translateY(0) scaleY(0.8); }
268 | to {
269 | transform: translateY(-10px); } }
270 |
271 | /*# sourceMappingURL=style.css.map */
272 |
--------------------------------------------------------------------------------
/Public/scss/style.sass:
--------------------------------------------------------------------------------
1 | // Mixins
2 | @mixin center
3 | position: absolute
4 | top: 50%
5 | left: 50%
6 | transform: translate(-50%, -50%)
7 |
8 |
9 | @mixin ball
10 | @include center
11 | content: ''
12 | display: block
13 | width: 3px
14 | height: 3px
15 | border-radius: 50%
16 | background: rgba(255, 255, 255, .5)
17 | z-index: 2
18 | margin-top: 4px
19 | animation: ball .45s cubic-bezier(0, 0, 0.15, 1) alternate infinite
20 |
21 | // Body
22 | *,
23 | *::before,
24 | *::after
25 | box-sizing: border-box
26 |
27 |
28 | html,
29 | body
30 | height: 100%
31 |
32 |
33 | body
34 | background: linear-gradient(135deg, #F7CAC9, #92A8D1)
35 | background-size: cover
36 | font-family: 'Open Sans', sans-serif
37 | font-size: 12px
38 | line-height: 1.3
39 | overflow: hidden
40 |
41 | // Chat
42 | .chat
43 | @include center
44 |
45 | max-width: 500px
46 | width: 100%
47 | height: 80%
48 |
49 | position: relative
50 |
51 | padding-top: 44px
52 | padding-bottom: 40px
53 |
54 | z-index: 2
55 | overflow: hidden
56 | box-shadow: 0 5px 30px rgba(0, 0, 0, .2)
57 | background: rgba(0, 0, 0, .5)
58 | border-radius: 20px
59 |
60 | // Chat Title
61 | .chat-title
62 | position: absolute
63 | left: 0
64 | top: 0
65 | right: 0
66 |
67 | height: 44px
68 |
69 | z-index: 2
70 | background: rgba(0, 0, 0, 0.2)
71 | color: #fff
72 | text-transform: uppercase
73 | text-align: left
74 | padding: 10px 10px 10px 50px
75 |
76 | h1, h2
77 | font-weight: normal
78 | font-size: 10px
79 | margin: 0
80 | padding: 0
81 |
82 |
83 | h2
84 | color: rgba(255, 255, 255, .5)
85 | font-size: 8px
86 | letter-spacing: 1px
87 |
88 |
89 | .avatar
90 | position: absolute
91 | z-index: 1
92 | top: 8px
93 | left: 9px
94 | border-radius: 30px
95 | width: 30px
96 | height: 30px
97 | overflow: hidden
98 | margin: 0
99 | padding: 0
100 | border: 2px solid rgba(255, 255, 255, 0.24)
101 |
102 | img
103 | width: 100%
104 | height: auto
105 |
106 |
107 |
108 |
109 |
110 |
111 | // Messages
112 | .messages
113 | padding: 10px
114 | color: rgba(255, 255, 255, .5)
115 |
116 | width: 100%
117 | height: 100%
118 |
119 | overflow: hidden
120 | overflow-y: scroll
121 | -webkit-overflow-scrolling: touch
122 |
123 | .message
124 | color: #fff
125 | clear: both
126 | float: left
127 | padding: 6px 10px 7px
128 | border-radius: 10px 10px 10px 0
129 | background: rgba(0, 0, 0, .3)
130 | margin: 8px 0
131 | font-size: 11px
132 | line-height: 1.4
133 | margin-left: 35px
134 | position: relative
135 | text-shadow: 0 1px 1px rgba(0, 0, 0, .2)
136 | word-break: break-all
137 |
138 | .timestamp
139 | position: absolute
140 | bottom: -15px
141 | left: 10px
142 | font-size: 9px
143 | color: rgba(255, 255, 255, .3)
144 |
145 |
146 | &::before
147 | content: ''
148 | position: absolute
149 | bottom: -6px
150 | border-top: 6px solid rgba(0, 0, 0, .3)
151 | left: 0
152 | border-right: 7px solid transparent
153 |
154 |
155 | .avatar
156 | position: absolute
157 | z-index: 1
158 | bottom: -12px
159 | left: -35px
160 | border-radius: 30px
161 | width: 30px
162 | height: 30px
163 | overflow: hidden
164 | margin: 0
165 | padding: 0
166 | border: 2px solid rgba(255, 255, 255, 0.24)
167 |
168 | img
169 | width: 100%
170 | height: auto
171 |
172 |
173 |
174 | &.personal
175 | float: right
176 | color: #fff
177 | text-align: right
178 | background: rgba(0, 0, 0, .3)
179 | //background: linear-gradient(120deg, #248A52, #257287)
180 | border-radius: 10px 10px 0 10px
181 |
182 | .timestamp
183 | right: 5px
184 | left: auto
185 |
186 | &::before
187 | left: auto
188 | right: 0
189 | border-right: none
190 | border-left: 5px solid transparent
191 | border-top: 4px solid rgba(0, 0, 0, .3)
192 | bottom: -4px
193 |
194 |
195 |
196 | &.new
197 | transform: scale(0)
198 | transform-origin: 0 0
199 | animation: bounce 500ms linear both
200 |
201 |
202 | &.loading
203 |
204 | &::before
205 | @include ball
206 | border: none
207 | animation-delay: .15s
208 |
209 |
210 | & span
211 | display: block
212 | font-size: 0
213 | width: 20px
214 | height: 10px
215 | position: relative
216 |
217 | &::before
218 | @include ball
219 | margin-left: -7px
220 |
221 |
222 | &::after
223 | @include ball
224 | margin-left: 7px
225 | animation-delay: .3s
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | // Message Box
236 | .message-box
237 | bottom: 0
238 | left: 0
239 | right: 0
240 |
241 | height: 40px
242 | width: 100%
243 | background: rgba(0, 0, 0, 0.3)
244 |
245 | position: absolute
246 |
247 | form
248 | width: 100%
249 | height: 100%
250 |
251 | & .message-input
252 | padding-left: 15px
253 | padding-right: 65px
254 |
255 | background: none
256 | border: none
257 | outline: none!important
258 | color: rgba(255, 255, 255, .7)
259 | font-size: 11px
260 | margin: 0
261 |
262 | width: 100%
263 | height: 100%
264 |
265 |
266 | textarea:focus:-webkit-placeholder
267 | color: transparent
268 |
269 |
270 | & .message-submit
271 | position: absolute
272 | z-index: 1
273 | top: 9px
274 | right: 10px
275 | color: #fff
276 | border: none
277 | background: #92A8D1
278 | font-size: 10px
279 | text-transform: uppercase
280 | line-height: 1
281 | padding: 6px 10px
282 | border-radius: 10px
283 | outline: none!important
284 | transition: background .2s ease
285 |
286 | &:hover
287 | background: #F7CAC9
288 |
289 |
290 |
291 | // Bounce
292 | @keyframes bounce
293 | 0%
294 | transform: matrix3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
295 | 4.7%
296 | transform: matrix3d(0.45, 0, 0, 0, 0, 0.45, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
297 | 9.41%
298 | transform: matrix3d(0.883, 0, 0, 0, 0, 0.883, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
299 | 14.11%
300 | transform: matrix3d(1.141, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
301 | 18.72%
302 | transform: matrix3d(1.212, 0, 0, 0, 0, 1.212, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
303 | 24.32%
304 | transform: matrix3d(1.151, 0, 0, 0, 0, 1.151, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
305 | 29.93%
306 | transform: matrix3d(1.048, 0, 0, 0, 0, 1.048, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
307 | 35.54%
308 | transform: matrix3d(0.979, 0, 0, 0, 0, 0.979, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
309 | 41.04%
310 | transform: matrix3d(0.961, 0, 0, 0, 0, 0.961, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
311 | 52.15%
312 | transform: matrix3d(0.991, 0, 0, 0, 0, 0.991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
313 | 63.26%
314 | transform: matrix3d(1.007, 0, 0, 0, 0, 1.007, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
315 | 85.49%
316 | transform: matrix3d(0.999, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
317 | 100%
318 | transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
319 |
320 |
321 |
322 | @keyframes ball
323 | from
324 | transform: translateY(0) scaleY(.8)
325 |
326 | to
327 | transform: translateY(-10px)
328 |
329 |
330 |
--------------------------------------------------------------------------------