diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce2c48..ec5e990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - compression and minify - auto replacing image links with images in the chat - auto replacing urls to urls with link in the chat +- message editing and deleting *(undo your mistakes)* +- changelog to `bingo-create` -## Changed +### Changed - frontend to use socket.io instead of graphql for refreshing - use of socket.io for toggeling binogo fields +- button behaviour on `bingo-create` to respond to the situation *(whatever that means)* ### Removed @@ -24,9 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Socket reconnect doesn't load old messages (#20) -- error message on create ui load -- chat doesn't scroll down when an image is send +- error message when loading `bingo-create` +- chat doesn't scroll down when an image is send *(r/mildlyinfuriating)* +- some style issues ## [0.1.1] - 2019-05-21 diff --git a/lib/globals.js b/lib/globals.js index e4d7c0a..9fe4ae3 100644 --- a/lib/globals.js +++ b/lib/globals.js @@ -1,10 +1,12 @@ const utils = require('./utils'), + fsx = require('fs-extra'), pg = require('pg'); const settings = utils.readSettings('.'); Object.assign(exports, { settings: settings, + changelog: fsx.readFileSync('CHANGELOG.md', 'utf-8'), pgPool: new pg.Pool({ host: settings.postgres.host, port: settings.postgres.port, @@ -14,7 +16,7 @@ Object.assign(exports, { }), cookieInfo: { headline: 'This website uses cookies', - content: 'This website uses cookies to store your session data (like for bingo).', + content: "This website uses cookies to store your session data. No data is permanently stored.", onclick: 'acceptCookies()', id: 'cookie-container', button: 'All right!' diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 7a961d5..9d74d08 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -246,6 +246,95 @@ class BingoGraphqlHelper { } } +class ChatInput { + constructor(element) { + this.element = element; + this.mode = 0; + this.user = 0; + this.editId = null; + } + + + /** + * Sends a message to the chat + * @returns {Promise} + */ + async sendChatMessage() { + if (this.element.value && this.element.value.length > 0) { + let message = this.element.value; + this.element.value = ''; + + if (this.mode === 0) { + if (/^\/\.*/g.test(message)) + await executeCommand(message); + else + socket.emit('message', message); + } else { + socket.emit('messageEdit', message, this.editId); + this.setNormal(); + } + } else if (this.mode === 1) { + socket.emit('messageDelete', this.editId); + this.setNormal(); + } + } + + /** + * Returns the last message + * @param [before] {Number} - last message before a specific id + * @param [after] {Number} - last message after a specific id + * @returns {Element|*} + * @private + */ + _getMessage(before, after) { + let messages = [...document.querySelectorAll(`.chatMessage[msg-pid='${this.user}']`)]; + let message = null; + + if (before) + message = messages.filter(x => Number(x.getAttribute('msg-id') < before)).slice(-1); + else if (after) + message = messages.filter(x => Number(x.getAttribute('msg-id') > after)); + else + message = messages.slice(-1); + if (message.length > 0) + return message[0]; + } + + setEdit(after) { + let message = null; + let lastMessage = document.querySelector(`.chatMessage[msg-id='${this.editId}']`); + if (this.mode === 0 && !after) { + this.mode = 1; + message = this._getMessage(); + } else if (after && this.mode === 1) { + message = this._getMessage(null, this.editId); + } else if (this.mode === 1) { + message = this._getMessage(this.editId); + } + if (message) { + message.classList.add('selected'); + if (lastMessage) + lastMessage.classList.remove('selected'); + + this.element.value = message.getAttribute('msg-raw'); + this.editId = Number(message.getAttribute('msg-id')); + } else { + this.setNormal(); + } + } + + setNormal() { + if (this.mode !== 0) { + this.element.value = ''; + this.mode = 0; + let lastMessage = document.querySelector(`.chatMessage[msg-id='${this.editId}']`); + if (lastMessage) + lastMessage.classList.remove('selected'); + this.editId = null; + } + } +} + /** * Returns the value of the url-param 'g' * @returns {string} @@ -397,23 +486,6 @@ async function executeCommand(message) { } } -/** - * Sends a message to the chat - * @returns {Promise} - */ -async function sendChatMessage() { - let messageInput = document.querySelector('#chat-input'); - if (messageInput.value && messageInput.value.length > 0) { - let message = messageInput.value; - messageInput.value = ''; - - if (/^\/\.*/g.test(message)) - await executeCommand(message); - else - socket.emit('message', message); - } -} - /** * Starts a new round of bingo * @returns {Promise} @@ -531,18 +603,35 @@ async function submitBingo() { } } +async function onInputKeypress(e) { + switch (e.which) { + case 13: + await chatInput.sendChatMessage(); + break; + case 38: + chatInput.setEdit(); + break; + case 27: + chatInput.setNormal(); + break; + case 40: + chatInput.setEdit(true); + } +} + /** * Displays the winner of the game in a popup. * @param winner {Object} - the round object as returned by graphql */ -function displayWinner(winner) { +function displayWinner(winner, isPlayer) { let name = winner.username; let winnerDiv = document.createElement('div'); let greyoverDiv = document.createElement('div'); winnerDiv.setAttribute('class', 'popup'); winnerDiv.innerHTML = ` -

