├── images ├── README.md └── app_preview_image.png ├── .gitignore ├── blobchat.png ├── public ├── icon.png ├── 1_index_screenshot.png ├── 2_posts_screenshot.png ├── 3_chat_screenshot.png ├── blobchat_thumbnail.jpeg ├── sample_posts.json ├── icon.svg └── style.css ├── docker-compose.yml ├── package.json ├── LICENSE ├── marketplace.json ├── views ├── index.ejs ├── posts.ejs └── chat.ejs ├── README.md └── server.js /images/README.md: -------------------------------------------------------------------------------- 1 | # Images 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /blobchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/blobchat.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/icon.png -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/images/app_preview_image.png -------------------------------------------------------------------------------- /public/1_index_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/1_index_screenshot.png -------------------------------------------------------------------------------- /public/2_posts_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/2_posts_screenshot.png -------------------------------------------------------------------------------- /public/3_chat_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/3_chat_screenshot.png -------------------------------------------------------------------------------- /public/blobchat_thumbnail.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/blobchat_thumbnail.jpeg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis: 4 | image: redislabs/redisearch:latest 5 | ports: 6 | - 6379:6379 7 | 8 | redis-commander: 9 | hostname: redis-commander 10 | image: rediscommander/redis-commander:latest 11 | restart: always 12 | environment: 13 | - REDIS_HOSTS=local:redis:6379 14 | ports: 15 | - "8081:8081" 16 | -------------------------------------------------------------------------------- /public/sample_posts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "CookieMonster", 4 | "content": "A brown fox jumps over the fence." 5 | }, 6 | { 7 | "username": "SpongeBob", 8 | "content": "A green fox said wow." 9 | }, 10 | { 11 | "username": "Ninja123", 12 | "content": "It's a very nice day out there. Want to take a walk?" 13 | }, 14 | { 15 | "username": "PlaidShirt", 16 | "content": "The light from the stars finally shines through the clouds in the night sky. How pretty." 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlobChat", 3 | "version": "1.0.0", 4 | "description": "A minimalist and elegant chat app.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/pumpkiny9120/BlobChat.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/pumpkiny9120/BlobChat/issues" 18 | }, 19 | "homepage": "https://github.com/pumpkiny9120/BlobChat#readme", 20 | "dependencies": { 21 | "ejs": "^3.1.6", 22 | "express": "^4.17.1", 23 | "redis": "^3.1.2", 24 | "redis-redisearch": "^1.0.1", 25 | "socket.io": "^4.0.2" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^2.0.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redis Developer 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 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "BlobChat", 3 | "description": "Finding Like-Minded people in Chat App using Search and Query feature of Redis", 4 | "type": "Building Block", 5 | "contributed_by": "Community", 6 | "repo_url": "https://github.com/redis-developer/BlobChat", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/BlobChat/main/images/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/BlobChat/archive/refs/heads/main.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "false", 11 | "deploy_buttons": [], 12 | "language": [ 13 | "JavaScript" 14 | ], 15 | "redis_commands": [ 16 | "FT.CREATE", 17 | "HSET", 18 | "FT.SEARCH", 19 | "RPUSH", 20 | "EXPIRE", 21 | "LRANGE" 22 | ], 23 | "redis_use_cases": [ 24 | "Caching" 25 | ], 26 | "redis_features": [ 27 | "Search and Query" 28 | ], 29 | "app_image_urls": [], 30 | "youtube_url": "https://www.youtube.com/watch?v=OOWw_tPG9ls", 31 | "special_tags": [ 32 | "Hackathon" 33 | ], 34 | "verticals": [ 35 | "Retail" 36 | ], 37 | "markdown": "https://raw.githubusercontent.com/redis-developer/BlobChat/main/README.md" 38 | } -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to BlobChat! 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 |

17 | BlobChat Demo

18 | Here you can have fun starting a chat with a similar-minded person.
19 | And don't worry about saying bye if the conversation gets boring, the app will end it for 20 | you.

