├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── .finished-upgraders ├── packages └── versions ├── .gitignore ├── public ├── tada.png └── question-white.svg ├── server └── main.js ├── api ├── collections.js └── server │ ├── publications.js │ └── methods.js ├── package.json ├── client ├── message.html ├── message.js ├── loader.html ├── main.html ├── main.js └── main.css ├── LICENSE └── README.md /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .deploy/ -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.6.1.1 2 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /public/tada.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/meteor-live-chat/master/public/tada.png -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | Meteor.startup(() => { 4 | // code to run on server at startup 5 | }); 6 | -------------------------------------------------------------------------------- /api/collections.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo'; 2 | 3 | //declare the Mongo collections to use 4 | 5 | export const Messages = new Mongo.Collection('messages'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live-chat", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run" 6 | }, 7 | "dependencies": { 8 | "@babel/runtime": "^7.0.0-beta.42", 9 | "meteor-node-stubs": "^0.3.2", 10 | "moment": "^2.22.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/server/publications.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Messages } from '../collections.js'; 3 | 4 | Meteor.publish("messages", function() { 5 | return Messages.find({}, { fields: { name: 1, message: 1, createdAt: 1, announcement: 1 }, limit: 100, sort: { createdAt: -1 } }); //we want the 100 most recent messages 6 | }); -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | o2iybehd9kb4.fhcarzk2x079 8 | -------------------------------------------------------------------------------- /client/message.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/message.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating'; 2 | import moment from 'moment'; 3 | import './message.html'; 4 | 5 | Template.message.helpers({ 6 | 7 | timestamp() { 8 | const sentTime = moment(this.createdAt); 9 | //if today, just show time, else if some other day, show date and time 10 | if (sentTime.isSame(new Date(), "day")) { 11 | return sentTime.format("h:mm a"); 12 | } 13 | return sentTime.format("M/D/YY h:mm a"); 14 | } 15 | 16 | }); -------------------------------------------------------------------------------- /client/loader.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/question-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.3.0 # Packages every Meteor app needs to have 8 | mobile-experience@1.0.5 # Packages for a great mobile UX 9 | mongo@1.4.2 # The database Meteor supports right now 10 | blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views 11 | reactive-var@1.0.11 # Reactive variable for tracker 12 | tracker@1.1.3 # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css@1.4.0 # CSS minifier run for production mode 15 | standard-minifier-js@2.3.1 # JS minifier run for production mode 16 | es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers 17 | ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code 18 | shell-server@0.3.1 # Server-side component of the `meteor shell` command 19 | 20 | mrt:cookies 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pitchly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/server/methods.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { check, Match } from 'meteor/check'; 3 | 4 | import { Messages } from '../collections.js'; 5 | 6 | Meteor.methods({ 7 | 8 | 'sendMessage'(data) { 9 | 10 | check(data, { 11 | message: String, //the message to send 12 | name: Match.Optional(String) //if the user already has a name 13 | }); 14 | 15 | if (data.message=="") { 16 | throw new Meteor.Error("message-empty", "Your message is empty"); 17 | } 18 | 19 | let userName = (data.name && data.name!="") ? data.name : "Anonymous"; 20 | 21 | const matchName = data.message.match(/^My name is (.*)/i); 22 | 23 | if (matchName && matchName[1]!="") { 24 | userName = matchName[1]; 25 | Messages.insert({ 26 | name: "Chat Bot", 27 | message: "Hey everyone, " + userName + " is here!", 28 | createdAt: new Date(), 29 | announcement: true 30 | }); 31 | } else { 32 | Messages.insert({ 33 | name: userName, 34 | message: data.message, 35 | createdAt: new Date() 36 | }); 37 | } 38 | 39 | return { 40 | name: userName 41 | }; 42 | 43 | } 44 | 45 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Live Chat with Meteor! 2 | A simple live chat app written in Meteor 3 | 4 | See the [live demo](http://chat.pitchly.net/). Just be nice! 5 | 6 | ## How to Run 7 | Clone this repo. If you don't already have Meteor, see https://www.meteor.com/install or just run: 8 | 9 | ``` 10 | curl https://install.meteor.com/ | sh 11 | ``` 12 | 13 | Then in your project directory, run: 14 | 15 | ``` 16 | meteor npm install 17 | meteor 18 | ``` 19 | 20 | Open up http://localhost:3000/ and see the result. Trying opening in two different windows! 21 | 22 | --- 23 | 24 | This app was largely made in just two hours, styling aside, and shows the power of Meteor and its real-time capabilities. This sample app covers a few key principles: 25 | 26 | - How to use Blaze Templates 27 | - How to publish/subscribe to data from the server 28 | - How to call methods on the server 29 | - How to insert data into MongoDB 30 | - How to deal with data reactively 31 | - How to use Meteor helpers 32 | - How to use Meteor events 33 | - How to import Meteor and NPM third-party packages 34 | - Basic backend validation using Meteor check 35 | - Restricting data access with Meteor.publish 36 | 37 | Extra: In this app, we also deal with 38 | 39 | - Dates using the moment.js NPM package 40 | - Cookies using the Meteor mrt:cookies package 41 | 42 | What this app does not cover: 43 | 44 | - Saving local state (aside from cookies) 45 | - Login or auth flows 46 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to Live Chat with Meteor! 5 | 6 | 7 | 8 |
9 |
10 |
11 |

Hint

12 |
You can send messages anonymously or tell us your name. To set your name, just send a message saying "My name is _____" and we'll introduce you!
13 |
14 |
15 | 16 |
17 |
18 |
19 |

Welcome to Meteor Live Chat!

20 |
See this project on GitHub
21 |
22 |
23 | {{#if Template.subscriptionsReady}} 24 | {{#each messages}} 25 | {{> message}} 26 | {{else}} 27 |
Be the first to write a message!
28 | {{/each}} 29 | {{else}} 30 | {{> loader}} 31 | {{/if}} 32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | autoupdate@1.4.0 3 | babel-compiler@7.0.7 4 | babel-runtime@1.2.2 5 | base64@1.0.11 6 | binary-heap@1.0.10 7 | blaze@2.3.2 8 | blaze-html-templates@1.1.2 9 | blaze-tools@1.0.10 10 | boilerplate-generator@1.4.0 11 | caching-compiler@1.1.11 12 | caching-html-compiler@1.1.2 13 | callback-hook@1.1.0 14 | check@1.3.1 15 | ddp@1.4.0 16 | ddp-client@2.3.2 17 | ddp-common@1.4.0 18 | ddp-server@2.1.2 19 | deps@1.0.12 20 | diff-sequence@1.1.0 21 | dynamic-import@0.3.0 22 | ecmascript@0.10.7 23 | ecmascript-runtime@0.5.0 24 | ecmascript-runtime-client@0.6.2 25 | ecmascript-runtime-server@0.5.0 26 | ejson@1.1.0 27 | es5-shim@4.7.3 28 | geojson-utils@1.0.10 29 | hot-code-push@1.0.4 30 | html-tools@1.0.11 31 | htmljs@1.0.11 32 | http@1.4.0 33 | id-map@1.1.0 34 | jquery@1.11.11 35 | launch-screen@1.1.1 36 | livedata@1.0.18 37 | logging@1.1.20 38 | meteor@1.8.6 39 | meteor-base@1.3.0 40 | minifier-css@1.3.1 41 | minifier-js@2.3.4 42 | minimongo@1.4.4 43 | mobile-experience@1.0.5 44 | mobile-status-bar@1.0.14 45 | modules@0.11.6 46 | modules-runtime@0.9.2 47 | mongo@1.4.7 48 | mongo-dev-server@1.1.0 49 | mongo-id@1.0.7 50 | mrt:cookies@0.3.0 51 | npm-mongo@2.2.34 52 | observe-sequence@1.0.16 53 | ordered-dict@1.1.0 54 | promise@0.10.2 55 | random@1.1.0 56 | reactive-var@1.0.11 57 | reload@1.2.0 58 | retry@1.1.0 59 | routepolicy@1.0.13 60 | server-render@0.3.0 61 | shell-server@0.3.1 62 | shim-common@0.1.0 63 | socket-stream-client@0.1.0 64 | spacebars@1.0.15 65 | spacebars-compiler@1.1.3 66 | standard-minifier-css@1.4.1 67 | standard-minifier-js@2.3.3 68 | templating@1.3.2 69 | templating-compiler@1.3.3 70 | templating-runtime@1.3.2 71 | templating-tools@1.1.2 72 | tracker@1.1.3 73 | ui@1.0.13 74 | underscore@1.0.10 75 | url@1.2.0 76 | webapp@1.5.0 77 | webapp-hashing@1.0.9 78 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating'; 2 | import { Cookies } from 'meteor/mrt:cookies'; 3 | import { Messages } from '../api/collections.js'; 4 | import './main.html'; 5 | import './message.js'; 6 | import './loader.html'; 7 | 8 | Template.body.onCreated(function bodyOnCreated() { 9 | 10 | this.messagesSub = this.subscribe("messages"); //get messages 11 | 12 | }); 13 | 14 | Template.body.onRendered(function bodyOnRendered() { 15 | 16 | const $messagesScroll = this.$('.messages-scroll'); 17 | 18 | //this is used to auto-scroll to new messages whenever they come in 19 | 20 | let initialized = false; 21 | 22 | this.autorun(() => { 23 | if (this.messagesSub.ready()) { 24 | Messages.find({}, { fields: { _id: 1 } }).fetch(); 25 | Tracker.afterFlush(() => { 26 | //only auto-scroll if near the bottom already 27 | if (!initialized || Math.abs($messagesScroll[0].scrollHeight - $messagesScroll.scrollTop() - $messagesScroll.outerHeight()) < 200) { 28 | initialized = true; 29 | $messagesScroll.stop().animate({ 30 | scrollTop: $messagesScroll[0].scrollHeight 31 | }); 32 | } 33 | }); 34 | } 35 | }); 36 | 37 | }); 38 | 39 | Template.body.helpers({ 40 | 41 | messages() { 42 | return Messages.find({}, { sort: { createdAt: 1 } }); //most recent at the bottom 43 | }, 44 | 45 | hideHint() { 46 | return (Cookie.get("hideHint")=="true"); //convert from string to boolean 47 | } 48 | 49 | }); 50 | 51 | Template.body.events({ 52 | 53 | //send message 54 | 55 | 'submit form'(event, instance) { 56 | 57 | event.preventDefault(); 58 | 59 | const $el = $(event.currentTarget); 60 | const $input = $el.find('.message-input'); 61 | 62 | const data = { message: $input.val() }; 63 | const userName = Cookie.get("name"); 64 | 65 | if (userName) { 66 | data.name = userName; 67 | } 68 | 69 | Meteor.call("sendMessage", data, (error, response) => { 70 | if (error) { 71 | alert(error.reason); 72 | } else { 73 | Cookie.set("name", response.name); 74 | $input.val(""); 75 | } 76 | }); 77 | 78 | }, 79 | 80 | //hide hint in the top right corner 81 | 82 | 'click .hide-hint-button'(event, instance) { 83 | 84 | //cookies only understand strings 85 | Cookie.set("hideHint", (Cookie.get("hideHint")=="true") ? "false" : "true"); 86 | 87 | } 88 | 89 | }); -------------------------------------------------------------------------------- /client/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | body { 11 | margin: 0px; 12 | padding: 0px; 13 | height: 100%; 14 | font-family: 'Arial', sans-serif; 15 | font-size: 14px; 16 | color: #444444; 17 | text-align: left; 18 | line-height: 1.5; 19 | } 20 | 21 | input { 22 | display: inline-block; 23 | width: 400px; 24 | margin: 0px; 25 | padding: 10px; 26 | background-color: rgba(255, 255, 255, 0.8); 27 | border: 1px solid #C2C8D9; 28 | color: #444444; 29 | border-radius: 3px; 30 | font-size: 14px; 31 | font-family: 'Arial', sans-serif; 32 | outline: none; 33 | transition: all 500ms cubic-bezier(0.165, 0.840, 0.440, 1.000); /* easeOutQuart */ 34 | } 35 | 36 | input:hover { 37 | background-color: white; 38 | border-color: #999fb1; 39 | } 40 | 41 | input:focus { 42 | background-color: white; 43 | border-color: #2a2e3c; 44 | } 45 | 46 | button { 47 | display: inline-block; 48 | margin: 0px; 49 | padding: 10px 20px; 50 | background-color: #6D6DD0; 51 | border: 1px solid #6D6DD0; 52 | color: white; 53 | border-radius: 4px; 54 | font-size: 12px; 55 | font-family: 'Arial', sans-serif; 56 | font-weight: bold; 57 | outline: none; 58 | cursor: pointer; 59 | cursor: hand; 60 | transition: all 500ms cubic-bezier(0.165, 0.840, 0.440, 1.000); /* easeOutQuart */ 61 | } 62 | 63 | button:hover, button:focus { 64 | background-color: #5757B2; 65 | border-color: #5757B2; 66 | color: white; 67 | box-shadow: 0px 10px 30px 0px rgba(87, 87, 179, 0.3); 68 | } 69 | 70 | /* selections made by the user dragging along text */ 71 | 72 | ::selection { 73 | background: #ffb7b7; /* WebKit/Blink Browsers */ 74 | } 75 | ::-moz-selection { 76 | background: #ffb7b7; /* Gecko Browsers */ 77 | } 78 | 79 | /* input placeholder color */ 80 | 81 | ::-webkit-input-placeholder { /* WebKit, Blink, Edge */ 82 | color: #999fb1; 83 | } 84 | :-moz-placeholder { /* Mozilla Firefox 4 to 18 */ 85 | color: #999fb1; 86 | opacity: 1; 87 | } 88 | ::-moz-placeholder { /* Mozilla Firefox 19+ */ 89 | color: #999fb1; 90 | opacity: 1; 91 | } 92 | :-ms-input-placeholder { /* Internet Explorer 10-11 */ 93 | color: #999fb1; 94 | } 95 | ::-ms-input-placeholder { /* Microsoft Edge */ 96 | color: #999fb1; 97 | } 98 | 99 | /* loading spinner animation that can be applied to SVG spinner (uses GPU which makes animation as smooth as possible) */ 100 | 101 | @-ms-keyframes spin { 102 | from { -ms-transform: rotate(0deg); } 103 | to { -ms-transform: rotate(360deg); } 104 | } 105 | @-moz-keyframes spin { 106 | from { -moz-transform: rotate(0deg); } 107 | to { -moz-transform: rotate(360deg); } 108 | } 109 | @-webkit-keyframes spin { 110 | from { -webkit-transform: rotate(0deg); } 111 | to { -webkit-transform: rotate(360deg); } 112 | } 113 | @keyframes spin { 114 | from { transform:rotate(0deg); } 115 | to { transform:rotate(360deg); } 116 | } 117 | 118 | .loader { 119 | -webkit-animation-name: spin; 120 | -webkit-animation-duration: 400ms; 121 | -webkit-animation-iteration-count: infinite; 122 | -webkit-animation-timing-function: linear; 123 | -moz-animation-name: spin; 124 | -moz-animation-duration: 400ms; 125 | -moz-animation-iteration-count: infinite; 126 | -moz-animation-timing-function: linear; 127 | -ms-animation-name: spin; 128 | -ms-animation-duration: 400ms; 129 | -ms-animation-iteration-count: infinite; 130 | -ms-animation-timing-function: linear; 131 | animation-name: spin; 132 | animation-duration: 400ms; 133 | animation-iteration-count: infinite; 134 | animation-timing-function: linear; 135 | } 136 | 137 | .hint-box-container { 138 | position: fixed; 139 | top: 50px; 140 | right: 50px; 141 | z-index: 5; 142 | opacity: 0.7; 143 | transition: all 500ms cubic-bezier(0.165, 0.840, 0.440, 1.000); /* easeOutQuart */ 144 | } 145 | 146 | .hint-box-container:hover { 147 | opacity: 1; 148 | } 149 | 150 | .hint-box-container .hide-hint-button { 151 | position: absolute; 152 | top: -10px; 153 | right: -10px; 154 | width: 30px; 155 | height: 30px; 156 | z-index: 1; 157 | background-color: #444444; 158 | border-radius: 50%; 159 | border: 5px solid white; 160 | transition: all 500ms cubic-bezier(0.165, 0.840, 0.440, 1.000); /* easeOutQuart */ 161 | cursor: pointer; 162 | cursor: hand; 163 | } 164 | 165 | .hint-box-container .hide-hint-button:before, .hint-box-container .hide-hint-button:after { 166 | position: absolute; 167 | left: 50%; 168 | top: 50%; 169 | margin-left: -1px; 170 | margin-top: -6px; 171 | content: ' '; 172 | height: 12px; 173 | width: 2px; 174 | background-color: white; 175 | } 176 | .hint-box-container .hide-hint-button:before { 177 | transform: rotate(45deg); 178 | } 179 | .hint-box-container .hide-hint-button:after { 180 | transform: rotate(-45deg); 181 | } 182 | 183 | .hint-box-container .hint-box { 184 | padding: 20px; 185 | background-color: white; 186 | max-width: 320px; 187 | box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.15); 188 | transition: all 500ms cubic-bezier(0.165, 0.840, 0.440, 1.000); /* easeOutQuart */ 189 | } 190 | 191 | @media (max-width: 420px) { 192 | 193 | /* in case the hint box gets squeezed out */ 194 | 195 | .hint-box-container { 196 | left: 50px; 197 | } 198 | 199 | } 200 | 201 | .hint-box-container .hint-box h2 { 202 | margin: 0px 0px 5px 0px; 203 | padding: 0px; 204 | font-size: 21px; 205 | } 206 | 207 | .hint-box-container.hidden .hint-box { 208 | display: none; 209 | } 210 | 211 | .hint-box-container.hidden .hide-hint-button { 212 | width: 40px; 213 | height: 40px; 214 | box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.4); 215 | } 216 | 217 | .hint-box-container.hidden .hide-hint-button:after { 218 | display: none; 219 | } 220 | 221 | .hint-box-container.hidden .hide-hint-button:before { 222 | position: absolute; 223 | left: 50%; 224 | top: 50%; 225 | content: ''; 226 | height: 14px; 227 | width: 14px; 228 | margin-top: -7px; 229 | margin-left: -7px; 230 | transform: rotate(0deg); 231 | background-color: transparent; 232 | background-image: url('/question-white.svg'); 233 | background-position: center; 234 | background-repeat: no-repeat; 235 | background-size: contain; 236 | } 237 | 238 | .messages-box { 239 | position: fixed; 240 | top: 0px; 241 | left: 0px; 242 | right: 0px; 243 | bottom: 80px; 244 | overflow: none; 245 | } 246 | 247 | .messages-box .messages-scroll { 248 | position: absolute; 249 | bottom: 0px; 250 | left: 0px; 251 | right: 0px; 252 | overflow: auto; 253 | max-height: 100%; 254 | -webkit-overflow-scrolling: touch; /* enable intertial scroll on mobile devices */ 255 | } 256 | 257 | .messages-box .messages-scroll .header { 258 | margin: 20px; 259 | } 260 | 261 | .messages-box .messages-scroll .header h1 { 262 | font-size: 25px; 263 | margin: 0px 0px 10px 0px; 264 | padding: 0px; 265 | } 266 | 267 | .messages-box .messages-scroll .messages { 268 | padding: 20px; 269 | } 270 | 271 | .messages-box .messages-scroll .messages .message { 272 | margin-top: 15px; 273 | border-top: 1px solid #EDEEF2; 274 | padding-top: 15px; 275 | } 276 | 277 | .messages-box .messages-scroll .messages .message .name { 278 | font-weight: bold; 279 | margin-bottom: 2px; 280 | } 281 | 282 | .messages-box .messages-scroll .messages .message.announcement .name { 283 | color: #ff5722; 284 | } 285 | 286 | .messages-box .messages-scroll .messages .message .timestamp { 287 | color: #95979C; 288 | font-weight: normal; 289 | margin-left: 5px; 290 | font-size: 12px; 291 | } 292 | 293 | .messages-box .messages-scroll .messages .message.announcement .emoji { 294 | height: 25px; 295 | margin-left: 5px; 296 | vertical-align: bottom; 297 | } 298 | 299 | .new-message-container { 300 | position: fixed; 301 | left: 0px; 302 | right: 0px; 303 | bottom: 0px; 304 | height: 80px; 305 | border-top: 1px solid #EDEEF2; 306 | background-color: #FAFBFF; 307 | padding: 20px; 308 | } 309 | 310 | .new-message-container form { 311 | display: flex; 312 | flex-direction: row; 313 | } 314 | 315 | .new-message-container form input { 316 | flex: 1; 317 | } 318 | 319 | .new-message-container form button { 320 | margin-left: 10px; 321 | } --------------------------------------------------------------------------------