set up websocket and emoji in chat component

This commit is contained in:
Ginger Wong 2020-08-13 09:28:47 -07:00
parent 7a1512ef6b
commit 3814c24cab
7 changed files with 156 additions and 124 deletions

View File

@ -1,10 +1,16 @@
import { h, Component, render } from 'https://unpkg.com/preact?module'; import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module'; import htm from 'https://unpkg.com/htm?module';
// Initialize htm with Preact // Initialize htm with Preact
const html = htm.bind(h); const html = htm.bind(h);
import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button';
import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js';
import Message from './message.js'; import Message from './message.js';
import Websocket, { CALLBACKS } from '../websocket.js';
import { URL_CHAT_HISTORY, URL_CUSTOM_EMOJIS } from '../utils.js';
export default class Chat extends Component { export default class Chat extends Component {
constructor(props, context) { constructor(props, context) {
@ -21,9 +27,27 @@ export default class Chat extends Component {
chatUserNames: [], chatUserNames: [],
} }
this.emojiPicker = null;
this.websocket = null;
this.handleEmojiButtonClick = this.handleEmojiButtonClick.bind(this);
this.handleEmojiSelected = this.handleEmojiSelected.bind(this);
this.getCustomEmojis = this.getCustomEmojis.bind(this);
this.getChatHistory = this.getChatHistory.bind(this);
this.receivedWebsocketMessage = this.receivedWebsocketMessage.bind(this);
this.websocketDisconnected = this.websocketDisconnected.bind(this);
} }
componentDidMount() { componentDidMount() {
/*
- set up websocket
- get emojis
- get chat history
*/
this.setupWebSocket();
this.getChatHistory();
this.getCustomEmojis();
} }
@ -37,6 +61,64 @@ export default class Chat extends Component {
} }
setupWebSocket() {
this.websocket = new Websocket();
this.websocket.addListener(CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, this.receivedWebsocketMessage);
this.websocket.addListener(CALLBACKS.WEBSOCKET_DISCONNECTED, this.websocketDisconnected);
}
// fetch chat history
getChatHistory() {
fetch(URL_CHAT_HISTORY)
.then(response => {
if (!response.ok) {
throw new Error(`Network response was not ok ${response.ok}`);
}
return response.json();
})
.then(data => {
this.setState({
messages: data,
});
// const formattedMessages = data.map(function (message) {
// return new Message(message);
// })
// this.vueApp.messages = formattedMessages.concat(this.vueApp.messages);
})
.catch(error => {
this.handleNetworkingError(`Fetch getChatHistory: ${error}`);
});
}
getCustomEmojis() {
fetch(URL_CUSTOM_EMOJIS)
.then(response => {
if (!response.ok) {
throw new Error(`Network response was not ok ${response.ok}`);
}
return response.json();
})
.then(json => {
this.emojiPicker = new EmojiButton({
zIndex: 100,
theme: 'dark',
custom: json,
initialCategory: 'custom',
showPreview: false,
position: {
top: '50%',
right: '100'
},
});
this.emojiPicker.on('emoji', emoji => {
this.handleEmojiSelected(emoji);
});
})
.catch(error => {
this.handleNetworkingError(`Emoji Fetch: ${error}`);
});
}
sendUsernameChange(oldName, newName, image) { sendUsernameChange(oldName, newName, image) {
const nameChange = { const nameChange = {
type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, type: SOCKET_MESSAGE_TYPES.NAME_CHANGE,
@ -45,10 +127,51 @@ export default class Chat extends Component {
image: image, image: image,
}; };
this.send(nameChange); this.send(nameChange);
} }
handleEmojiButtonClick() {
if (this.emojiPicker) {
this.emojiPicker.togglePicker(this.picker);
}
}
handleEmojiSelected(emoji) {
if (emoji.url) {
const url = location.protocol + "//" + location.host + "/" + emoji.url;
const name = url.split('\\').pop().split('/').pop();
document.querySelector('#message-body-form').innerHTML += "<img class=\"emoji\" alt=\"" + name + "\" src=\"" + url + "\"/>";
} else {
document.querySelector('#message-body-form').innerHTML += emoji.emoji;
}
}
receivedWebsocketMessage(message) {
this.addMessage(message);
// if (model.type === SOCKET_MESSAGE_TYPES.CHAT) {
// const message = new Message(model);
// this.addMessage(message);
// } else if (model.type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) {
// this.addMessage(model);
// }
}
addMessage(message) {
const { messages: curMessages } = this.state;
const existing = curMessages.filter(function (item) {
return item.id === message.id;
})
if (existing.length === 0 || !existing) {
this.setState({
messages: [...curMessages, message],
});
}
}
websocketDisconnected() {
this.websocket = null;
}
render(props, state) { render(props, state) {
const { username, userAvatarImage } = props; const { username } = props;
const { messages } = state; const { messages } = state;
return ( return (
@ -74,7 +197,10 @@ export default class Chat extends Component {
placeholder="Message" placeholder="Message"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white" class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white"
></textarea> ></textarea>
<div id="emoji-button">😏</div> <button
id="emoji-button"
onClick=${this.handleEmojiButtonClick}
>😏</button>
<div id="message-form-actions" class="flex"> <div id="message-form-actions" class="flex">
<span id="message-form-warning" class="text-red-600 text-xs"></span> <span id="message-form-warning" class="text-red-600 text-xs"></span>

View File

@ -1,7 +1,4 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module'; import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module";
import htm from 'https://unpkg.com/htm?module';
// Initialize htm with Preact
const html = htm.bind(h);
import { messageBubbleColorForString } from '../utils/user-colors.js'; import { messageBubbleColorForString } from '../utils/user-colors.js';
import { formatMessageText } from '../utils/chat.js'; import { formatMessageText } from '../utils/chat.js';
@ -14,7 +11,7 @@ export default class Message extends Component {
const { type } = message; const { type } = message;
if (type === SOCKET_MESSAGE_TYPES.CHAT) { if (type === SOCKET_MESSAGE_TYPES.CHAT) {
const { image, author, body, type } = message; const { image, author, body } = message;
const formattedMessage = formatMessageText(body); const formattedMessage = formatMessageText(body);
const avatar = image || generateAvatar(author); const avatar = image || generateAvatar(author);
const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) }; const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) };
@ -43,8 +40,7 @@ export default class Message extends Component {
<span class="font-bold">${oldName}</span> is now known as <span class="font-bold">${newName}</span>. <span class="font-bold">${oldName}</span> is now known as <span class="font-bold">${newName}</span>.
</div> </div>
</div> </div>
` `);
)
} }
} }
} }

