diff --git a/CHANGELOG.md b/CHANGELOG.md index 6689dee..fae8712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CHANGELOG.md Changelog - content to the README.md +- Chat to the bingo game (renderd with markdown-it) diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 43e0d79..36ec33a 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -10,10 +10,13 @@ type BingoMutation { toggleWord(input: WordInput!): BingoGrid # set the username of the current session - setUsername(input: UsernameInput): BingoUser + setUsername(input: UsernameInput!): BingoUser # recreates the active game to a follow-up createFollowupGame: BingoGame + + # sends a message to the current sessions chat + sendChatMessage(input: MessageInput!): ChatMessage } type BingoQuery { @@ -28,36 +31,6 @@ type BingoQuery { activeGrid: BingoGrid } -input CreateGameInput { - - # the words used to fill the bingo grid - words: [String!]! - - # the size of the bingo grid - size: Int! = 3 -} - -input WordInput { - - # the normal word string - word: String - - # the base64-encoded word - base64Word: String -} - -input UsernameInput { - - # the username string - username: String! -} - -input IdInput { - - # the id - id: ID! -} - type BingoGame { # the id of the bingo game @@ -80,6 +53,9 @@ type BingoGame { # the id of the followup game if it has been created followup: ID + + # Returns the last n chat-messages + getMessages(input: MessageQueryInput): [ChatMessage!] } type BingoUser { @@ -117,3 +93,83 @@ type BingoField { # the base64 encoded word base64Word: String } + +type ChatMessage { + + # the id of the message + id: ID! + + # the content of the message + content: String! + + # the content of the message rendered by markdown-it + htmlContent: String + + # the type of the message + type: MessageType! + + # the username of the sender + username: String + + # the time the message was send (in milliseconds) + datetime: String! +} + +# # +# input Types # +# # + +input CreateGameInput { + + # the words used to fill the bingo grid + words: [String!]! + + # the size of the bingo grid + size: Int! = 3 +} + +input WordInput { + + # the normal word string + word: String + + # the base64-encoded word + base64Word: String +} + +input UsernameInput { + + # the username string + username: String! +} + +input IdInput { + + # the id + id: ID! +} + +input MessageInput { + + # the message + message: String! +} + +input MessageQueryInput { + + # search for a specific id + id: ID + + # get the last n messages + last: Int = 10 +} + +# # +# enum Types # +# # + +enum MessageType { + USER + ERROR + INFO +} diff --git a/package-lock.json b/package-lock.json index 389a636..e1f03ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -761,6 +761,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2158,6 +2163,14 @@ "invert-kv": "^1.0.0" } }, + "linkify-it": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -2216,6 +2229,28 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3570,6 +3605,11 @@ "mime-types": "~2.1.24" } }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index 627a90c..a17f1e6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "graphql-import": "^0.7.1", "http-errors": "~1.6.3", "js-yaml": "latest", + "markdown-it": "^8.4.2", + "markdown-it-emoji": "^1.4.0", "morgan": "~1.9.1", "node-sass": "^4.12.0", "pug": "2.0.0-beta11" diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 882af3a..c89e5ac 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -187,6 +187,12 @@ async function refresh() { username id } + getMessages { + id + username + type + htmlContent + } } } }`, null, `/graphql?game=${getGameParam()}`); @@ -213,6 +219,10 @@ async function refresh() { } } } + for (let chatMessage of bingoSession.getMessages) { + if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`)) + addChatMessage(chatMessage); + } } else { if (response.status === 400) clearInterval(refrInterval); @@ -260,6 +270,54 @@ function showError(errorMessage) { }, 10000); } +async function sendChatMessage() { + let messageInput = document.querySelector('#chat-input'); + if (messageInput.value && messageInput.value.length > 0) { + let message = messageInput.value; + let response = await postGraphqlQuery(` + mutation($message: String!) { + bingo { + sendChatMessage(input: { message: $message }) { + id + htmlContent + username + type + } + } + }`,{message: message}, `/graphql?game=${getGameParam()}`); + if (response.status === 200) { + addChatMessage(response.data.bingo.sendChatMessage); + messageInput.value = ''; + } else { + console.error(response); + showError('Error when sending message.'); + } + } +} + +/** + * Adds a message to the chat + * @param messageObject {Object} - the message object returned by graphql + */ +function addChatMessage(messageObject) { + let msgSpan = document.createElement('span'); + msgSpan.setAttribute('class', 'chatMessage'); + msgSpan.setAttribute('msg-id', messageObject.id); + if (messageObject.type === "USER") { + msgSpan.innerHTML = ` + ${messageObject.username}: + ${messageObject.htmlContent} + `; + } else { + msgSpan.innerHTML = ` + ${messageObject.htmlContent} + `; + } + let chatContent = document.querySelector('#chat-content'); + chatContent.appendChild(msgSpan); + chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom +} + /** * Executes the provided function if the key-event is an ENTER-key * @param event {Event} - the generated key event diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index d79b6c2..ab28468 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -22,6 +22,7 @@ textarea #content-container grid-template-columns: 0 100% !important grid-template-rows: 10% 80% 10% !important + #players-container div display: none padding: 0 @@ -126,11 +127,14 @@ textarea #content-container display: grid - grid-template-columns: 20% 80% - grid-template-rows: 10% 80% 10% + grid-template-columns: 25% 75% + grid-template-rows: 10% 40% 40% 10% height: 100% width: 100% + div + overflow: auto + #button-container grid-column-start: 1 grid-column-end: 1 @@ -150,28 +154,63 @@ textarea grid-column-start: 1 grid-column-end: 1 grid-row-start: 2 - grid-row-end: 2 + grid-row-end: 3 h1 margin: 0 0 1rem 0 #words-container grid-column-start: 2 - grid-column-end: 2 + grid-column-end: 3 grid-row-start: 2 - grid-row-end: 2 + grid-row-end: 4 + + #chat-container + grid-column-start: 1 + grid-column-end: 1 + grid-row-start: 3 + grid-row-end: 4 + height: 100% + border: 1px solid $inactive + margin: 0 0.5rem + + #chat-content + height: calc(100% - 3.5rem) + background-color: $primary + overflow: auto + + .chatMessage + display: list-item + padding: 0.2rem + + .chatUsername + color: $inactive + + .ERROR + color: $error + + .INFO + color: $inactive + font-style: italic + + #chat-input + width: 100% + margin: 1rem 0 0 0 + height: 2.5rem + border-radius: 0 .errorDiv grid-column-start: 2 - grid-column-end: 2 - grid-row-start: 3 - grid-row-end: 3 + grid-column-end: 3 + grid-row-start: 4 + grid-row-end: 4 background-color: $error text-align: center margin: 0.75rem 0 border-radius: 1rem height: calc(100% - 1.5rem) display: table + span display: table-cell font-size: 1.8rem @@ -210,4 +249,4 @@ textarea z-index: 99 top: 0 left: 0 - background-color: rgba(0,0,0,0.5) + background-color: rgba(0, 0, 0, 0.5) diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 066a693..3b56a96 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -53,3 +53,18 @@ input textarea background-color: lighten($primary, 15%) + +a + color: $secondary + +::-webkit-scrollbar + width: 12px + height: 12px + +::-webkit-scrollbar-thumb + background: darken($secondary, 5) + border-radius: 10px + +::-webkit-scrollbar-track + background: lighten($primary, 5) + border-radius: 10px diff --git a/routes/bingo.js b/routes/bingo.js index ec1cb0a..3daa8b1 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1,7 +1,9 @@ const express = require('express'), router = express.Router(), cproc = require('child_process'), - fsx = require('fs-extra'); + fsx = require('fs-extra'), + mdEmoji = require('markdown-it-emoji'), + md = require('markdown-it')().use(mdEmoji); const rWordOnly = /^\w+$/; @@ -21,6 +23,7 @@ class BingoSession { this.bingos = []; // array with the users that already had bingo this.finished = false; this.followup = null; + this.chatMessages = []; } /** @@ -53,8 +56,54 @@ class BingoSession { let followup = new BingoSession(this.words, this.gridSize); this.followup = followup.id; bingoSessions[followup.id] = followup; + followup.chatMessages = this.chatMessages; + followup.chatMessages.push(new BingoChatMessage('--- Rematch ---', "INFO")); return followup; } + + /** + * Graphql endpoint to get the last n messages or messages by id + * @param args {Object} - arguments passed by graphql + * @returns {[]} + */ + getMessages(args) { + let input = args.input || null; + if (input && input.id) { + return this.chatMessages.find(x => (x && x.id === input.id)); + } else if (input && input.last) { + return this.chatMessages.slice(-input.last); + } else { + return this.chatMessages.slice(-10); + } + } + + /** + * Sends the message that a user toggled a word. + * @param base64Word + * @param bingoUser + */ + sendToggleInfo(base64Word, bingoUser) { + let word = Buffer.from(base64Word, 'base64').toString(); + let toggleMessage = new BingoChatMessage(`${bingoUser.username} toggled phrase "${word}"`, "INFO"); + this.chatMessages.push(toggleMessage); + } +} + +class BingoChatMessage { + /** + * Chat Message class constructor + * @param messageContent {String} - the messages contents + * @param type {String} - the type constant of the message (USER, ERROR, INFO) + * @param [username] {String} - the username of the user who send this message + */ + constructor(messageContent, type="USER", username) { + this.id = generateBingoId(); + this.content = messageContent; + this.htmlContent = md.renderInline(messageContent); + this.datetime = Date.now(); + this.username = username; + this.type = type; + } } class BingoUser { @@ -96,6 +145,15 @@ class BingoGrid { } } +/** + * Replaces tag signs with html-escaped signs. + * @param htmlString + * @returns {string} + */ +function replaceTagSigns(htmlString) { + return htmlString.replace(//g, '>'); +} + /** * Shuffles the elements in an array * @param array {Array<*>} @@ -316,6 +374,7 @@ router.graphqlResolver = (req, res) => { input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); if (bingoUser.grids[gameId]) { toggleHeared(input.base64Word, bingoUser.grids[gameId]); + bingoSession.sendToggleInfo(input.base64Word, bingoUser); return bingoUser.grids[gameId]; } else { res.status(400); @@ -343,6 +402,16 @@ router.graphqlResolver = (req, res) => { } else { res.status(400); } + }, + sendChatMessage: ({input}) => { + input.message = replaceTagSigns(input.message); + if (bingoSession && input.message) { + let userMessage = new BingoChatMessage(input.message, 'USER', bingoUser.username); + bingoSession.chatMessages.push(userMessage); + return userMessage; + } else { + res.status(400); + } } }; }; diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index 0cffc18..78654b6 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -13,6 +13,9 @@ block content each player in players div(class='player-container', b-pid=`${player.id}`) span(class='player-name-span')= player.username + div(id='chat-container') + div(id='chat-content') + input(id='chat-input' type='text', placeholder='chat', onkeypress='submitOnEnter(event, sendChatMessage)') div(id='words-container') each val in grid div(class='bingo-word-row')