Improvements and message editing

- added chat message editing
- added chat message deleting
- changing the username refreshes it in the chat
- fixed chat parsing issues
- fixed mobile lobby style (slightly)
- changed bingo create style to be more easy to understand
- added changelog to bingo create view
- redesigned winner display
pull/24/head
Trivernis 5 years ago
parent 5f0674b977
commit db5650e329

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

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

@ -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<void>}
*/
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<void>}
*/
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<boolean>}
@ -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 = `
<h1>${name} has won!</h1>
<button id="button-lobbyreturn" onclick="window.location.reload()">Return to Lobby!</button>
<div id="container-winner"><h1>${name} has won!</h1>
<p>${isPlayer? 'Congratulations!':'And you lost. How does this make you feel?'}</p>
<button id="button-lobbyreturn" onclick="window.location.reload()">Return to Lobby!</button></div>
`;
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 = `
<span class="chatUsername">${messageObject.author.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`;
else
msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
msgSpan.setAttribute('msg-pid', messageObject.author.id);
} else {
msgSpan.innerHTML = `<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
}
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;

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

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

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

@ -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())
});
}
});

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

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

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

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

Loading…
Cancel
Save