21 | Step 1: Type a username and anything to start.
22 |

23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /views/posts.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time to pick 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |

16 | BlobChat Demo

17 | We have found some matches for you based on the content of your post.

18 | Step 2: Select another user's post to enter the chat room.
19 |

20 |
21 | 24 | <% posts.forEach(post => { %> 25 |
26 |
27 |
28 |

29 | 30 | <%= post.username %> 31 |

32 |

<%= post.content %>

33 | Connect 34 |
35 |
36 |
37 | <% }) %> 38 |
39 |
40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlobChat 2 | 3 | BlobChat is a minimalist and elegant chat app. 4 | 5 | 1. Make a post and talk about anything you want. 6 | 2. Blob will find similar posts made by other users. 7 | Select another user's post and start chatting. 8 | 3. Chat for as long as you wish until there's no new response from either party for longer than 3 days (in demo it's set to 60 seconds). 9 | 10 | ![Make a post](https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/1_index_screenshot.png) 11 | 12 | # Technology Used 13 | 14 | The local server/client setup to support a web application. 15 | 16 | - NodeJS 17 | - SocketIO 18 | - Express 19 | 20 | # Redis Modules 21 | 22 | - Redis - to store posts and chat messages 23 | -Redis Search- to index and explore potential matches. 24 | 25 | 26 | # Build Locally 27 | 28 | ## Step 1. Clone the repo to your local directory and start the application 29 | 30 | ``` 31 | git clone https://github.com/redis-developer/BlobChat 32 | ``` 33 | 34 | 35 | ## Step 2. Build Docker images locally. 36 | 37 | ``` 38 | docker-compose build 39 | ``` 40 | 41 | ## Step 3. Start the Redis server. 42 | 43 | ``` 44 | docker-compose up -d 45 | ``` 46 | 47 | ## Step 4. Install the npm dependencies. 48 | 49 | ``` 50 | npm install 51 | ``` 52 | 53 | ## Step 5. Start the chat client (with nodemon). 54 | 55 | ``` 56 | npm run start 57 | ``` 58 | Now you can go to http://localhost:5000/ to start. 59 | 60 | # How to Blob 61 | 62 | ![Find a match](https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/2_posts_screenshot.png) 63 | ![Enjoy a chat](https://raw.githubusercontent.com/redis-developer/BlobChat/main/public/3_chat_screenshot.png) 64 | 65 | > For the simplicity of the demonstration, some logic have been altered: 66 | > * Pre-populated sample posts are in [/public/sample_posts.json]. 67 | > * Chat timeouts in 60s instead of 3 days. History is not automatically cleared in the UI, but refreshing the page will do. 68 | 69 | 70 | # Redis Command Explained 71 | 72 | ### Posts (Redis Hash, Redis Search) 73 | **FT.CREATE** 74 | Create aRedis Searchindex on all posts (key prefix `post`) and make the content of the posts full-text searchable. 75 | e.g. `Redis: FT.CREATE posts_idx ON HASH PREFIX 1 post SCHEMA content TEXT` 76 | 77 | **HSET** 78 | When user creates a new post, store the content with the username. 79 | e.g. `Redis: HSET post_CookieMonster username CookieMonster content "A brown fox jumps over the fence."` 80 | 81 | **FT.SEARCH** 82 | Search among existing posts in Redis Search for similar posts. 83 | e.g. `Redis: FT.SEARCH posts_idx "what | a | lovely | day"` 84 | 85 | ### Chat (Redis List) 86 | **RPUSH** 87 | Add a new message to the bottom of the chat history between two users. 88 | e.g. `Redis: RPUSH messages fromUser:toUser:Hi` 89 | 90 | **EXPIRE** 91 | Set the conversation history to expire. 92 | e.g. `Redis: EXPIRE messages 60` 93 | 94 | **LRANGE** 95 | Retrieves all chat messages from Redis. 96 | e.g. `Redis: LRANGE messages 0 -1` 97 | 98 | # Tech Debt 99 | ### Future improvements 100 | * Use RedisAI to train on the user data (content of posts, and what posts got chosen by certain users), 101 | and make better recommendations. 102 | * Use Redis to store user connections so that a chat room can only be joined by people who are a match from the previous step. 103 | 104 | ### Smaller UI issues 105 | * Username does not allow whitespaces but there's no actual validation. 106 | * Auto scrolling is not implemented, users might not see that they have got new messages. 107 | * Pushing the enter button on a keyboard does not send the message, only the UI button does. 108 | 109 | # Reference 110 | Used [CSS style sheet](https://bbbootstrap.com/snippets/simple-chat-application-57631463) as a css starter. 111 | Blob icon is created via [Blobs.app](https://blobs.app/?e=6&gw=6&se=3&g=eecda3|ef629f&o=0). 112 | [Color palette](https://coolors.co/f6bd60-f7ede2-f5cac3-84a59d-f28482). 113 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const express = require("express"); 3 | const http = require("http"); 4 | const socketio = require("socket.io"); 5 | const redis = require("redis"); 6 | const redisearch = require('redis-redisearch'); 7 | const PORT = 5000; 8 | 9 | // Connects to local Redis and enable RediSearch commands. 10 | const redisClient = redis.createClient(); 11 | redisearch(redis); 12 | // Creates RediSearch index if it does not exist. 13 | console.log(`Redis: FT.CREATE posts_idx ON HASH PREFIX 1 post SCHEMA content TEXT`) 14 | redisClient.ft_create("posts_idx", "ON", "HASH", "PREFIX", "1", "post", "SCHEMA", "content", "TEXT", 15 | function (err, response) { 16 | if (err && err.message !== "Index already exists") throw err; 17 | }); 18 | 19 | // Loads sample posts into Redis. 20 | let rawPosts = fs.readFileSync('./public/sample_posts.json'); 21 | let jsonPosts = JSON.parse(rawPosts); 22 | jsonPosts.map(item => { 23 | const username = item.username; 24 | const content = item.content; 25 | // Saves user's post to Redis. 26 | console.log(`Redis: HSET post_${username} username ${username} content "${content}"`); 27 | redisClient.hset(`post_${username}`, "username", username, "content", content); 28 | }) 29 | 30 | // Sets up application. 31 | const app = express(); 32 | app.set("view engine", "ejs"); 33 | // Hosts css files from public directory. 34 | app.use(express.static('public')); 35 | const server = http.createServer(app); 36 | const io = socketio(server).listen(server); 37 | 38 | // Index page for making a post. 39 | app.get("/", (req, res) => { 40 | res.render("index"); 41 | }); 42 | 43 | // Posts page for viewing matched posts and selecting a user. 44 | app.get('/posts', (req, res) => { 45 | const username = req.query.username; 46 | const myPost = req.query.post; 47 | // Saves user's post to Redis. 48 | console.log(`Redis: HSET post_${username} username ${username} content "${myPost}"`); 49 | redisClient.hset(`post_${username}`, "username", username, "content", myPost); 50 | // Finds similar posts in Redis. 51 | // Data cleaning - remove non-alphanumeric characters and redundant spaces. 52 | const searchString = myPost.replace(/\W+/g, " ").replace(/ +/g, ' ').split(" ").join(" | "); 53 | console.log(`Redis: FT.SEARCH posts_idx "${searchString}"`); 54 | redisClient.ft_search("posts_idx", searchString, function (err, data) { 55 | // Search response looks like 56 | // [ 57 | // 5, # number of records 58 | // 'post_user5', # key of first record 59 | // [ 'username', 'user5', 'content', 'fox jumps' ], # value of first record 60 | // ] 61 | let posts = []; 62 | let keyCount = data.length - 1; 63 | // Remove first item (number of records), and parse the rest. 64 | data.slice(1).map(function (record) { 65 | if (Array.isArray(record)) { 66 | let post = {}; 67 | for (let i = 0; i < record.length; i += 2) { 68 | post[record[i]] = record[i + 1]; 69 | } 70 | // But not this user's own post. 71 | if (username !== post.username) { 72 | posts.push(post); 73 | } 74 | // Only when all records are loaded, we will render the webpage. 75 | keyCount = keyCount - 2 76 | if (keyCount === 0) { 77 | res.render('posts', {username: username, posts: posts}); 78 | } 79 | } 80 | }); 81 | }); 82 | }); 83 | 84 | // Chat page for... chatting. 85 | app.get("/chat", (req, res) => { 86 | const username = req.query.username; 87 | const matched = req.query.matched; 88 | 89 | io.emit("join", username); 90 | res.render("chat", {username, matched}); 91 | }); 92 | 93 | // Message bus. 94 | io.on("connection", socket => { 95 | loadExistingMessages(socket); 96 | 97 | socket.on("message", ({message, from, to}) => { 98 | // Sorts the usernames so the Redis key stays the same regardless of who sent the message. 99 | const sortedUsernames = [from, to]; 100 | sortedUsernames.sort(); 101 | //const redisKey = `messages_${sortedUsernames[0]}_${sortedUsernames[1]}`; 102 | const redisKey = "messages" 103 | const redisValue = `${from}:${to}:${message}`; 104 | // Saves the new message to the end of the message list in Redis. 105 | console.log(`Redis: RPUSH ${redisKey} ${redisValue}`); 106 | redisClient.rpush(`${redisKey}`, `${redisValue}`); 107 | // Resets TTL to 60 seconds. 108 | console.log(`Redis: EXPIRE ${redisKey} 60`); 109 | redisClient.expire(`${redisKey}`, 60); 110 | io.emit("message", {message, from, to}); 111 | }); 112 | }); 113 | 114 | function loadExistingMessages(socket) { 115 | console.log("Redis: LRANGE messages 0 -1"); 116 | redisClient.lrange("messages", "0", "-1", (err, data) => { 117 | data.map(x => { 118 | const messageSections = x.split(":"); 119 | const from = messageSections[0]; 120 | const to = messageSections[1]; 121 | const message = messageSections[2]; 122 | 123 | socket.emit("message", { 124 | message: message, 125 | from: from, 126 | to: to 127 | }); 128 | }); 129 | if (data.length !== 0) { 130 | socket.emit("loadFinished"); 131 | } 132 | }); 133 | } 134 | 135 | server.listen(PORT, () => { 136 | console.log(`Server stared at ${PORT}.`); 137 | }); 138 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | .card-bordered { 2 | border: 1px solid #ebebeb 3 | } 4 | 5 | .card { 6 | max-width: 700px; 7 | border: 0; 8 | border-radius: 0px; 9 | margin-bottom: 30px; 10 | -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03); 11 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03); 12 | -webkit-transition: .5s; 13 | transition: .5s 14 | } 15 | 16 | .padding { 17 | padding: 3rem !important 18 | } 19 | 20 | body { 21 | background-color: #84A59D 22 | } 23 | 24 | .card-header:first-child { 25 | border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0 26 | } 27 | 28 | .card-header { 29 | display: -webkit-box; 30 | display: flex; 31 | -webkit-box-pack: justify; 32 | justify-content: space-between; 33 | -webkit-box-align: center; 34 | align-items: center; 35 | padding: 15px 20px; 36 | background-color: #f5f6f7; 37 | border-bottom: 1px solid rgba(77, 82, 89, 0.07) 38 | } 39 | 40 | .card-header .card-title { 41 | padding: 0; 42 | border: none 43 | } 44 | 45 | h4.card-title { 46 | font-size: 17px 47 | } 48 | 49 | .card-header>*:last-child { 50 | margin-right: 0 51 | } 52 | 53 | .card-header>* { 54 | margin-left: 8px; 55 | margin-right: 8px 56 | } 57 | 58 | .btn-secondary { 59 | color: #4d5259 !important; 60 | background-color: #e4e7ea; 61 | border-color: #e4e7ea; 62 | color: #fff 63 | } 64 | 65 | .btn-xs { 66 | font-size: 11px; 67 | padding: 2px 8px; 68 | line-height: 18px 69 | } 70 | 71 | .btn-xs:hover { 72 | color: #fff !important 73 | } 74 | 75 | .card-title { 76 | font-family: Roboto, sans-serif; 77 | font-weight: 300; 78 | line-height: 1.5; 79 | margin-bottom: 0; 80 | padding: 15px 20px; 81 | border-bottom: 1px solid rgba(77, 82, 89, 0.07) 82 | } 83 | 84 | .ps-container { 85 | position: relative; 86 | background-color: #f5f6f7; 87 | } 88 | 89 | .ps-container { 90 | -ms-touch-action: auto; 91 | touch-action: auto; 92 | overflow: hidden !important; 93 | -ms-overflow-style: none 94 | } 95 | 96 | .media-chat { 97 | padding-right: 64px; 98 | margin-bottom: 0 99 | } 100 | 101 | .media { 102 | -webkit-transition: background-color .2s linear; 103 | transition: background-color .2s linear 104 | } 105 | 106 | .media .avatar { 107 | flex-shrink: 0 108 | } 109 | 110 | .avatar { 111 | position: relative; 112 | display: inline-block; 113 | width: 36px; 114 | height: 36px; 115 | line-height: 36px; 116 | text-align: center; 117 | border-radius: 100%; 118 | background-color: #f5f6f7; 119 | color: #8b95a5; 120 | text-transform: uppercase 121 | } 122 | 123 | .avatar-reverse { 124 | padding-bottom: 8px; 125 | float: right; 126 | position: relative; 127 | display: inline-block; 128 | width: 36px; 129 | height: 36px; 130 | line-height: 36px; 131 | text-align: center; 132 | border-radius: 100%; 133 | background-color: #f5f6f7; 134 | color: #8b95a5; 135 | text-transform: uppercase 136 | } 137 | 138 | .media-chat .media-body { 139 | -webkit-box-flex: initial; 140 | flex: initial; 141 | } 142 | 143 | .media-body { 144 | min-width: 0 145 | } 146 | 147 | .media-chat .media-body p { 148 | position: relative; 149 | padding: 6px 8px; 150 | margin: 4px 0; 151 | background-color: #F6BD60; 152 | border-radius: 3px; 153 | font-weight: 100; 154 | color: #111 155 | } 156 | 157 | .media>* { 158 | margin: 0 8px 159 | } 160 | 161 | .media-chat .media-body p.meta { 162 | background-color: transparent !important; 163 | padding: 0; 164 | opacity: .8 165 | } 166 | 167 | .media-meta-day { 168 | -webkit-box-pack: justify; 169 | justify-content: space-between; 170 | -webkit-box-align: center; 171 | text-align: center; 172 | margin-bottom: 0; 173 | color: #8b95a5; 174 | opacity: .8; 175 | font-weight: 400 176 | } 177 | 178 | .media { 179 | padding: 16px 12px; 180 | -webkit-transition: background-color .2s linear; 181 | transition: background-color .2s linear 182 | } 183 | 184 | .media-meta-day::before { 185 | margin-right: 16px 186 | } 187 | 188 | .media-meta-day::before, 189 | .media-meta-day::after { 190 | content: ''; 191 | -webkit-box-flex: 1; 192 | flex: 1 1; 193 | border-top: 1px solid #ebebeb 194 | } 195 | 196 | .media-meta-day::after { 197 | content: ''; 198 | -webkit-box-flex: 1; 199 | flex: 1 1; 200 | border-top: 1px solid #ebebeb 201 | } 202 | 203 | .media-meta-day::after { 204 | margin-left: 16px 205 | } 206 | 207 | .media-chat.media-chat-reverse { 208 | padding-right: 12px; 209 | padding-left: 64px; 210 | -webkit-box-orient: horizontal; 211 | -webkit-box-direction: reverse; 212 | flex-direction: row-reverse 213 | } 214 | 215 | .media-chat { 216 | padding-right: 64px; 217 | margin-bottom: 0; 218 | background-color: #f5f6f7; 219 | } 220 | 221 | .media { 222 | -webkit-transition: background-color .2s linear; 223 | transition: background-color .2s linear 224 | } 225 | 226 | .media-chat.media-chat-reverse .media-body p { 227 | text-align: right; 228 | clear: right; 229 | background-color: #F5CAC3; 230 | color: #111 231 | } 232 | 233 | .post-header p { 234 | position: relative; 235 | padding: 10px 10px; 236 | background-color: #F6BD60; 237 | border-radius: 3px 238 | } 239 | 240 | .post-header h3 { 241 | text-align: center; 242 | padding: 0px 0px; 243 | border-radius: 3px; 244 | font-family: Roboto, sans-serif; 245 | font-size: x-large; 246 | color: #4d5259; 247 | } 248 | 249 | .post-header img { 250 | vertical-align: sub; 251 | 252 | } 253 | 254 | 255 | .border-light { 256 | border-color: #f1f2f3 !important 257 | } 258 | 259 | .bt-1 { 260 | border-top: 1px solid #ebebeb !important 261 | } 262 | 263 | .publisher { 264 | position: relative; 265 | display: -webkit-box; 266 | display: flex; 267 | -webkit-box-align: center; 268 | align-items: center; 269 | padding: 12px 20px; 270 | background-color: #f9fafb 271 | } 272 | 273 | .publisher>*:first-child { 274 | margin-left: 0 275 | } 276 | 277 | .publisher>* { 278 | margin: 0 8px 279 | } 280 | 281 | .publisher-input { 282 | -webkit-box-flex: 1; 283 | flex-grow: 1; 284 | border: none; 285 | outline: none !important; 286 | background-color: transparent 287 | } 288 | 289 | .publisher-input-textarea { 290 | -webkit-box-flex: 1; 291 | flex-grow: 1; 292 | border: none; 293 | outline: none !important; 294 | height: 200px; 295 | width: auto 296 | } 297 | 298 | button, 299 | input, 300 | optgroup, 301 | select, 302 | a, 303 | textarea { 304 | font-family: Roboto, sans-serif; 305 | font-weight: 300 306 | } 307 | 308 | .publisher-btn { 309 | background-color: transparent; 310 | border: none; 311 | color: #8b95a5; 312 | font-size: 16px; 313 | cursor: pointer; 314 | overflow: -moz-hidden-unscrollable; 315 | -webkit-transition: .2s linear; 316 | transition: .2s linear 317 | } 318 | 319 | .file-group { 320 | position: relative; 321 | overflow: hidden 322 | } 323 | 324 | .publisher-btn { 325 | background-color: transparent; 326 | border: none; 327 | color: #cac7c7; 328 | font-size: 16px; 329 | cursor: pointer; 330 | overflow: -moz-hidden-unscrollable; 331 | -webkit-transition: .2s linear; 332 | transition: .2s linear 333 | } 334 | 335 | .file-group input[type="file"] { 336 | position: absolute; 337 | opacity: 0; 338 | z-index: -1; 339 | width: 20px 340 | } 341 | 342 | .text-info { 343 | color: #FFF !important; 344 | background-color: #F28482; 345 | padding: 5px 5px; 346 | border-radius: 3px; 347 | margin-top: 10px; 348 | text-decoration: none; 349 | } 350 | 351 | .icon-big { 352 | width: 350px; 353 | height: 350px 354 | } 355 | 356 | .icon-small { 357 | width: 30px; 358 | height: 30px 359 | } 360 | -------------------------------------------------------------------------------- /views/chat.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Let's have some conversation... 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |

17 | BlobChat Demo

18 | If both of you stops talking, the conversation will "magically" end in 60 seconds.

19 | Step 3: Have fun chatting and feel free to leave at any time. 20 |

21 |
22 |
23 |

Welcome, <%= username %>.

24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 158 | 159 | --------------------------------------------------------------------------------