├── 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 |
41 | 42 | 43 |
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 | --------------------------------------------------------------------------------