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')