View File

@ -1,10 +1,4 @@
import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module";
// import { h, Component, render } from 'https://unpkg.com/preact?module';
// import htm from 'https://unpkg.com/htm?module';
// Initialize htm with Preact
// const html = htm.bind(h);
import UserInfo from './user-info.js'; import UserInfo from './user-info.js';
import Chat from './chat.js'; import Chat from './chat.js';

View File

@ -1,3 +1,4 @@
// DEPRECATE.
import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button' import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button'
fetch('/emoji') fetch('/emoji')
@ -29,6 +30,7 @@ function setupEmojiPickerWithCustomEmoji(customEmoji) {
const trigger = document.querySelector('#emoji-button'); const trigger = document.querySelector('#emoji-button');
trigger.addEventListener('click', () => picker.togglePicker(picker)); trigger.addEventListener('click', () => picker.togglePicker(picker));
picker.on('emoji', emoji => { picker.on('emoji', emoji => {
if (emoji.url) { if (emoji.url) {
const url = location.protocol + "//" + location.host + "/" + emoji.url; const url = location.protocol + "//" + location.host + "/" + emoji.url;

View File

@ -20,9 +20,9 @@ class Message {
formatText() { formatText() {
showdown.setFlavor('github'); showdown.setFlavor('github');
let formattedText = new showdown.Converter({ let formattedText = new showdown.Converter({
emoji: true, emoji: true,
openLinksInNewWindow: true, openLinksInNewWindow: true,
tables: false, tables: false,
simplifiedAutoLink: false, simplifiedAutoLink: false,
literalMidWordUnderscores: true, literalMidWordUnderscores: true,
strikethrough: true, strikethrough: true,
@ -132,8 +132,8 @@ class MessagingInterface {
this.inputMessageAuthor = document.getElementById('self-message-author'); this.inputMessageAuthor = document.getElementById('self-message-author');
this.inputChangeUserName = document.getElementById('username-change-input'); this.inputChangeUserName = document.getElementById('username-change-input');
this.btnUpdateUserName = document.getElementById('button-update-username'); this.btnUpdateUserName = document.getElementById('button-update-username');
this.btnCancelUpdateUsername = document.getElementById('button-cancel-change'); this.btnCancelUpdateUsername = document.getElementById('button-cancel-change');
this.btnSubmitMessage = document.getElementById('button-submit-message'); this.btnSubmitMessage = document.getElementById('button-submit-message');
this.formMessageInput = document.getElementById('message-body-form'); this.formMessageInput = document.getElementById('message-body-form');
@ -146,10 +146,10 @@ class MessagingInterface {
// add events // add events
this.tagChatToggle.addEventListener('click', this.handleChatToggle.bind(this)); this.tagChatToggle.addEventListener('click', this.handleChatToggle.bind(this));
this.textUserInfoDisplay.addEventListener('click', this.handleShowChangeNameForm.bind(this)); this.textUserInfoDisplay.addEventListener('click', this.handleShowChangeNameForm.bind(this));
this.btnUpdateUserName.addEventListener('click', this.handleUpdateUsername.bind(this)); this.btnUpdateUserName.addEventListener('click', this.handleUpdateUsername.bind(this));
this.btnCancelUpdateUsername.addEventListener('click', this.handleHideChangeNameForm.bind(this)); this.btnCancelUpdateUsername.addEventListener('click', this.handleHideChangeNameForm.bind(this));
this.inputChangeUserName.addEventListener('keydown', this.handleUsernameKeydown.bind(this)); this.inputChangeUserName.addEventListener('keydown', this.handleUsernameKeydown.bind(this));
this.formMessageInput.addEventListener('keydown', this.handleMessageInputKeydown.bind(this)); this.formMessageInput.addEventListener('keydown', this.handleMessageInputKeydown.bind(this));
this.formMessageInput.addEventListener('keyup', this.handleMessageInputKeyup.bind(this)); this.formMessageInput.addEventListener('keyup', this.handleMessageInputKeyup.bind(this));
@ -194,7 +194,7 @@ class MessagingInterface {
this.setChatPlaceholderText(); this.setChatPlaceholderText();
} }
handleChatToggle() { handleChatToggle() {
this.chatDisplayed = !this.chatDisplayed; this.chatDisplayed = !this.chatDisplayed;
if (this.chatDisplayed) { if (this.chatDisplayed) {
@ -305,7 +305,7 @@ class MessagingInterface {
this.submitChat(value); this.submitChat(value);
event.preventDefault(); event.preventDefault();
this.prepNewLine = false; this.prepNewLine = false;
return; return;
} }
} }
@ -436,87 +436,3 @@ class MessagingInterface {
} }
export { Message, MessagingInterface } export { Message, MessagingInterface }
function stripTags(str) {
return str.replace(/<\/?[^>]+(>|$)/g, "");
}
function getURLs(str) {
var exp = /((\w+:\/\/\S+)|(\w+[\.:]\w+\S+))[^\s,\.]/ig;
return str.match(exp);
}
function getYoutubeIdFromURL(url) {
try {
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = url.match(regExp);
if (match && match[2].length == 11) {
return match[2];
} else {
return null;
}
} catch (e) {
console.log(e);
return null;
}
}
function getYoutubeEmbedFromID(id) {
return `<iframe class="chat-embed" src="//www.youtube.com/embed/${id}" frameborder="0" allowfullscreen></iframe>`;
}
function getInstagramEmbedFromURL(url) {
const urlObject = new URL(url.replace(/\/$/, ""));
urlObject.pathname += "/embed";
return `<iframe class="chat-embed instagram-embed" height="150px" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
}
function isImage(url) {
const re = /\.(jpe?g|png|gif)$/;
const isImage = re.test(url);
return isImage;
}
function getImageForURL(url) {
return `<a target="_blank" href="${url}"><img class="embedded-image" src="${url}" width="100%" height="150px"/></a>`;
}
// Taken from https://stackoverflow.com/questions/3972014/get-contenteditable-caret-index-position
function getCaretPosition(editableDiv) {
var caretPos = 0,
sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos;
}
// Pieced together from parts of https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div
function setCaretPosition(editableDiv, position) {
var range = document.createRange();
var sel = window.getSelection();
range.selectNode(editableDiv);
range.setStart(editableDiv.childNodes[0], position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}

View File

@ -1,12 +1,12 @@
export const URL_STATUS = `/status`;
const URL_STATUS = `/status`; export const URL_CHAT_HISTORY = `/chat`;
const URL_CHAT_HISTORY = `/chat`; export const URL_CUSTOM_EMOJIS = `/emoji`;
// TODO: This directory is customizable in the config. So we should expose this via the config API. // TODO: This directory is customizable in the config. So we should expose this via the config API.
const URL_STREAM = `/hls/stream.m3u8`; export const URL_STREAM = `/hls/stream.m3u8`;
const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`;
const POSTER_DEFAULT = `/img/logo.png`; export const POSTER_DEFAULT = `/img/logo.png`;
const POSTER_THUMB = `/thumbnail.jpg`; export const POSTER_THUMB = `/thumbnail.jpg`;
export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
@ -126,5 +126,3 @@ export function setVHvar() {
export function doesObjectSupportFunction(object, functionName) { export function doesObjectSupportFunction(object, functionName) {
return typeof object[functionName] === "function"; return typeof object[functionName] === "function";
} }
const DEFAULT_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

View File

@ -4,7 +4,7 @@ const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${loca
const TIMER_WEBSOCKET_RECONNECT = 5000; // ms const TIMER_WEBSOCKET_RECONNECT = 5000; // ms
const CALLBACKS = { export const CALLBACKS = {
RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived', RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived',
WEBSOCKET_CONNECTED: 'websocketConnected', WEBSOCKET_CONNECTED: 'websocketConnected',
WEBSOCKET_DISCONNECTED: 'websocketDisconnected', WEBSOCKET_DISCONNECTED: 'websocketDisconnected',
@ -42,7 +42,7 @@ class Websocket {
} }
} }
// Interface with other components // Interface with other components
// Outbound: Other components can pass an object to `send`. // Outbound: Other components can pass an object to `send`.
@ -51,7 +51,7 @@ class Websocket {
if (!message.type || !SOCKET_MESSAGE_TYPES[message.type]) { if (!message.type || !SOCKET_MESSAGE_TYPES[message.type]) {
console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`); console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`);
} }
const messageJSON = JSON.stringify(message); const messageJSON = JSON.stringify(message);
this.websocket.send(messageJSON); this.websocket.send(messageJSON);
} }
@ -114,7 +114,7 @@ class Websocket {
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
// Send PONGs // Send PONGs
if (model.type === SOCKET_MESSAGE_TYPES.PING) { if (model.type === SOCKET_MESSAGE_TYPES.PING) {
this.sendPong(); this.sendPong();
@ -136,4 +136,4 @@ class Websocket {
}; };
} }
export default Websocket; export default Websocket;