${name} has won!

- +

${name} has won!

+

${isPlayer? 'Congratulations!':'And you lost. How does this make you feel?'}

+
`; greyoverDiv.setAttribute('class', 'greyover'); document.body.append(greyoverDiv); @@ -594,14 +683,16 @@ function addChatMessage(messageObject, player) { msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-type', messageObject.type); msgSpan.setAttribute('msg-id', messageObject.id); + msgSpan.setAttribute('msg-raw', messageObject.content); - if (messageObject.type === "USER") + if (messageObject.type === "USER") { msgSpan.innerHTML = ` ${messageObject.author.username}: ${messageObject.htmlContent}`; - else - msgSpan.innerHTML = ` - ${messageObject.htmlContent}`; + msgSpan.setAttribute('msg-pid', messageObject.author.id); + } else { + msgSpan.innerHTML = `${messageObject.htmlContent}`; + } if (messageObject.type === 'USER' && messageObject.author && messageObject.author.id !== player) @@ -611,9 +702,11 @@ function addChatMessage(messageObject, player) { chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom - msgSpan.querySelector('img').onload = () => { - chatContent.scrollTop = chatContent.scrollHeight; - }; + let msgImg = msgSpan.querySelector('img'); + if (msgImg) + msgImg.onload = () => { + chatContent.scrollTop = chatContent.scrollHeight; + }; } /** @@ -701,14 +794,34 @@ function initSocketEvents(data) { showError(`Socket Error: ${JSON.stringify(error)}`); }); - socket.on('message', (msg) => { + socket.on('userError', (error) => { + showError(error); + }); + + socket.on('message', msg => { addChatMessage(msg, playerId); }); + socket.on('messageEdit', msg => { + let message = document.querySelector(`.chatMessage[msg-id='${msg.id}']`); + message.setAttribute('msg-raw', msg.content); + message.querySelector('.chatMessageContent').innerHTML = msg.htmlContent; + let chatContent = document.querySelector('#chat-content'); + let msgImg = message.querySelector('img'); + if (msgImg) + msgImg.onload = () => { + chatContent.scrollTop = chatContent.scrollHeight; + }; + }); + + socket.on('messageDelete', msgId => { + document.querySelector(`.chatMessage[msg-id='${msgId}'`).remove(); + }); + socket.on('statusChange', (status, winner) => { if (status === 'FINISHED' && winner) { if (document.querySelector('#container-bingo-round')) - displayWinner(winner); + displayWinner(winner, winner.id === Number(playerId)); } else { window.location.reload(); } @@ -724,6 +837,9 @@ function initSocketEvents(data) { socket.on('usernameChange', (playerObject) => { document.querySelector(`.playerEntryContainer[b-pid='${playerObject.id}'] .playerNameSpan`).innerText = playerObject.username; + let msgUsernames = document.querySelectorAll(`.chatMessage[msg-pid='${playerObject.id}'] .chatUsername`); + for (let element of msgUsernames) + element.innerText = `${playerObject.username}: `; }); socket.on('wordsChange', async () => { @@ -752,6 +868,7 @@ function initRefresh() { getPlayerInfo().then((data) => { socket = new SimpleSocket(`/bingo/${getLobbyParam()}`, {playerId: data.id}); initSocketEvents(data); + chatInput.user = data.id; }); let chatContent = document.querySelector('#chat-content'); chatContent.scrollTop = chatContent.scrollHeight; @@ -771,6 +888,10 @@ window.addEventListener("keydown", async (e) => { await statusWrap(async () => await BingoGraphqlHelper.setLobbySettings(getLobbyWords(), gridSize)); } } + if ([40, 38, 27].includes(e.which) && e.target === document.querySelector('#chat-Input')) { + e.preventDefault(); + await onInputKeypress(e); + } }, false); window.onload = async () => { @@ -782,6 +903,8 @@ window.onload = async () => { showError(err.message); } } + chatInput = new ChatInput(document.querySelector('#chat-input')); }; let socket = null; +let chatInput = null; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index 43769b7..f5cdcb0 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -164,9 +164,15 @@ async function indicateStatus(func, indicatorSelector) { statusIndicator.setAttribute('status', 'success'); else statusIndicator.setAttribute('status', 'error'); + setTimeout(() => { + statusIndicator.setAttribute('status', ''); + }, 1000); } catch (err) { console.error(err); statusIndicator.setAttribute('status', 'error'); + setTimeout(() => { + statusIndicator.setAttribute('status', ''); + }, 1000); } } diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 8ab627d..d154b6a 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -1,31 +1,46 @@ @import ../mixins @import ../vars -//@media(max-device-width: 641px) +@media(max-device-height: 500px) + div#container-winner + margin: 5% auto !important + +@media(max-device-width: 600px) + div#container-bingo-lobby + grid-template: 0 10% 85% 5% / 0 0 0 100% 0 !important + + div#container-lobby-settings + display: none !important + + div#container-players + display: none !important + //@media(min-device-width: 641px) .popup - @include default-element position: fixed - display: grid - height: calc(50% - 1rem) - width: calc(40% - 1rem) - top: 25% - left: 30% - text-align: center - vertical-align: middle - padding: 1rem + top: 0 + left: 0 + height: 100% + width: 100% z-index: 1000 - grid-template: 60% 40% / 100% - h1 - @include gridPosition(1, 2, 1, 1) - button - margin: 1rem - font-size: 2rem - @include gridPosition(2, 3, 1, 1) + #container-winner + @include default-element + position: relative + margin: 20% auto + text-align: center + width: 50% + + h1 + @include gridPosition(1, 2, 1, 1) + + button + margin: 1rem + font-size: 2rem + @include gridPosition(2, 3, 1, 1) .greyover width: 100% @@ -74,8 +89,12 @@ height: auto border-radius: 0.2em +.chatMessage.selected + background-color: lighten($primary, 15%) + #container-chat height: calc(100% - 2px) + position: relative #chat-content height: calc(100% - 3rem) width: calc(100% - 2px) @@ -222,29 +241,55 @@ #container-bingo-create display: grid - grid-template: 5% 10% 10% 70% 5% /10% 80% 10% + grid-template: 5% 45% 45% 5% /5% 35% 5% 50% 5% height: 100% width: 100% #username-form @include gridPosition(2, 3, 2, 3) margin: auto + position: relative + + h2 + text-align: center + margin: 0 0 1rem 0 + + > * + margin: auto + width: 100% + - .statusIndicator - height: 1em - width: 1em - display: inline-block - margin: auto 1em + #submit-username[status='success'] + background-color: $success + transition-duration: 1s - #input-username - margin: 0 0 0 3em + #submit-username[status='pending'] + transition-duration: 1s + background-color: $pending + animation-name: pulse-opacity + animation-duration: 4s + animation-iteration-count: infinite #lobby-form - @include gridPosition(3, 5, 2, 3) - margin: 10% auto + @include gridPosition(3, 4, 2, 3) + margin: auto + width: 100% button width: 100% + margin: auto + + button.inactive + background-color: lighten($primary, 10%) + + #changelog + @include gridPosition(2, 4, 4, 5) + height: 100% + width: calc(100% - 2rem) + overflow-y: auto + margin: 0 0.5rem + padding: 0 0.5rem + box-shadow: inset 0 0 1rem darken($primary, 10%) #container-bingo-lobby @include fillWindow @@ -256,7 +301,7 @@ #lobby-title @include gridPosition(2, 3, 2, 5) - margin: auto + margin: 0.5rem auto #container-players @include gridPosition(3, 4, 2, 3) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 409128a..cc4e442 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -7,14 +7,6 @@ .hidden display: None !important -.popup - height: 60% - width: 40% - z-index: 1000 - position: fixed - top: 20% - left: 30% - .grid display: grid diff --git a/routes/bingo.js b/routes/bingo.js index 2b9a659..ae4b7b7 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -4,7 +4,10 @@ const express = require('express'), mdEmoji = require('markdown-it-emoji'), mdMark = require('markdown-it-mark'), mdSmartarrows = require('markdown-it-smartarrows'), - md = require('markdown-it')() + md = require('markdown-it')({ + linkify: true, + typographer: true + }) .use(mdEmoji) .use(mdMark) .use(mdSmartarrows), @@ -440,6 +443,34 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addUserMessage.sql, [playerId, lobbyId, messageContent]); } + /** + * Edits a message + * @param messageId {Number} - the id of the message + * @param messageContent {String} - the new content of the message + * @returns {Promise<*>} + */ + async editMessage(messageId, messageContent) { + return await this._queryFirstResult(this.queries.editMessage.sql, [messageId, messageContent]); + } + + /** + * Deletes a message + * @param messageId {Number} - the id of the message + * @returns {Promise<*>} + */ + async deleteMessage(messageId) { + return await this._queryFirstResult(this.queries.deleteMessage.sql, [messageId]); + } + + /** + * Returns the data of a message + * @param messageId {Number} - the id of the message + * @returns {Promise<*>} + */ + async getMessageData(messageId) { + return await this._queryFirstResult(this.queries.getMessageData.sql, [messageId]); + } + /** * Adds a message of type "INFO" to the lobby * @param lobbyId {Number} - the id of the lobby @@ -1374,7 +1405,7 @@ function checkBingo(fg) { * @param message {String} - the raw message */ function preMarkdownParse(message) { - let linkMatch = /https?:\/\/((([\w-]+\.)+[\w-]+)(\S*))/g; + let linkMatch = /(^|[^(])https?:\/\/((([\w-]+\.)+[\w-]+)(\S*))([^)]|$)/g; let imageMatch = /.*\.(\w+)/g; let imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg']; let links = message.match(linkMatch); @@ -1386,8 +1417,6 @@ function preMarkdownParse(message) { if (imgGroups && imgGroups[1] && imageExtensions.includes(imgGroups[1])) message = message.replace(link, `![${linkGroups[1]}](${link})`); - else if (linkGroups && linkGroups[1]) - message = message.replace(link, `[${linkGroups[1]}](${link})`); } return message; @@ -1512,7 +1541,10 @@ async function getMessageData(lobbyId) { let messages = await lobbyWrapper.messages({limit: 20}); let msgReturn = []; for (let message of messages) - msgReturn.push(Object.assign(message, {username: await message.author.username()})); + msgReturn.push(Object.assign(message, { + playerId: message.author.id, + username: await message.author.username() + })); return msgReturn; } @@ -1536,9 +1568,35 @@ function createSocketIfNotExist(io, lobbyId) { let messageWrapper = new MessageWrapper(result); lobbySocket.emit('message', await resolveMessage(messageWrapper)); } catch (err) { - console.log(err); + console.error(err); } }); + socket.on('messageEdit', async (context, message, messageId) => { + try { + let row = await bdm.getMessageData(messageId); + if (row.player_id === Number(context.playerId)) { + let result = await bdm.editMessage(messageId, message); + let messageWrapper = new MessageWrapper(result); + lobbySocket.emit('messageEdit', await resolveMessage(messageWrapper)); + } else { + socket.emit('userError', "You are only allowed to edit your messages."); + } + } catch (err) { + console.error(err); + } + }); + socket.on('messageDelete', async (context, messageId) => { + try { + let row = await bdm.getMessageData(messageId); + + if (row.player_id === Number(context.playerId)) { + await bdm.deleteMessage(messageId); + lobbySocket.emit('messageDelete', messageId); + } + } catch (err) { + console.error(err); + } + }); socket.on('fieldToggle', async (context, location) => { let {row, column} = location; let result = await (await (new PlayerWrapper(context.playerId)).grid({lobbyId: lobbyId})) @@ -1609,7 +1667,9 @@ router.init = async (bingoIo, io) => { } else { res.render('bingo/bingo-create', { info: info, - username: await playerWrapper.username() + username: await playerWrapper.username(), + changelog: md.render(globals.changelog), + primaryJoin: (req.query.g && await lobbyWrapper.exists()) }); } }); diff --git a/routes/changelog.js b/routes/changelog.js index d4648a5..849a59e 100644 --- a/routes/changelog.js +++ b/routes/changelog.js @@ -6,12 +6,10 @@ const express = require('express'), md = require('markdown-it')() .use(mdEmoji); -let changelog = fsx.readFileSync('CHANGELOG.md', 'utf-8'); - /* GET home page. */ router.get('/', (req, res) => { let info = req.session.acceptedCookies? null: globals.cookieInfo; - res.render('changelog/changes', { changelog: md.render(changelog), info: info}); + res.render('changelog/changes', { changelog: md.render(globals.changelog), info: info}); }); module.exports = router; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index cb36b68..ee2b0b0 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -262,9 +262,28 @@ getWordsForGridId: addUserMessage: sql: INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *; +# edits a message +# params: +# - {Number} - the id of the message +# - {Number} - the new content of the message +editMessage: + sql: UPDATE bingo.messages SET content = $2 WHERE id = $1 RETURNING *; + # inserts a info message # params: # - {Number} - the id of the lobby # - {String} - the content of the message addInfoMessage: sql: INSERT INTO bingo.messages (type, lobby_id, content) VALUES ('INFO', $1, $2) RETURNING *; + +# returns the data of a message +# params: +# - {Number} - the id of the message +getMessageData: + sql: SELECT * from bingo.messages WHERE id = $1; + +# deletes a message +# params: +# - {Number} - the id of the message +deleteMessage: + sql: DELETE FROM bingo.messages WHERE id = $1; diff --git a/views/bingo/bingo-create.pug b/views/bingo/bingo-create.pug index dd603e1..0da1566 100644 --- a/views/bingo/bingo-create.pug +++ b/views/bingo/bingo-create.pug @@ -3,17 +3,25 @@ extends includes/bingo-layout block content div(id='container-bingo-create') div(id='username-form') + h2 Please enter a username input(id='input-username' type='text' placeholder='Enter your name' value=username maxlength=30 onkeydown='submitOnEnter(event, () => indicateStatus(submitUsername, "#username-status"))') - button( - id='submit-username' - onclick='indicateStatus(submitUsername, "#username-status")') Set Username - div(id='username-status' class='statusIndicator') + if primaryJoin + button( + id='submit-username' + onclick='joinLobby()') Join Lobby + else + button( + id='submit-username' + onclick='indicateStatus(submitUsername, "#submit-username")') Set Username div(id='lobby-form') - button(id='join-lobby' onclick='joinLobby()') Join Lobby - button(id='create-lobby' onclick='createLobby()') Create Lobby + if primaryJoin + button(id='create-lobby' class='inactive' onclick='createLobby()') Create Lobby + else + button(id='create-lobby' onclick='createLobby()') Create Lobby + div(id='changelog')!= changelog include includes/bingo-statusbar diff --git a/views/bingo/includes/bingo-chat.pug b/views/bingo/includes/bingo-chat.pug index 3e362f1..a827953 100644 --- a/views/bingo/includes/bingo-chat.pug +++ b/views/bingo/includes/bingo-chat.pug @@ -2,7 +2,12 @@ div(id='container-chat') style(id='js-style') div(id='chat-content') for message in messages - span.chatMessage(msg-type=message.type msg-id=message.id) + span.chatMessage( + msg-type=message.type + msg-id=message.id + msg-pid=message.playerId + msg-raw=message.content) + if message.type === 'USER' span.chatUsername= `${message.username}: ` span(class=`chatMessageContent ${message.type}`)!= message.htmlContent @@ -10,6 +15,6 @@ div(id='container-chat') id='chat-input' type='text' placeholder='send message' - onkeypress='submitOnEnter(event, () => statusWrap(sendChatMessage))' + onkeypress='onInputKeypress(event)' maxlength="250" autocomplete='off')