diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 23cfa3b4d..f5f246aa2 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -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'; // Initialize htm with Preact 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 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 { constructor(props, context) { @@ -21,9 +27,27 @@ export default class Chat extends Component { 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() { + /* + - 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) { const nameChange = { type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, @@ -45,10 +127,51 @@ export default class Chat extends Component { image: image, }; 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 += "\"""; + } 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) { - const { username, userAvatarImage } = props; + const { username } = props; const { messages } = state; return ( @@ -74,7 +197,10 @@ export default class Chat extends Component { 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" > -
😏
+
diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index 90105091a..f6084c9dd 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -1,7 +1,4 @@ -import { h, Component, createRef } from 'https://unpkg.com/preact?module'; -import htm from 'https://unpkg.com/htm?module'; -// Initialize htm with Preact -const html = htm.bind(h); +import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; import { messageBubbleColorForString } from '../utils/user-colors.js'; import { formatMessageText } from '../utils/chat.js'; @@ -14,7 +11,7 @@ export default class Message extends Component { const { type } = message; if (type === SOCKET_MESSAGE_TYPES.CHAT) { - const { image, author, body, type } = message; + const { image, author, body } = message; const formattedMessage = formatMessageText(body); const avatar = image || generateAvatar(author); const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) }; @@ -43,8 +40,7 @@ export default class Message extends Component { ${oldName} is now known as ${newName}.
- ` - ) + `); } } } diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index 3489f44ff..e36025044 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -1,10 +1,4 @@ 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 Chat from './chat.js'; diff --git a/webroot/js/emoji.js b/webroot/js/emoji.js index 33ef36b48..23a212828 100644 --- a/webroot/js/emoji.js +++ b/webroot/js/emoji.js @@ -1,3 +1,4 @@ +// DEPRECATE. import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button' fetch('/emoji') @@ -29,6 +30,7 @@ function setupEmojiPickerWithCustomEmoji(customEmoji) { const trigger = document.querySelector('#emoji-button'); trigger.addEventListener('click', () => picker.togglePicker(picker)); + picker.on('emoji', emoji => { if (emoji.url) { const url = location.protocol + "//" + location.host + "/" + emoji.url; diff --git a/webroot/js/message.js b/webroot/js/message.js index f3ca460b0..7d14cba2c 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -20,9 +20,9 @@ class Message { formatText() { showdown.setFlavor('github'); let formattedText = new showdown.Converter({ - emoji: true, - openLinksInNewWindow: true, - tables: false, + emoji: true, + openLinksInNewWindow: true, + tables: false, simplifiedAutoLink: false, literalMidWordUnderscores: true, strikethrough: true, @@ -132,8 +132,8 @@ class MessagingInterface { this.inputMessageAuthor = document.getElementById('self-message-author'); this.inputChangeUserName = document.getElementById('username-change-input'); - this.btnUpdateUserName = document.getElementById('button-update-username'); - this.btnCancelUpdateUsername = document.getElementById('button-cancel-change'); + this.btnUpdateUserName = document.getElementById('button-update-username'); + this.btnCancelUpdateUsername = document.getElementById('button-cancel-change'); this.btnSubmitMessage = document.getElementById('button-submit-message'); this.formMessageInput = document.getElementById('message-body-form'); @@ -146,10 +146,10 @@ class MessagingInterface { // add events this.tagChatToggle.addEventListener('click', this.handleChatToggle.bind(this)); this.textUserInfoDisplay.addEventListener('click', this.handleShowChangeNameForm.bind(this)); - + this.btnUpdateUserName.addEventListener('click', this.handleUpdateUsername.bind(this)); this.btnCancelUpdateUsername.addEventListener('click', this.handleHideChangeNameForm.bind(this)); - + this.inputChangeUserName.addEventListener('keydown', this.handleUsernameKeydown.bind(this)); this.formMessageInput.addEventListener('keydown', this.handleMessageInputKeydown.bind(this)); this.formMessageInput.addEventListener('keyup', this.handleMessageInputKeyup.bind(this)); @@ -194,7 +194,7 @@ class MessagingInterface { this.setChatPlaceholderText(); } - + handleChatToggle() { this.chatDisplayed = !this.chatDisplayed; if (this.chatDisplayed) { @@ -305,7 +305,7 @@ class MessagingInterface { this.submitChat(value); event.preventDefault(); this.prepNewLine = false; - + return; } } @@ -436,87 +436,3 @@ class 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 ``; -} - -function getInstagramEmbedFromURL(url) { - const urlObject = new URL(url.replace(/\/$/, "")); - urlObject.pathname += "/embed"; - return ``; -} - -function isImage(url) { - const re = /\.(jpe?g|png|gif)$/; - const isImage = re.test(url); - return isImage; -} - -function getImageForURL(url) { - return ``; -} - - -// 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); -} \ No newline at end of file diff --git a/webroot/js/utils.js b/webroot/js/utils.js index ef388da94..26da002e1 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,12 +1,12 @@ - -const URL_STATUS = `/status`; -const URL_CHAT_HISTORY = `/chat`; +export const URL_STATUS = `/status`; +export 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. -const URL_STREAM = `/hls/stream.m3u8`; -const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; +export const URL_STREAM = `/hls/stream.m3u8`; +export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; -const POSTER_DEFAULT = `/img/logo.png`; -const POSTER_THUMB = `/thumbnail.jpg`; +export const POSTER_DEFAULT = `/img/logo.png`; +export const POSTER_THUMB = `/thumbnail.jpg`; export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer @@ -126,5 +126,3 @@ export function setVHvar() { export function doesObjectSupportFunction(object, functionName) { return typeof object[functionName] === "function"; } - -const DEFAULT_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; diff --git a/webroot/js/websocket.js b/webroot/js/websocket.js index 6c628d6e5..b1ba7dae1 100644 --- a/webroot/js/websocket.js +++ b/webroot/js/websocket.js @@ -4,7 +4,7 @@ const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${loca const TIMER_WEBSOCKET_RECONNECT = 5000; // ms -const CALLBACKS = { +export const CALLBACKS = { RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived', WEBSOCKET_CONNECTED: 'websocketConnected', WEBSOCKET_DISCONNECTED: 'websocketDisconnected', @@ -42,7 +42,7 @@ class Websocket { } } - + // Interface with other components // Outbound: Other components can pass an object to `send`. @@ -51,7 +51,7 @@ class Websocket { if (!message.type || !SOCKET_MESSAGE_TYPES[message.type]) { console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`); } - + const messageJSON = JSON.stringify(message); this.websocket.send(messageJSON); } @@ -114,7 +114,7 @@ class Websocket { } catch (e) { console.log(e) } - + // Send PONGs if (model.type === SOCKET_MESSAGE_TYPES.PING) { this.sendPong(); @@ -136,4 +136,4 @@ class Websocket { }; } -export default Websocket; \ No newline at end of file +export default Websocket;