Added Bingo Chat

- added a chat to the bingo game
pull/7/head
Trivernis 6 years ago
parent 3ac3180957
commit 20e1540175

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

@ -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
}

40
package-lock.json generated

@ -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",

@ -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"

@ -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 = `
<span class="chatUsername">${messageObject.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>
`;
} else {
msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>
`;
}
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

@ -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

@ -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

@ -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, '&#60;').replace(/>/g, '&#62;');
}
/**
* 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);
}
}
};
};

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

Loading…
Cancel
Save