From dad802f19a53d296508f01394bc1df264af12a06 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 01:28:25 -0700 Subject: [PATCH 01/35] Initial setup for standalone chat with Preact. - set up standalone static page and message related components - start separating out css into smaller more manageable files - start separating out utils into smaller modular files - renaming some files for consistency --- webroot/js/chat/chat.js | 117 ++++++++++++++++++ webroot/js/chat/message.js | 52 ++++++++ webroot/js/chat/standalone.js | 54 ++++++++ webroot/js/chat/user-info.js | 108 ++++++++++++++++ webroot/js/utils.js | 44 +++---- webroot/js/utils/chat.js | 7 ++ .../js/{chat => utils}/socketMessageTypes.js | 2 +- .../{usercolors.js => utils/user-colors.js} | 10 +- webroot/standalone-chat.html | 22 ++++ webroot/styles/message.css | 87 +++++++++++++ webroot/styles/user-content.css | 102 +++++++++++++++ 11 files changed, 578 insertions(+), 27 deletions(-) create mode 100644 webroot/js/chat/chat.js create mode 100644 webroot/js/chat/message.js create mode 100644 webroot/js/chat/standalone.js create mode 100644 webroot/js/chat/user-info.js create mode 100644 webroot/js/utils/chat.js rename webroot/js/{chat => utils}/socketMessageTypes.js (99%) rename webroot/js/{usercolors.js => utils/user-colors.js} (92%) create mode 100644 webroot/standalone-chat.html create mode 100644 webroot/styles/message.css create mode 100644 webroot/styles/user-content.css diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js new file mode 100644 index 000000000..280abafef --- /dev/null +++ b/webroot/js/chat/chat.js @@ -0,0 +1,117 @@ +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 SOCKET_MESSAGE_TYPES from '../utils/socketMessageTypes.js'; + + +export default class Chat extends Component { + constructor(props, context) { + super(props, context); + + this.messageCharCount = 0; + this.maxMessageLength = 500; + this.maxMessageBuffer = 20; + + this.state = { + inputEnabled: false, + messages: [], + + chatUserNames: [], + } + + } + + componentDidMount() { + + } + + componentDidUpdate(prevProps) { + const { username: prevName } = prevProps; + const { username, userAvatarImage } = this.props; + // if username updated, send a message + if (prevName !== username) { + this.sendUsernameChange(prevName, username, userAvatarImage); + } + + } + + sendUsernameChange(oldName, newName, image) { + const nameChange = { + type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, + oldName: oldName, + newName: newName, + image: image, + }; + this.send(nameChange); + } + + render() { + const { username, userAvatarImage } = this.state; + return ( + html` +
+
+
+ messages... + +
+ + +
+
+ + + + + +
+ + +
+
+
+
+
+ `); + } + +} + + + + + + diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js new file mode 100644 index 000000000..321c66b47 --- /dev/null +++ b/webroot/js/chat/message.js @@ -0,0 +1,52 @@ +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 {messageBubbleColorForString } from '../utils/user-colors.js'; + +export default class Message extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + displayForm: false, + }; + + this.handleKeydown = this.handleKeydown.bind(this); + this.handleDisplayForm = this.handleDisplayForm.bind(this); + this.handleHideForm = this.handleHideForm.bind(this); + this.handleUpdateUsername = this.handleUpdateUsername.bind(this); + } + + + + render(props) { + const { message, type } = props; + const { image, author, text } = message; + + const styles = { + info: { + display: displayForm || narrowSpace ? 'none' : 'flex', + }, + form: { + display: displayForm ? 'flex' : 'none', + }, + }; + + return ( + html` +
+
+ +
+
+

{{ message.author }}

+

+
+
+ `); + } +} diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js new file mode 100644 index 000000000..3489f44ff --- /dev/null +++ b/webroot/js/chat/standalone.js @@ -0,0 +1,54 @@ +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'; + +import { getLocalStorage, generateAvatar, generateUsername } from '../utils.js'; +import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; + +export class StandaloneChat extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + chatEnabled: true, // always true for standalone chat + username: getLocalStorage(KEY_USERNAME) || generateUsername(), + userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), + }; + + this.handleUsernameChange = this.handleUsernameChange.bind(this); + } + + handleUsernameChange(newName, newAvatar) { + this.setState({ + username: newName, + userAvatarImage: newAvatar, + }); + } + + handleChatToggle() { + return; + } + + render(props, state) { + const { username, userAvatarImage } = state; + return ( + html` +
+ <${UserInfo} + username=${username} + userAvatarImage=${userAvatarImage} + handleUsernameChange=${this.handleUsernameChange} + handleChatToggle=${this.handleChatToggle} + /> + <${Chat} username=${username} userAvatarImage=${userAvatarImage} chatEnabled /> +
+ `); + } + +} diff --git a/webroot/js/chat/user-info.js b/webroot/js/chat/user-info.js new file mode 100644 index 000000000..6cd54958f --- /dev/null +++ b/webroot/js/chat/user-info.js @@ -0,0 +1,108 @@ +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 { generateAvatar, setLocalStorage } from '../utils.js'; +import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; + + +export default class UserInfo extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + displayForm: false, + }; + + this.textInput = createRef(); + + this.handleKeydown = this.handleKeydown.bind(this); + this.handleDisplayForm = this.handleDisplayForm.bind(this); + this.handleHideForm = this.handleHideForm.bind(this); + this.handleUpdateUsername = this.handleUpdateUsername.bind(this); + } + + handleDisplayForm() { + this.setState({ + displayForm: true, + }); + } + + handleHideForm() { + this.setState({ + displayForm: false, + }); + } + + handleKeydown(event) { + if (event.keyCode === 13) { // enter + this.handleUpdateUsername(); + } else if (event.keyCode === 27) { // esc + this.handleHideForm(); + } + } + + handleUpdateUsername() { + const { username: curName, handleUsernameChange } = this.props; + let newName = this.textInput.current.value; + newName = newName.trim(); + if (newName !== '' && newName !== curName) { + const newAvatar = generateAvatar(`${newName}${Date.now()}`); + setLocalStorage(KEY_USERNAME, newName); + setLocalStorage(KEY_AVATAR, newAvatar); + if (handleUsernameChange) { + handleUsernameChange(newName, newAvatar); + } + this.handleHideForm(); + } + + } + + render(props, state) { + const { username, userAvatarImage, handleChatToggle } = props; + const { displayForm } = state; + + const narrowSpace = document.body.clientWidth < 640; + const styles = { + info: { + display: displayForm || narrowSpace ? 'none' : 'flex', + }, + form: { + display: displayForm ? 'flex' : 'none', + }, + }; + if (narrowSpace) { + styles.form.display = 'inline-block'; + } + return ( + html` +
+
+
+ + ${username} +
+ +
+ + + +
+
+ +
+ `); + } +} diff --git a/webroot/js/utils.js b/webroot/js/utils.js index ba09ca479..632beb573 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,13 +1,13 @@ -const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; -const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; +export const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; +export const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; -const POSTER_DEFAULT = `${URL_PREFIX}/img/logo.png`; -const POSTER_THUMB = `${URL_PREFIX}/thumbnail.jpg`; +export const POSTER_DEFAULT = `${URL_PREFIX}/img/logo.png`; +export const POSTER_THUMB = `${URL_PREFIX}/thumbnail.jpg`; -const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer +export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer -function getLocalStorage(key) { +export function getLocalStorage(key) { try { return localStorage.getItem(key); } catch (e) { @@ -15,7 +15,7 @@ function getLocalStorage(key) { return null; } -function setLocalStorage(key, value) { +export function setLocalStorage(key, value) { try { if (value !== "" && value !== null) { localStorage.setItem(key, value); @@ -27,12 +27,12 @@ function setLocalStorage(key, value) { return false; } -function clearLocalStorage(key) { +export function clearLocalStorage(key) { localStorage.removeItem(key); } // jump down to the max height of a div, with a slight delay -function jumpToBottom(element) { +export function jumpToBottom(element) { if (!element) return; setTimeout(() => { @@ -45,11 +45,11 @@ function jumpToBottom(element) { } // convert newlines to
s -function addNewlines(str) { +export function addNewlines(str) { return str.replace(/(?:\r\n|\r|\n)/g, '
'); } -function pluralize(string, count) { +export function pluralize(string, count) { if (count === 1) { return string; } else { @@ -60,12 +60,12 @@ function pluralize(string, count) { // Trying to determine if browser is mobile/tablet. // Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent -function hasTouchScreen() { +export function hasTouchScreen() { var hasTouchScreen = false; - if ("maxTouchPoints" in navigator) { + if ("maxTouchPoints" in navigator) { hasTouchScreen = navigator.maxTouchPoints > 0; } else if ("msMaxTouchPoints" in navigator) { - hasTouchScreen = navigator.msMaxTouchPoints > 0; + hasTouchScreen = navigator.msMaxTouchPoints > 0; } else { var mQ = window.matchMedia && matchMedia("(pointer:coarse)"); if (mQ && mQ.media === "(pointer:coarse)") { @@ -85,20 +85,20 @@ function hasTouchScreen() { } // generate random avatar from https://robohash.org -function generateAvatar(hash) { +export function generateAvatar(hash) { const avatarSource = 'https://robohash.org/'; const optionSize = '?size=80x80'; - const optionSet = '&set=set3'; + const optionSet = '&set=set3'; const optionBg = ''; // or &bgset=bg1 or bg2 return avatarSource + hash + optionSize + optionSet + optionBg; } -function generateUsername() { +export function generateUsername() { return `User ${(Math.floor(Math.random() * 42) + 1)}`; } -function secondsToHMMSS(seconds = 0) { +export function secondsToHMMSS(seconds = 0) { const finiteSeconds = Number.isFinite(+seconds) ? Math.abs(seconds) : 0; const hours = Math.floor(finiteSeconds / 3600); @@ -113,13 +113,15 @@ function secondsToHMMSS(seconds = 0) { return hoursString + minString + secsString; } -function setVHvar() { +export function setVHvar() { var vh = window.innerHeight * 0.01; // Then we set the value in the --vh custom property to the root of the document document.documentElement.style.setProperty('--vh', `${vh}px`); console.log("== new vh", vh) } -function doesObjectSupportFunction(object, functionName) { +export function doesObjectSupportFunction(object, functionName) { return typeof object[functionName] === "function"; -} \ No newline at end of file +} + +const DEFAULT_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js new file mode 100644 index 000000000..2bf744ee9 --- /dev/null +++ b/webroot/js/utils/chat.js @@ -0,0 +1,7 @@ +export const KEY_USERNAME = 'owncast_username'; +export const KEY_AVATAR = 'owncast_avatar'; +export const KEY_CHAT_DISPLAYED = 'owncast_chat'; +export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; +export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; +export const CHAT_PLACEHOLDER_TEXT = 'Message'; +export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; diff --git a/webroot/js/chat/socketMessageTypes.js b/webroot/js/utils/socketMessageTypes.js similarity index 99% rename from webroot/js/chat/socketMessageTypes.js rename to webroot/js/utils/socketMessageTypes.js index 54116e1b0..f52b57a3e 100644 --- a/webroot/js/chat/socketMessageTypes.js +++ b/webroot/js/utils/socketMessageTypes.js @@ -8,4 +8,4 @@ export default { PING: 'PING', NAME_CHANGE: 'NAME_CHANGE', PONG: 'PONG' -} \ No newline at end of file +}; diff --git a/webroot/js/usercolors.js b/webroot/js/utils/user-colors.js similarity index 92% rename from webroot/js/usercolors.js rename to webroot/js/utils/user-colors.js index bad61fa39..c1b161f5b 100644 --- a/webroot/js/usercolors.js +++ b/webroot/js/utils/user-colors.js @@ -1,4 +1,4 @@ -function getHashFromString(string) { +export function getHashFromString(string) { let hash = 1; for (let i = 0; i < string.length; i++) { const codepoint = string.charCodeAt(i); @@ -8,7 +8,7 @@ function getHashFromString(string) { return Math.abs(hash); } -function digitsFromNumber(number) { +export function digitsFromNumber(number) { const numberString = number.toString(); let digits = []; @@ -50,7 +50,7 @@ function digitsFromNumber(number) { // return filename + '.svg'; // } -function colorForString(str) { +export function colorForString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { // eslint-disable-next-line @@ -65,7 +65,7 @@ function colorForString(str) { return colour; } -function messageBubbleColorForString(str) { +export function messageBubbleColorForString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { // eslint-disable-next-line @@ -85,4 +85,4 @@ function messageBubbleColorForString(str) { b: parseInt(result[3], 16), } : null; return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.4)'; -} \ No newline at end of file +} diff --git a/webroot/standalone-chat.html b/webroot/standalone-chat.html new file mode 100644 index 000000000..4eb58c1e4 --- /dev/null +++ b/webroot/standalone-chat.html @@ -0,0 +1,22 @@ + + + + + + + + + + + +
+ + + + diff --git a/webroot/styles/message.css b/webroot/styles/message.css new file mode 100644 index 000000000..67b7b77d6 --- /dev/null +++ b/webroot/styles/message.css @@ -0,0 +1,87 @@ + +#messages-container { + overflow: auto; + padding: 1em 0; +} +#message-input-container { + width: 100%; + padding: 1em; +} + +#message-form { + flex-direction: column; + align-items: flex-end; + margin-bottom: 0; +} +#message-body-form { + font-size: 1em; + height: 60px; +} +#message-body-form:disabled{ + opacity: .5; +} +#message-body-form img { + display: inline; + padding-left: 5px; + padding-right: 5px; +} + +#message-body-form .emoji { + width: 40px; +} + +#message-form-actions { + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.message-text img { + display: inline; + padding-left: 5px; + padding-right: 5px; +} + +.message-text .emoji { + width: 60px; +} + + +.message { + padding: .85em; + align-items: flex-start; +} +.message-avatar { + margin-right: .75em; +} +.message-avatar img { + max-width: unset; + height: 3.0em; + width: 3.0em; + padding: 5px; +} + +.message-content { + font-size: .85em; + max-width: 85%; + word-wrap: break-word; +} +.message-content a { + color: #7F9CF5; /* indigo-400 */ +} +.message-content a:hover { + text-decoration: underline; +} + +.message-text iframe { + width: 100%; + height: 170px; + border-radius: 15px; +} + +/* Emoji picker */ +#emoji-button { + margin: 0 .5em; + font-size: 1.5em +} \ No newline at end of file diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css new file mode 100644 index 000000000..7a04ee539 --- /dev/null +++ b/webroot/styles/user-content.css @@ -0,0 +1,102 @@ +.extra-user-content { + padding: 1em 3em 3em 3em; +} + + + +.extra-user-content ol { + list-style: decimal; +} + +.extra-user-content ul { + list-style: unset; +} + +.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 { + color: #111111; + font-weight: 400; } + +.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p { + margin-bottom: 24px; + padding: 0; } + +.extra-user-content h1 { + font-size: 48px; } + +.extra-user-content h2 { + font-size: 36px; + margin: 24px 0 6px; } + +.extra-user-content h3 { + font-size: 24px; } + +.extra-user-content h4 { + font-size: 21px; } + +.extra-user-content h5 { + font-size: 18px; } + +.extra-user-content a { + color: #0099ff; + margin: 0; + padding: 0; + vertical-align: baseline; } + +.extra-user-content ul, .extra-user-content ol { + padding: 0; + margin: 0; } + +.extra-user-content li { + line-height: 24px; } + +.extra-user-content li ul, .extra-user-content li ul { + margin-left: 24px; } + +.extra-user-content p, .extra-user-content ul, .extra-user-content ol { + font-size: 16px; + line-height: 24px; + } + +.extra-user-content pre { + padding: 0px 24px; + max-width: 800px; + white-space: pre-wrap; } + +.extra-user-content code { + font-family: Consolas, Monaco, Andale Mono, monospace; + line-height: 1.5; + font-size: 13px; } + +.extra-user-content aside { + display: block; + float: right; + width: 390px; } + +.extra-user-content blockquote { + margin: 1em 2em; + max-width: 476px; } + +.extra-user-content blockquote p { + color: #666; + max-width: 460px; } + +.extra-user-content hr { + width: 540px; + text-align: left; + margin: 0 auto 0 0; + color: #999; } + +.extra-user-content table { + border-collapse: collapse; + margin: 1em 1em; + border: 1px solid #CCC; } + +.extra-user-content table thead { + background-color: #EEE; } + +.extra-user-content table thead td { + color: #666; } + +.extra-user-content table td { + padding: 0.5em 1em; + border: 1px solid #CCC; } From 64e7809c26cb890d75e6ec9cb3a3160ae38b7c7c Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 01:49:10 -0700 Subject: [PATCH 02/35] separate out message relate utils, create message component --- webroot/js/chat/message.js | 78 ++++++++++++++++++------------------ webroot/js/utils/chat.js | 28 +++++++++++++ webroot/standalone-chat.html | 1 + 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index 321c66b47..e3f38dda2 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -3,50 +3,48 @@ 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 { generateAvatar } from '../utils.js'; +import SOCKET_MESSAGE_TYPES from './chat/socket-message-types.js'; export default class Message extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - displayForm: false, - }; - - this.handleKeydown = this.handleKeydown.bind(this); - this.handleDisplayForm = this.handleDisplayForm.bind(this); - this.handleHideForm = this.handleHideForm.bind(this); - this.handleUpdateUsername = this.handleUpdateUsername.bind(this); - } - - - render(props) { - const { message, type } = props; - const { image, author, text } = message; + const { message } = props; + const { type } = message; - const styles = { - info: { - display: displayForm || narrowSpace ? 'none' : 'flex', - }, - form: { - display: displayForm ? 'flex' : 'none', - }, - }; - - return ( - html` -
-
- + if (type === SOCKET_MESSAGE_TYPES.CHAT) { + const { image, author, body, type } = message; + const formattedMessage = formatMessageText(body); + const avatar = image || generateAvatar(author); + const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) }; + return ( + html` +
+
+ +
+
+

${author}

+

${formattedMessage}

+
+
+ `); + } else if (type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { + const { oldName, newName, image } = message; + return ( + html` +
+ +
+ ${oldName} is now known as ${newName}. +
-
-

{{ message.author }}

-

-
-
- `); + ` + ) + } } } diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 2bf744ee9..939b072c5 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -5,3 +5,31 @@ export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; export const CHAT_PLACEHOLDER_TEXT = 'Message'; export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; + +export function formatMessageText(message) { + showdown.setFlavor('github'); + var markdownToHTML = new showdown.Converter({ + emoji: true, + openLinksInNewWindow: true, + tables: false, + simplifiedAutoLink: false, + literalMidWordUnderscores: true, + strikethrough: true, + ghMentions: false, + }).makeHtml(this.body); + const linked = autoLink(markdownToHTML, { + embed: true, + removeHTTP: true, + linkAttr: { + target: '_blank' + } + }); + const highlighted = highlightUsername(linked); + return addNewlines(highlighted); +} + +function highlightUsername(message) { + const username = document.getElementById('self-message-author').value; + const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); + return message.replace(pattern, '$&'); +} diff --git a/webroot/standalone-chat.html b/webroot/standalone-chat.html index 4eb58c1e4..22b871829 100644 --- a/webroot/standalone-chat.html +++ b/webroot/standalone-chat.html @@ -4,6 +4,7 @@ + From e1e43656921c2f6c60928e20b3bfbe08aefd4455 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 02:08:14 -0700 Subject: [PATCH 03/35] integrate message comp --- webroot/js/chat/chat.js | 38 +++++-------------- webroot/js/chat/message.js | 2 +- ...essageTypes.js => socket-message-types.js} | 0 3 files changed, 11 insertions(+), 29 deletions(-) rename webroot/js/utils/{socketMessageTypes.js => socket-message-types.js} (100%) diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 280abafef..bc000bade 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -3,8 +3,8 @@ import htm from 'https://unpkg.com/htm?module'; // Initialize htm with Preact const html = htm.bind(h); -import SOCKET_MESSAGE_TYPES from '../utils/socketMessageTypes.js'; - +import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; +import Message from './message.js'; export default class Chat extends Component { constructor(props, context) { @@ -47,37 +47,19 @@ export default class Chat extends Component { this.send(nameChange); } - render() { - const { username, userAvatarImage } = this.state; + render(props, state) { + const { username, userAvatarImage } = props; + const { messages } = state; + return ( html`
- messages... - + ${ + messages.map(message => (html`<${Message} message=${message} />`)) + } + messages..
diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index e3f38dda2..90105091a 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -6,7 +6,7 @@ const html = htm.bind(h); import { messageBubbleColorForString } from '../utils/user-colors.js'; import { formatMessageText } from '../utils/chat.js'; import { generateAvatar } from '../utils.js'; -import SOCKET_MESSAGE_TYPES from './chat/socket-message-types.js'; +import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; export default class Message extends Component { render(props) { diff --git a/webroot/js/utils/socketMessageTypes.js b/webroot/js/utils/socket-message-types.js similarity index 100% rename from webroot/js/utils/socketMessageTypes.js rename to webroot/js/utils/socket-message-types.js From 7a1512ef6b00081c1d6e9fda926978e6fef5d226 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 02:43:41 -0700 Subject: [PATCH 04/35] breaking out styles into smaller files; break out chat helper functions into utils --- webroot/js/chat/chat.js | 1 + webroot/js/utils/chat.js | 162 ++++++++++++-- webroot/styles/layout.css | 362 +------------------------------- webroot/styles/message.css | 86 +++++++- webroot/styles/user-content.css | 75 ++++++- 5 files changed, 314 insertions(+), 372 deletions(-) diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index bc000bade..23cfa3b4d 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -74,6 +74,7 @@ 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/utils/chat.js b/webroot/js/utils/chat.js index 939b072c5..dc26d23ca 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -8,7 +8,7 @@ export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; export function formatMessageText(message) { showdown.setFlavor('github'); - var markdownToHTML = new showdown.Converter({ + let formattedText = new showdown.Converter({ emoji: true, openLinksInNewWindow: true, tables: false, @@ -16,20 +16,158 @@ export function formatMessageText(message) { literalMidWordUnderscores: true, strikethrough: true, ghMentions: false, - }).makeHtml(this.body); - const linked = autoLink(markdownToHTML, { - embed: true, - removeHTTP: true, - linkAttr: { - target: '_blank' - } - }); - const highlighted = highlightUsername(linked); - return addNewlines(highlighted); + }).makeHtml(message); + + formattedText = linkify(formattedText, message); + formattedText = highlightUsername(formattedText); + + return addNewlines(formattedText); } function highlightUsername(message) { const username = document.getElementById('self-message-author').value; - const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); + const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); return message.replace(pattern, '$&'); } + +function linkify(text, rawText) { + const urls = getURLs(stripTags(rawText)); + if (urls) { + urls.forEach(function (url) { + let linkURL = url; + + // Add http prefix if none exist in the URL so it actually + // will work in an anchor tag. + if (linkURL.indexOf('http') === -1) { + linkURL = 'http://' + linkURL; + } + + // Remove the protocol prefix in the display URLs just to make + // things look a little nicer. + const displayURL = url.replace(/(^\w+:|^)\/\//, ''); + const link = `${displayURL}`; + text = text.replace(url, link); + + if (getYoutubeIdFromURL(url)) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += '
'; + } + + const youtubeID = getYoutubeIdFromURL(url); + text += getYoutubeEmbedFromID(youtubeID); + } else if (url.indexOf('instagram.com/p/') > -1) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += `
`; + } + text += getInstagramEmbedFromURL(url); + } else if (isImage(url)) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += `
`; + } + text += getImageForURL(url); + } + }.bind(this)); + } + return text; +} + +function isTextJustURLs(text, urls) { + for (var i = 0; i < urls.length; i++) { + const url = urls[i]; + if (stripTags(text) === url) { + return true; + } + } + return false; +} + + +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 +export 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 +export 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); +} diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index b179dfef1..611408072 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -23,9 +23,9 @@ a:hover { } -.visually-hidden { +.visually-hidden { position: absolute !important; - height: 1px; + height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ @@ -111,7 +111,7 @@ footer span { flex-direction: row; justify-content: space-between; - + } #stream-info span { font-size: .7em; @@ -128,23 +128,7 @@ footer span { /* ************************************************8 */ -.user-content { - padding: 3em; - display: flex; - flex-direction: row; -} -.user-content .user-image { - padding: 1em; - margin-right: 2em; - min-width: var(--user-image-width); - width: var(--user-image-width); - height: var(--user-image-width); - max-height: var(--user-image-width); - background-repeat: no-repeat; - background-position: center center; - background-size: calc(var(--user-image-width) - 1em); -} /* .user-image img { display: inline-block; @@ -156,58 +140,7 @@ footer span { } h2 { - font-size: 3em; -} -.user-content-header { - margin-bottom: 2em; -} - -.tag-list { - flex-direction: row; - margin: 1em 0; -} -.tag-list li { - font-size: .75em; - text-transform: uppercase; - margin-right: .75em; - padding: .5em; -} - - -.social-list { - flex-direction: row; - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; -} -.social-list .follow-label { - font-weight: bold; - font-size: .75em; - margin-right: .5em; - text-transform: uppercase; -} - -.user-social-item { - display: flex; - justify-content: flex-start; - align-items: center; - margin-right: -.25em; -} -.user-social-item .platform-icon { - --icon-width: 40px; - height: var(--icon-width); - width: var(--icon-width); - background-image: url(../img/social-icons.gif); - background-repeat: no-repeat; - background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); - transform: scale(.65); -} - -.user-social-item.use-default .platform-label { - font-size: .7em; - text-transform: uppercase; - display: inline-block; - max-width: 10em; + font-size: 3em; } @@ -281,8 +214,8 @@ h2 { margin-top: var(--header-height); background-position: center center; background-repeat: no-repeat; - - background-size: 30%; + + background-size: 30%; } .owncast-video-container { @@ -360,81 +293,6 @@ h2 { } -#messages-container { - overflow: auto; - padding: 1em 0; -} -#message-input-container { - width: 100%; - padding: 1em; -} - -#message-form { - flex-direction: column; - align-items: flex-end; - margin-bottom: 0; -} -#message-body-form { - font-size: 1em; - height: 60px; -} -#message-body-form:disabled{ - opacity: .5; -} -#message-body-form img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} - -#message-body-form .emoji { - width: 40px; -} - -#message-form-actions { - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.message-text img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} - -.message-text .emoji { - width: 60px; -} - -/* ************************************************8 */ - -.message { - padding: .85em; - align-items: flex-start; -} -.message-avatar { - margin-right: .75em; -} -.message-avatar img { - max-width: unset; - height: 3.0em; - width: 3.0em; - padding: 5px; -} - -.message-content { - font-size: .85em; - max-width: 85%; - word-wrap: break-word; -} -.message-content a { - color: #7F9CF5; /* indigo-400 */ -} -.message-content a:hover { - text-decoration: underline; -} /* ************************************************8 */ @@ -452,7 +310,7 @@ h2 { --right-col-width: 20em; --user-image-width: 6em; } - + #chat-container { width: var(--right-col-width); } @@ -504,21 +362,6 @@ h2 { } } -/* try not making the video fixed position for now */ -@media (min-height: 861px) { - /* main { - position: fixed; - z-index: 9; - width: 100%; - } - #user-content { - margin-top: calc(var(--video-container-height) + var(--header-height) + 2em) - } */ -} - - - - @@ -530,194 +373,3 @@ h2 { flex-direction: column; } } - -.extra-user-content { - padding: 1em 3em 3em 3em; -} - -.extra-user-content ol { - list-style: decimal; -} - -.extra-user-content ul { - list-style: unset; -} - -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 { - color: #111111; - font-weight: 400; } - -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p { - margin-bottom: 24px; - padding: 0; } - -.extra-user-content h1 { - font-size: 48px; } - -.extra-user-content h2 { - font-size: 36px; - margin: 24px 0 6px; } - -.extra-user-content h3 { - font-size: 24px; } - -.extra-user-content h4 { - font-size: 21px; } - -.extra-user-content h5 { - font-size: 18px; } - -.extra-user-content a { - color: #0099ff; - margin: 0; - padding: 0; - vertical-align: baseline; } - -.extra-user-content ul, .extra-user-content ol { - padding: 0; - margin: 0; } - -.extra-user-content li { - line-height: 24px; } - -.extra-user-content li ul, .extra-user-content li ul { - margin-left: 24px; } - -.extra-user-content p, .extra-user-content ul, .extra-user-content ol { - font-size: 16px; - line-height: 24px; - } - -.extra-user-content pre { - padding: 0px 24px; - max-width: 800px; - white-space: pre-wrap; } - -.extra-user-content code { - font-family: Consolas, Monaco, Andale Mono, monospace; - line-height: 1.5; - font-size: 13px; } - -.extra-user-content aside { - display: block; - float: right; - width: 390px; } - -.extra-user-content blockquote { - margin: 1em 2em; - max-width: 476px; } - -.extra-user-content blockquote p { - color: #666; - max-width: 460px; } - -.extra-user-content hr { - width: 540px; - text-align: left; - margin: 0 auto 0 0; - color: #999; } - -.extra-user-content table { - border-collapse: collapse; - margin: 1em 1em; - border: 1px solid #CCC; } - -.extra-user-content table thead { - background-color: #EEE; } - -.extra-user-content table thead td { - color: #666; } - -.extra-user-content table td { - padding: 0.5em 1em; - border: 1px solid #CCC; } - -.message-text iframe { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text .instagram-embed { - height: 314px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} - -.message-text .embedded-image { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} - -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} -.message-text .embedded-image { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} -.message-text .highlighted { - color: orange; - font-weight: 400; - font-size: 14px; - -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} - -/* -The chat input has a fake placeholder that is styled below. -It pulls the placeholder text from the div's placeholder attribute. -But really it's just the innerHTML content. -*/ - -/* If the div is empty then show the placeholder */ -#message-body-form:empty:before{ - content: attr(placeholder); - pointer-events: none; - display: block; /* For Firefox */ - - /* Style the div's placeholder text color */ - color: rgba(0, 0, 0, 0.5); -} - -/* When chat is enabled (contenteditable=true) */ -#message-body-form[contenteditable=true]:before { - opacity: 1.0; -} - - -/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ -#message-body-form[contenteditable=false] { - opacity: 0.6; -} - diff --git a/webroot/styles/message.css b/webroot/styles/message.css index 67b7b77d6..1dd2021ef 100644 --- a/webroot/styles/message.css +++ b/webroot/styles/message.css @@ -48,6 +48,7 @@ } + .message { padding: .85em; align-items: flex-start; @@ -74,14 +75,93 @@ text-decoration: underline; } + .message-text iframe { width: 100%; height: 170px; border-radius: 15px; } +.message-text .instagram-embed { + height: 314px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} /* Emoji picker */ #emoji-button { - margin: 0 .5em; - font-size: 1.5em -} \ No newline at end of file + position: relative; + top: -65px; + right: 10px; + cursor: pointer; +} + +.message-text .embedded-image { + width: 100%; + height: 170px; + border-radius: 15px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} + +/* Emoji picker */ +#emoji-button { + position: relative; + top: -65px; + right: 10px; + cursor: pointer; +} +.message-text .embedded-image { + width: 100%; + height: 170px; + border-radius: 15px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} +.message-text .highlighted { + color: orange; + font-weight: 400; + font-size: 14px; + +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} + +/* +The chat input has a fake placeholder that is styled below. +It pulls the placeholder text from the div's placeholder attribute. +But really it's just the innerHTML content. +*/ + +/* If the div is empty then show the placeholder */ +#message-body-form:empty:before{ + content: attr(placeholder); + pointer-events: none; + display: block; /* For Firefox */ + + /* Style the div's placeholder text color */ + color: rgba(0, 0, 0, 0.5); +} + +/* When chat is enabled (contenteditable=true) */ +#message-body-form[contenteditable=true]:before { + opacity: 1.0; +} + + +/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ +#message-body-form[contenteditable=false] { + opacity: 0.6; +} + diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css index 7a04ee539..dea858f08 100644 --- a/webroot/styles/user-content.css +++ b/webroot/styles/user-content.css @@ -1,8 +1,79 @@ -.extra-user-content { - padding: 1em 3em 3em 3em; +.user-content { + padding: 3em; + + display: flex; + flex-direction: row; +} +.user-content .user-image { + padding: 1em; + margin-right: 2em; + min-width: var(--user-image-width); + width: var(--user-image-width); + height: var(--user-image-width); + max-height: var(--user-image-width); + background-repeat: no-repeat; + background-position: center center; + background-size: calc(var(--user-image-width) - 1em); +} + +.user-content-header { + margin-bottom: 2em; +} + +.tag-list { + flex-direction: row; + margin: 1em 0; +} +.tag-list li { + font-size: .75em; + text-transform: uppercase; + margin-right: .75em; + padding: .5em; } +.social-list { + flex-direction: row; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; +} +.social-list .follow-label { + font-weight: bold; + font-size: .75em; + margin-right: .5em; + text-transform: uppercase; +} + +.user-social-item { + display: flex; + justify-content: flex-start; + align-items: center; + margin-right: -.25em; +} +.user-social-item .platform-icon { + --icon-width: 40px; + height: var(--icon-width); + width: var(--icon-width); + background-image: url(../img/social-icons.gif); + background-repeat: no-repeat; + background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); + transform: scale(.65); +} + +.user-social-item.use-default .platform-label { + font-size: .7em; + text-transform: uppercase; + display: inline-block; + max-width: 10em; +} + + + + +.extra-user-content { + padding: 1em 3em 3em 3em; +} .extra-user-content ol { list-style: decimal; From 3814c24cabbbae549b7db40eca7c071146f2504a Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 09:28:47 -0700 Subject: [PATCH 05/35] set up websocket and emoji in chat component --- webroot/js/chat/chat.js | 134 +++++++++++++++++++++++++++++++++- webroot/js/chat/message.js | 10 +-- webroot/js/chat/standalone.js | 6 -- webroot/js/emoji.js | 2 + webroot/js/message.js | 102 +++----------------------- webroot/js/utils.js | 16 ++-- webroot/js/websocket.js | 10 +-- 7 files changed, 156 insertions(+), 124 deletions(-) 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; From 63d7671fed02e7e7555ac7b0e319768023f36aee Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Fri, 14 Aug 2020 04:19:19 -0700 Subject: [PATCH 06/35] progress wip. separated out chat input component and its respective methods. --- webroot/js/app.js | 12 +- webroot/js/chat/chat-input.js | 211 ++++++++++++++++++++++++++++++ webroot/js/chat/chat.js | 236 ++++++++++++++++++++-------------- webroot/js/chat/message.js | 11 +- webroot/js/chat/standalone.js | 13 +- webroot/js/message.js | 2 +- webroot/js/utils/chat.js | 18 ++- webroot/js/websocket.js | 2 +- webroot/standalone-chat.html | 2 +- 9 files changed, 392 insertions(+), 115 deletions(-) create mode 100644 webroot/js/chat/chat-input.js diff --git a/webroot/js/app.js b/webroot/js/app.js index 1c5a8d9b5..6141b3e8c 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -1,6 +1,6 @@ import Websocket from './websocket.js'; import { MessagingInterface, Message } from './message.js'; -import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; +import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; import { OwncastPlayer } from './player.js'; const MESSAGE_OFFLINE = 'Stream is offline.'; @@ -18,7 +18,7 @@ const TIMER_STREAM_DURATION_COUNTER = 1000; class Owncast { constructor() { - this.player; + this.player; this.configData; this.vueApp; @@ -67,7 +67,7 @@ class Owncast { streamStatus: MESSAGE_OFFLINE, // Default state. viewerCount: 0, isOnline: false, - + // from config appVersion: '', extraUserContent: '', @@ -260,7 +260,7 @@ class Owncast { } } }; - + // update vueApp.streamStatus text when online setCurrentStreamDuration() { // Default to something @@ -272,7 +272,7 @@ class Owncast { } this.vueApp.streamStatus = `${MESSAGE_ONLINE} ${streamDurationString}.` } - + handleNetworkingError(error) { console.log(`>>> App Error: ${error}`) }; @@ -329,4 +329,4 @@ class Owncast { }; }; -export default Owncast; \ No newline at end of file +export default Owncast; diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js new file mode 100644 index 000000000..421b6a2aa --- /dev/null +++ b/webroot/js/chat/chat-input.js @@ -0,0 +1,211 @@ +import { h, Component, createRef } from 'https://unpkg.com/preact?module'; +import htm from 'https://unpkg.com/htm?module'; +const html = htm.bind(h); + +import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button'; + +import { URL_CUSTOM_EMOJIS } from '../utils.js'; +import { generatePlaceholderText } from '../utils/chat.js'; + +export default class ChatInput extends Component { + constructor(props, context) { + super(props, context); + this.formMessageInput = createRef(); + + this.messageCharCount = 0; + this.maxMessageLength = 500; + this.maxMessageBuffer = 20; + + this.emojiPicker = null; + + this.prepNewLine = false; + + this.handleEmojiButtonClick = this.handleEmojiButtonClick.bind(this); + this.handleEmojiSelected = this.handleEmojiSelected.bind(this); + this.getCustomEmojis = this.getCustomEmojis.bind(this); + + this.handleMessageInputKeydown = this.handleMessageInputKeydown.bind(this); + this.handleMessageInputKeyup = this.handleMessageInputKeyup.bind(this); + this.handleMessageInputBlur = this.handleMessageInputBlur.bind(this); + this.handleMessageInput = this.handleMessageInput.bind(this); + } + + componentDidMount() { + this.getCustomEmojis(); + } + + 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}`); + }); + } + + handleEmojiButtonClick() { + if (this.emojiPicker) { + this.emojiPicker.togglePicker(this.emojiPicker); + } + } + + 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; + } + } + + // autocomplete user names + autoCompleteNames() { + const { chatUserNames } = this.props; + const rawValue = this.formMessageInput.innerHTML; + const position = getCaretPosition(this.formMessageInput); + const at = rawValue.lastIndexOf('@', position - 1); + + if (at === -1) { + return false; + } + + var partial = rawValue.substring(at + 1, position).trim(); + + if (partial === this.suggestion) { + partial = this.partial; + } else { + this.partial = partial; + } + + const possibilities = chatUsernames.filter(function (username) { + return username.toLowerCase().startsWith(partial.toLowerCase()); + }); + + if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { + this.completionIndex = 0; + } + + if (possibilities.length > 0) { + this.suggestion = possibilities[this.completionIndex]; + + // TODO: Fix the space not working. I'm guessing because the DOM ignores spaces and it requires a nbsp or something? + this.formMessageInput.innerHTML = rawValue.substring(0, at + 1) + this.suggestion + ' ' + rawValue.substring(position); + setCaretPosition(this.formMessageInput, at + this.suggestion.length + 2); + } + + return true; + } + + handleMessageInputKeydown(event) { + console.log("========this.formMessageInput", this.formMessageInput) + const okCodes = [37,38,39,40,16,91,18,46,8]; + const value = this.formMessageInput.innerHTML.trim(); + const numCharsLeft = this.maxMessageLength - value.length; + if (event.keyCode === 13) { // enter + if (!this.prepNewLine) { + this.submitChat(value); + event.preventDefault(); + this.prepNewLine = false; + + return; + } + } + if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift + this.prepNewLine = true; + } + if (event.keyCode === 9) { // tab + if (this.autoCompleteNames()) { + event.preventDefault(); + + // value could have been changed, update variables + value = this.formMessageInput.innerHTML.trim(); + numCharsLeft = this.maxMessageLength - value.length; + } + } + + // if (numCharsLeft <= this.maxMessageBuffer) { + // this.tagMessageFormWarning.innerText = `${numCharsLeft} chars left`; + // if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) { + // event.preventDefault(); + // return; + // } + // } else { + // this.tagMessageFormWarning.innerText = ''; + // } + } + + handleMessageInputKeyup(event) { + if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift + this.prepNewLine = false; + } + } + + handleMessageInputBlur(event) { + this.prepNewLine = false; + } + + handleMessageInput(event) { + // event.target.value + } + + // setChatPlaceholderText() { + // // NOTE: This is a fake placeholder that is being styled via CSS. + // // You can't just set the .placeholder property because it's not a form element. + // const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); + // const placeholderText = hasSentFirstChatMessage ? CHAT_PLACEHOLDER_TEXT : CHAT_INITIAL_PLACEHOLDER_TEXT; + // this.formMessageInput.setAttribute("placeholder", placeholderText); + // } + + render(props, state) { + const { contenteditable, hasSentFirstChatMessage } = props; + const emojiButtonStyle = { + display: this.emojiPicker ? 'block' : 'none', + }; + + const placeholderText = generatePlaceholderText(contenteditable, hasSentFirstChatMessage); + + return ( + html` +
+
+ +
+ `); + } + +} diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index f5f246aa2..c3c1fc484 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -3,52 +3,49 @@ 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 { getLocalStorage, setLocalStorage } from '../utils.js'; +import { KEY_CHAT_FIRST_MESSAGE_SENT } from '../utils/chat.js'; import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; import Message from './message.js'; -import Websocket, { CALLBACKS } from '../websocket.js'; +import ChatInput from './chat-input.js'; +import { CALLBACKS } from '../websocket.js'; -import { URL_CHAT_HISTORY, URL_CUSTOM_EMOJIS } from '../utils.js'; + +import { URL_CHAT_HISTORY, setVHvar, hasTouchScreen } from '../utils.js'; export default class Chat extends Component { constructor(props, context) { super(props, context); - this.messageCharCount = 0; - this.maxMessageLength = 500; - this.maxMessageBuffer = 20; - this.state = { - inputEnabled: false, + inputEnabled: true, + hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT), + inputValue: '', + inputWarning: '', messages: [], 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); + + this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this); } componentDidMount() { - /* - - set up websocket - - get emojis - - get chat history - */ - this.setupWebSocket(); + this.setupWebSocketCallbacks(); this.getChatHistory(); - this.getCustomEmojis(); + if (hasTouchScreen()) { + setVHvar(); + window.addEventListener("orientationchange", setVHvar); + // this.tagAppContainer.classList.add('touch-screen'); + } } componentDidUpdate(prevProps) { @@ -58,13 +55,15 @@ export default class Chat extends Component { if (prevName !== username) { this.sendUsernameChange(prevName, username, userAvatarImage); } - } - setupWebSocket() { - this.websocket = new Websocket(); - this.websocket.addListener(CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, this.receivedWebsocketMessage); - this.websocket.addListener(CALLBACKS.WEBSOCKET_DISCONNECTED, this.websocketDisconnected); + setupWebSocketCallbacks() { + this.websocket = this.props.websocket; + if (this.websocket) { + this.websocket.addListener(CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, this.receivedWebsocketMessage); + this.websocket.addListener(CALLBACKS.WEBSOCKET_DISCONNECTED, this.websocketDisconnected); + } + } // fetch chat history @@ -77,6 +76,7 @@ export default class Chat extends Component { return response.json(); }) .then(data => { + console.log("=====chat history data",data) this.setState({ messages: data, }); @@ -86,38 +86,11 @@ export default class Chat extends Component { // this.vueApp.messages = formattedMessages.concat(this.vueApp.messages); }) .catch(error => { - this.handleNetworkingError(`Fetch getChatHistory: ${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 = { @@ -126,24 +99,10 @@ export default class Chat extends Component { newName: newName, image: image, }; - this.send(nameChange); + this.websocket.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); @@ -155,24 +114,117 @@ export default class Chat extends Component { // } } + // if incoming message has same id as existing message, don't add it 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({ + const newState = { messages: [...curMessages, message], - }); + }; + const updatedChatUserNames = this.updateAuthorList(message); + if (updatedChatUserNames.length) { + newState.chatUserNames = [...updatedChatUserNames]; + } + this.setState(newState); } + + // todo - jump to bottom + // jumpToBottom(this.scrollableMessagesContainer); } websocketDisconnected() { - this.websocket = null; + // this.websocket = null; + this.disableChat() } + handleSubmitChatButton(event) { + const { inputValue } = this.state; + var value = inputValue.trim(); + if (value) { + this.submitChat(value); + event.preventDefault(); + return false; + } + event.preventDefault(); + return false; + } + + submitChat(content) { + if (!content) { + return; + } + const { username, userAvatarImage } = this.props; + var message = new Message({ + body: content, + author: username, + image: userAvatarImage, + type: SOCKET_MESSAGE_TYPES.CHAT, + }); + this.websocket.send(message); + + // clear out things. + const newStates = { + inputValue: '', + inputWarning: '', + }; + // this.formMessageInput.innerHTML = ''; + // this.tagMessageFormWarning.innerText = ''; + + // const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); + if (!this.state.hasSentFirstChatMessage) { + newStates.hasSentFirstChatMessage = true; + setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); + // this.setChatPlaceholderText(); + } + this.setState(newStates); + } + + disableChat() { + this.setState({ + inputEnabled: false, + }); + // if (this.formMessageInput) { + // this.formMessageInput.contentEditable = false; + // this.formMessageInput.innerHTML = ''; + // this.formMessageInput.setAttribute("placeholder", CHAT_PLACEHOLDER_OFFLINE); + // } + } + + enableChat() { + this.setState({ + inputEnabled: true, + }); + // if (this.formMessageInput) { + // this.formMessageInput.contentEditable = true; + // this.setChatPlaceholderText(); + // } + } + + updateAuthorList(message) { + const { type } = message; + const username = ''; + const nameList = this.state.chatUserNames; + + if ( + type === SOCKET_MESSAGE_TYPES.CHAT && + !nameList.includes(message.author) + ) { + return nameList.push(message.author); + } else if (type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { + const { oldName, newName } = message; + const oldNameIndex = nameList.indexOf(oldName); + return nameList.splice(oldNameIndex, 1, newName); + } + return []; + } + + + render(props, state) { const { username } = props; - const { messages } = state; + const { messages, inputEnabled, hasSentFirstChatMessage, chatUserNames, inputWarning } = state; return ( html` @@ -180,37 +232,35 @@ export default class Chat extends Component {
${ - messages.map(message => (html`<${Message} message=${message} />`)) + messages.map(message => (html`<${Message} message=${message} username=${username} />`)) } messages..
-
+ - + - - + <${ChatInput} + contenteditable=${inputEnabled} + hasSentFirstChatMessage=${hasSentFirstChatMessage} + chatUserNames=${chatUserNames} + handleSubmitForm=${this.handleSubmitChatButton} + />
- + ${inputWarning}
-
+
@@ -218,9 +268,3 @@ export default class Chat extends Component { } } - - - - - - diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index f6084c9dd..6de5afa82 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -7,12 +7,12 @@ import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; export default class Message extends Component { render(props) { - const { message } = props; + const { message, username } = props; const { type } = message; if (type === SOCKET_MESSAGE_TYPES.CHAT) { const { image, author, body } = message; - const formattedMessage = formatMessageText(body); + const formattedMessage = formatMessageText(body, username); const avatar = image || generateAvatar(author); const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) }; return ( @@ -26,7 +26,12 @@ export default class Message extends Component {

${author}

-

${formattedMessage}

+
`); diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index e36025044..88c710ef7 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -1,20 +1,23 @@ import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; import UserInfo from './user-info.js'; import Chat from './chat.js'; +import Websocket from '../websocket.js'; import { getLocalStorage, generateAvatar, generateUsername } from '../utils.js'; import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; -export class StandaloneChat extends Component { +export default class StandaloneChat extends Component { constructor(props, context) { super(props, context); this.state = { + websocket: new Websocket(), chatEnabled: true, // always true for standalone chat username: getLocalStorage(KEY_USERNAME) || generateUsername(), userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), }; + this.websocket = null; this.handleUsernameChange = this.handleUsernameChange.bind(this); } @@ -30,7 +33,7 @@ export class StandaloneChat extends Component { } render(props, state) { - const { username, userAvatarImage } = state; + const { username, userAvatarImage, websocket } = state; return ( html`
@@ -40,7 +43,11 @@ export class StandaloneChat extends Component { handleUsernameChange=${this.handleUsernameChange} handleChatToggle=${this.handleChatToggle} /> - <${Chat} username=${username} userAvatarImage=${userAvatarImage} chatEnabled /> + <${Chat} + websocket=${websocket} + username=${username} + userAvatarImage=${userAvatarImage} + chatEnabled />
`); } diff --git a/webroot/js/message.js b/webroot/js/message.js index 7d14cba2c..2422fdfd8 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -1,4 +1,4 @@ -import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; +import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; const KEY_USERNAME = 'owncast_username'; const KEY_AVATAR = 'owncast_avatar'; diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index dc26d23ca..37d75a7b8 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -1,3 +1,5 @@ +import { addNewlines } from '../utils.js'; + export const KEY_USERNAME = 'owncast_username'; export const KEY_AVATAR = 'owncast_avatar'; export const KEY_CHAT_DISPLAYED = 'owncast_chat'; @@ -6,7 +8,7 @@ export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account nece export const CHAT_PLACEHOLDER_TEXT = 'Message'; export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; -export function formatMessageText(message) { +export function formatMessageText(message, username) { showdown.setFlavor('github'); let formattedText = new showdown.Converter({ emoji: true, @@ -19,13 +21,13 @@ export function formatMessageText(message) { }).makeHtml(message); formattedText = linkify(formattedText, message); - formattedText = highlightUsername(formattedText); + formattedText = highlightUsername(formattedText, username); return addNewlines(formattedText); } -function highlightUsername(message) { - const username = document.getElementById('self-message-author').value; +function highlightUsername(message, username) { + // const username = document.getElementById('self-message-author').value; const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); return message.replace(pattern, '$&'); } @@ -171,3 +173,11 @@ export function setCaretPosition(editableDiv, position) { sel.removeAllRanges(); sel.addRange(range); } + + +export function generatePlaceholderText(isEnabled, hasSentFirstChatMessage) { + if (isEnabled) { + return hasSentFirstChatMessage ? CHAT_PLACEHOLDER_TEXT : CHAT_INITIAL_PLACEHOLDER_TEXT; + } + return CHAT_PLACEHOLDER_OFFLINE; +} diff --git a/webroot/js/websocket.js b/webroot/js/websocket.js index b1ba7dae1..3b558f99f 100644 --- a/webroot/js/websocket.js +++ b/webroot/js/websocket.js @@ -1,4 +1,4 @@ -import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; +import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; diff --git a/webroot/standalone-chat.html b/webroot/standalone-chat.html index 22b871829..5cde0a98c 100644 --- a/webroot/standalone-chat.html +++ b/webroot/standalone-chat.html @@ -14,7 +14,7 @@ diff --git a/webroot/styles/message.css b/webroot/styles/chat.css similarity index 99% rename from webroot/styles/message.css rename to webroot/styles/chat.css index 1dd2021ef..c6b33a4b6 100644 --- a/webroot/styles/message.css +++ b/webroot/styles/chat.css @@ -165,3 +165,5 @@ But really it's just the innerHTML content. opacity: 0.6; } + + diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index 611408072..9eb6a6384 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -1,3 +1,8 @@ +/* +Overall layout styles for all of owncast app. + +*/ + /* variables */ :root { --header-height: 3.5em; diff --git a/webroot/styles/standalone-chat.css b/webroot/styles/standalone-chat.css new file mode 100644 index 000000000..898ece926 --- /dev/null +++ b/webroot/styles/standalone-chat.css @@ -0,0 +1,4 @@ +/* +The styles in this file mostly ovveride those coming fro chat.css + +*/ From d7b805826416a85d678976ff5f1dc3882006d8ff Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Wed, 19 Aug 2020 00:16:35 -0700 Subject: [PATCH 12/35] make edits for a messages-only view of the chat module --- webroot/js/chat/chat-input.js | 38 +++++----- webroot/js/chat/chat.js | 30 ++++---- webroot/js/chat/standalone.js | 26 ++++++- webroot/standalone-chat.html | 7 +- webroot/styles/chat.css | 135 ++++++++++++++++++---------------- 5 files changed, 135 insertions(+), 101 deletions(-) diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index 76a0e7a36..198501f5c 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -18,6 +18,7 @@ export default class ChatInput extends Component { constructor(props, context) { super(props, context); this.formMessageInput = createRef(); + this.emojiPickerButton = createRef(); this.messageCharCount = 0; this.maxMessageLength = 500; @@ -65,10 +66,7 @@ export default class ChatInput extends Component { custom: json, initialCategory: 'custom', showPreview: false, - position: { - top: '50%', - right: '100' - }, + position: 'top' }); this.emojiPicker.on('emoji', emoji => { this.handleEmojiSelected(emoji); @@ -81,7 +79,7 @@ export default class ChatInput extends Component { handleEmojiButtonClick() { if (this.emojiPicker) { - this.emojiPicker.togglePicker(this.emojiPicker); + this.emojiPicker.togglePicker(this.emojiPickerButton.current); } } @@ -253,20 +251,26 @@ export default class ChatInput extends Component { onPaste=${this.handlePaste} /> -
${inputWarning} - + +
+ + + +
`); diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 8eed44ed5..54caabc4c 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -128,18 +128,6 @@ export default class Chat extends Component { this.disableChat() } - // handleSubmitChatButton(event) { - // const { inputValue } = this.state; - // var value = inputValue.trim(); - // if (value) { - // this.submitChat(value); - // event.preventDefault(); - // return false; - // } - // event.preventDefault(); - // return false; - // } - submitChat(content) { if (!content) { return; @@ -186,18 +174,26 @@ export default class Chat extends Component { render(props, state) { - const { username } = props; + const { username, messagesOnly } = props; const { messages, inputEnabled, chatUserNames } = state; + const messageList = messages.map((message) => (html`<${Message} message=${message} username=${username} key=${message.id} />`)); + + if (messagesOnly) { + return ( + html` +
+ ${messageList} +
+ `); + } + return ( html`
- ${ - messages.map(message => (html`<${Message} message=${message} username=${username} />`)) - } - messages.. + ${messageList}
<${ChatInput} chatUserNames=${chatUserNames} diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index cb3a8243e..90d203e62 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -1,4 +1,8 @@ -import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; +import { h, Component, Fragment } from 'https://unpkg.com/preact?module'; +import htm from 'https://unpkg.com/htm?module'; +const html = htm.bind(h); + + import UsernameForm from './username.js'; import Chat from './chat.js'; import Websocket from '../websocket.js'; @@ -33,10 +37,26 @@ export default class StandaloneChat extends Component { } render(props, state) { + const { messagesOnly } = props; const { username, userAvatarImage, websocket } = state; + + + if (messagesOnly) { + return ( + html` + <${Chat} + websocket=${websocket} + username=${username} + userAvatarImage=${userAvatarImage} + chatEnabled + messagesOnly + /> + `); + } + return ( html` -
+ <${Fragment}> <${UsernameForm} username=${username} userAvatarImage=${userAvatarImage} @@ -50,7 +70,7 @@ export default class StandaloneChat extends Component { userAvatarImage=${userAvatarImage} chatEnabled /> -
+ `); } diff --git a/webroot/standalone-chat.html b/webroot/standalone-chat.html index 370467bb2..d7fa4b7f2 100644 --- a/webroot/standalone-chat.html +++ b/webroot/standalone-chat.html @@ -4,7 +4,7 @@ - + @@ -20,8 +20,11 @@ diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index c6b33a4b6..289c62363 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -3,6 +3,7 @@ overflow: auto; padding: 1em 0; } + #message-input-container { width: 100%; padding: 1em; @@ -13,22 +14,8 @@ align-items: flex-end; margin-bottom: 0; } -#message-body-form { - font-size: 1em; - height: 60px; -} -#message-body-form:disabled{ - opacity: .5; -} -#message-body-form img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} -#message-body-form .emoji { - width: 40px; -} + #message-form-actions { flex-direction: row; @@ -37,16 +24,22 @@ width: 100%; } -.message-text img { - display: inline; - padding-left: 5px; - padding-right: 5px; +#message-form-actions-buttons { + flex-direction: row; + justify-content: flex-end; + align-items: center; } -.message-text .emoji { - width: 60px; +/* Emoji picker button */ +#emoji-button { + font-size: 1.75em; + cursor: pointer; + margin-right: .5em; } +.emoji-picker__wrapper {} + + .message { @@ -68,13 +61,37 @@ max-width: 85%; word-wrap: break-word; } -.message-content a { + + + + + + +/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT CONTENT */ +.message-text a { color: #7F9CF5; /* indigo-400 */ } -.message-content a:hover { +.message-text a:hover { text-decoration: underline; } +.message-text img { + display: inline; + padding-left: 5px; + padding-right: 5px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} + +.message-text .emoji { + width: 60px; +} + .message-text iframe { width: 100%; @@ -86,57 +103,27 @@ height: 314px; } -.message-text code { - background-color:darkslategrey; - padding: 3px; -} -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} - .message-text .embedded-image { width: 100%; height: 170px; border-radius: 15px; } -.message-text code { - background-color:darkslategrey; - padding: 3px; -} - -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} -.message-text .embedded-image { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} .message-text .highlighted { color: orange; font-weight: 400; font-size: 14px; - } +/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT CONTENT */ + + + + + -.message-text code { - background-color:darkslategrey; - padding: 3px; -} /* The chat input has a fake placeholder that is styled below. @@ -144,6 +131,30 @@ It pulls the placeholder text from the div's placeholder attribute. But really it's just the innerHTML content. */ + + + + + + +#message-body-form { + font-size: 1em; + height: 60px; +} +#message-body-form:disabled{ + opacity: .5; +} +#message-body-form img { + display: inline; + padding-left: 5px; + padding-right: 5px; +} + +#message-body-form .emoji { + width: 40px; +} + + /* If the div is empty then show the placeholder */ #message-body-form:empty:before{ content: attr(placeholder); From 0325af3ce7aa085aa0274974b44cf2beea2e175f Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Wed, 19 Aug 2020 00:47:41 -0700 Subject: [PATCH 13/35] styles for message only view --- webroot/js/chat/chat.js | 2 +- webroot/js/chat/message.js | 5 ++++- webroot/styles/standalone-chat.css | 32 +++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 54caabc4c..4b9ee9649 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -182,7 +182,7 @@ export default class Chat extends Component { if (messagesOnly) { return ( html` -
+
${messageList}
`); diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index 6de5afa82..413148576 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -14,7 +14,10 @@ export default class Message extends Component { const { image, author, body } = message; const formattedMessage = formatMessageText(body, username); const avatar = image || generateAvatar(author); - const avatarBgColor = { backgroundColor: messageBubbleColorForString(author) }; + + const authorColor = messageBubbleColorForString(author); + const avatarBgColor = { backgroundColor: authorColor }; + const authorTextColor = { color: authorColor }; return ( html`
diff --git a/webroot/styles/standalone-chat.css b/webroot/styles/standalone-chat.css index 898ece926..8a4b40e88 100644 --- a/webroot/styles/standalone-chat.css +++ b/webroot/styles/standalone-chat.css @@ -1,4 +1,34 @@ /* -The styles in this file mostly ovveride those coming fro chat.css +The styles in this file mostly ovveride those coming from chat.css */ + + +#messages-container.messages-only { + + /* modify this px number if you want things to be relatively bigger or smaller 8*/ + font-size: 16px; + + padding: 1em .5em; + background-color: rgba(0,0,0,.4); +} +.messages-only .message-content { + text-shadow: 1px 1px 0px rgba(0,0,0,0.25); +} +.message-avatar { + box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.25); +} +.messages-only .message { + padding: .5em; +} + +.messages-only .message-text { + font-weight: 400; + color: white; +} +.messages-only .message-text a { + color: #fc0; +} +.messages-only .message-author { + color: rgba(20,0,40,1); +} From 756311f03c0318c85b122319a51b602dba0c5c18 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Wed, 19 Aug 2020 14:46:20 -0700 Subject: [PATCH 14/35] style updates to message-only --- webroot/js/chat/chat-input.js | 2 ++ webroot/js/chat/chat.js | 2 +- webroot/js/chat/message.js | 2 +- webroot/standalone-chat.html | 4 ++-- webroot/styles/chat.css | 9 +++------ webroot/styles/standalone-chat.css | 9 ++++++--- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index 198501f5c..b6f3bf189 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -66,6 +66,8 @@ export default class ChatInput extends Component { custom: json, initialCategory: 'custom', showPreview: false, + emojiSize: '30px', + emojisPerRow: 6, position: 'top' }); this.emojiPicker.on('emoji', emoji => { diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 4b9ee9649..54caabc4c 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -182,7 +182,7 @@ export default class Chat extends Component { if (messagesOnly) { return ( html` -
+
${messageList}
`); diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index 413148576..1e52a76d3 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -28,7 +28,7 @@ export default class Message extends Component {
-

${author}

+

${author}

- +
@@ -21,7 +21,7 @@ import { render, html } from "https://unpkg.com/htm/preact/index.mjs?module"; import StandaloneChat from './js/chat/standalone.js'; - const messagesOnly = true; + const messagesOnly = false; (function () { render(html`<${StandaloneChat} messagesOnly=${messagesOnly} />`, document.getElementById("chat-container")); diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index 289c62363..c291a9a8d 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -36,10 +36,9 @@ cursor: pointer; margin-right: .5em; } - -.emoji-picker__wrapper {} - - +.emoji-picker__emoji { + border-radius: 10px; +} .message { @@ -65,8 +64,6 @@ - - /* MESSAGE TEXT CONTENT */ /* MESSAGE TEXT CONTENT */ /* MESSAGE TEXT CONTENT */ diff --git a/webroot/styles/standalone-chat.css b/webroot/styles/standalone-chat.css index 8a4b40e88..187cf0cd2 100644 --- a/webroot/styles/standalone-chat.css +++ b/webroot/styles/standalone-chat.css @@ -4,20 +4,23 @@ The styles in this file mostly ovveride those coming from chat.css */ -#messages-container.messages-only { +.messages-only { /* modify this px number if you want things to be relatively bigger or smaller 8*/ font-size: 16px; - padding: 1em .5em; - background-color: rgba(0,0,0,.4); } .messages-only .message-content { text-shadow: 1px 1px 0px rgba(0,0,0,0.25); } .message-avatar { + display: none; box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.25); } +.message-avatar img { + height: 1.8em; + width: 1.8em; +} .messages-only .message { padding: .5em; } From df852221d5044aa3e136791d3ea653b51758e6cf Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Wed, 19 Aug 2020 23:22:08 -0700 Subject: [PATCH 15/35] Update user color to be a more customizable hsl color with constant saturation and lightness --- webroot/js/utils/user-colors.js | 89 +++------------------------------ 1 file changed, 8 insertions(+), 81 deletions(-) diff --git a/webroot/js/utils/user-colors.js b/webroot/js/utils/user-colors.js index c1b161f5b..9dac505c4 100644 --- a/webroot/js/utils/user-colors.js +++ b/webroot/js/utils/user-colors.js @@ -1,88 +1,15 @@ -export function getHashFromString(string) { - let hash = 1; - for (let i = 0; i < string.length; i++) { - const codepoint = string.charCodeAt(i); - hash *= codepoint; - } - - return Math.abs(hash); -} - -export function digitsFromNumber(number) { - const numberString = number.toString(); - let digits = []; - - for (let i = 0, len = numberString.length; i < len; i += 1) { - digits.push(numberString.charAt(i)); - } - - return digits; -} - -// function avatarFromString(string) { -// const hash = getHashFromString(string); -// const digits = digitsFromNumber(hash); -// // eslint-disable-next-line -// const sum = digits.reduce(function (total, number) { -// return total + number; -// }); -// const sumDigits = digitsFromNumber(sum); -// const first = sumDigits[0]; -// const second = sumDigits[1]; -// let filename = '/avatars/'; - -// // eslint-disable-next-line -// if (first == 1 || first == 2) { -// filename += '1' + second.toString(); -// // eslint-disable-next-line -// } else if (first == 3 || first == 4) { -// filename += '2' + second.toString(); -// // eslint-disable-next-line -// } else if (first == 5 || first == 6) { -// filename += '3' + second.toString(); -// // eslint-disable-next-line -// } else if (first == 7 || first == 8) { -// filename += '4' + second.toString(); -// } else { -// filename += '5'; -// } - -// return filename + '.svg'; -// } - -export function colorForString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - // eslint-disable-next-line - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let colour = '#'; - for (let i = 0; i < 3; i++) { - // eslint-disable-next-line - let value = (hash >> (i * 8)) & 0xff; - colour += ('00' + value.toString(16)).substr(-2); - } - return colour; -} - export function messageBubbleColorForString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { // eslint-disable-next-line hash = str.charCodeAt(i) + ((hash << 5) - hash); } - let color = '#'; - for (let i = 0; i < 3; i++) { - // eslint-disable-next-line - let value = (hash >> (i * 8)) & 0xff; - color += ('00' + value.toString(16)).substr(-2); - } - // Convert to RGBA - let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); - let rgb = result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - } : null; - return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.4)'; + + // Tweak these to adjust the result of the color + const saturation = 70; + const lightness = 50; + const alpha = 1.0; + const hue = parseInt(Math.abs(hash), 16) % 300; + + return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; } From 136a5759734a0fe8c89c789ad15c4e2349c98e84 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 20 Aug 2020 12:59:07 -0700 Subject: [PATCH 16/35] wip.. initial setup for preact integration into main app --- webroot/index2.html | 76 +++++++++ webroot/js/app2.js | 310 ++++++++++++++++++++++++++++++++++ webroot/js/chat/chat-input.js | 2 +- webroot/js/chat/standalone.js | 1 + webroot/js/utils.js | 2 + 5 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 webroot/index2.html create mode 100644 webroot/js/app2.js diff --git a/webroot/index2.html b/webroot/index2.html new file mode 100644 index 000000000..5ef518b28 --- /dev/null +++ b/webroot/index2.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/webroot/js/app2.js b/webroot/js/app2.js new file mode 100644 index 000000000..8ee9c1aaf --- /dev/null +++ b/webroot/js/app2.js @@ -0,0 +1,310 @@ +import { h, Component, Fragment } from 'https://unpkg.com/preact?module'; +import htm from 'https://unpkg.com/htm?module'; +const html = htm.bind(h); + + +import UsernameForm from './chat/username.js'; +import Chat from './chat/chat.js'; +import Websocket from './websocket.js'; + +import { getLocalStorage, generateAvatar, generateUsername, URL_OWNCAST, URL_CONFIG, URL_STATUS, addNewlines } from './utils.js'; +import { KEY_USERNAME, KEY_AVATAR, } from './utils/chat.js'; + +export default class App extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + websocket: new Websocket(), + chatEnabled: true, // always true for standalone chat + username: getLocalStorage(KEY_USERNAME) || generateUsername(), + userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), + + streamStatus: null, + player: null, + configData: {}, + }; + + // timers + this.playerRestartTimer = null; + this.offlineTimer = null; + this.statusTimer = null; + this.disableChatTimer = null; + this.streamDurationTimer = null; + + this.handleUsernameChange = this.handleUsernameChange.bind(this); + this.getConfig = this.getConfig.bind(this); + this.getStreamStatus = this.getStreamStatus.bind(this); + this.getExtraUserContent = this.getExtraUserContent.bind(this); + + } + + componentDidMount() { + this.getConfig(); + + // DO LATER.. + // this.player = new OwncastPlayer(); + // this.player.setupPlayerCallbacks({ + // onReady: this.handlePlayerReady, + // onPlaying: this.handlePlayerPlaying, + // onEnded: this.handlePlayerEnded, + // onError: this.handlePlayerError, + // }); + // this.player.init(); + } + + // fetch /config data + getConfig() { + fetch(URL_CONFIG) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); + }) + .then(json => { + this.setConfigData(json); + }) + .catch(error => { + this.handleNetworkingError(`Fetch config: ${error}`); + }); + } + + // fetch stream status + getStreamStatus() { + fetch(URL_STATUS) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); + }) + .then(json => { + this.updateStreamStatus(json); + }) + .catch(error => { + this.handleOfflineMode(); + this.handleNetworkingError(`Stream status: ${error}`); + }); + } + + // fetch content.md + getExtraUserContent(path) { + fetch(path) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.text(); + }) + .then(text => { + const descriptionHTML = new showdown.Converter().makeHtml(text); + this.vueApp.extraUserContent = descriptionHTML; + }) + .catch(error => { + this.handleNetworkingError(`Fetch extra content: ${error}`); + }); + } + + + setConfigData(data = {}) { + const { title, extraUserInfoFileName, summary } = data; + + window.document.title = title; + if (extraUserInfoFileName) { + this.getExtraUserContent(extraUserInfoFileName); + } + + this.setState({ + configData: { + ...data, + summary: summary && addNewlines(summary), + }, + }); + } + + // handle UI things from stream status result + updateStreamStatus(status = {}) { + if (!status) { + return; + } + // update UI + this.vueApp.viewerCount = status.viewerCount; + this.vueApp.sessionMaxViewerCount = status.sessionMaxViewerCount; + this.vueApp.overallMaxViewerCount = status.overallMaxViewerCount; + + this.lastDisconnectTime = status.lastDisconnectTime; + + if (!this.streamStatus) { + // display offline mode the first time we get status, and it's offline. + if (!status.online) { + this.handleOfflineMode(); + } else { + this.handleOnlineMode(); + } + } else { + if (status.online && !this.streamStatus.online) { + // stream has just come online. + this.handleOnlineMode(); + } else if (!status.online && this.streamStatus.online) { + // stream has just flipped offline. + this.handleOfflineMode(); + } + } + + // keep a local copy + this.streamStatus = status; + + if (status.online) { + // only do this if video is paused, so no unnecessary img fetches + if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { + this.player.setPoster(); + } + } + } + + // stop status timer and disable chat after some time. + handleOfflineMode() { + this.vueApp.isOnline = false; + clearInterval(this.streamDurationTimer); + this.vueApp.streamStatus = MESSAGE_OFFLINE; + if (this.streamStatus) { + const remainingChatTime = TIMER_DISABLE_CHAT_AFTER_OFFLINE - (Date.now() - new Date(this.lastDisconnectTime)); + const countdown = (remainingChatTime < 0) ? 0 : remainingChatTime; + this.disableChatTimer = setTimeout(this.messagingInterface.disableChat, countdown); + } + } + + // play video! + handleOnlineMode() { + this.vueApp.playerOn = true; + this.vueApp.isOnline = true; + this.vueApp.streamStatus = MESSAGE_ONLINE; + + this.player.startPlayer(); + clearTimeout(this.disableChatTimer); + this.disableChatTimer = null; + this.messagingInterface.enableChat(); + + this.streamDurationTimer = + setInterval(this.setCurrentStreamDuration, TIMER_STREAM_DURATION_COUNTER); + } + + + handleUsernameChange(newName, newAvatar) { + this.setState({ + username: newName, + userAvatarImage: newAvatar, + }); + } + + handleChatToggle() { + const { chatEnabled: curChatEnabled } = this.state; + this.setState({ + chatEnabled: !curChatEnabled, + }); + } + + handleNetworkingError(error) { + console.log(`>>> App Error: ${error}`); + } + + render(props, state) { + const { username, userAvatarImage, websocket, configData } = state; + const { + version: appVersion, + logo = {}, + socialHandles, + name: streamnerName, + summary, + tags, + title, + } = configData; + const { small: smallLogo, large: largeLogo } = logo; + + const bgLogo = { backgroundImage: `url(${smallLogo})` }; + const bgLogoLarge = { backgroundImage: `url(${largeLogo})` }; + + // not needed for standalone, just messages only. remove later. + return ( + html` +
+
+
+

+ + + + ${title} +

+ + <${UsernameForm} + username=${username} + userAvatarImage=${userAvatarImage} + handleUsernameChange=${this.handleUsernameChange} + handleChatToggle=${this.handleChatToggle} + /> + +
+
+ +
+
+ +
+ + +
+ {{ streamStatus }} + {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. + Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}. + {{ overallMaxViewerCount }} overall. +
+
+ +
+ {{streamerName}} + +
{{extraUserContent}}
+
+ + +
+ + <${Chat} + websocket=${websocket} + username=${username} + userAvatarImage=${userAvatarImage} + chatEnabled + /> + +
+ `); + } +} + diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index b6f3bf189..7d9fed2ca 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -111,7 +111,7 @@ export default class ChatInput extends Component { return false; } - const partial = inputHTML.substring(at + 1, position).trim(); + let partial = inputHTML.substring(at + 1, position).trim(); if (partial === this.suggestion) { partial = this.partial; diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index 90d203e62..14f0fd951 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -54,6 +54,7 @@ export default class StandaloneChat extends Component { `); } + // not needed for standalone, just messages only. remove later. return ( html` <${Fragment}> diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 26da002e1..238bd8f58 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,6 +1,8 @@ export const URL_STATUS = `/status`; export const URL_CHAT_HISTORY = `/chat`; export const URL_CUSTOM_EMOJIS = `/emoji`; +export const URL_CONFIG = `/config`; + // TODO: This directory is customizable in the config. So we should expose this via the config API. export const URL_STREAM = `/hls/stream.m3u8`; export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; From c3adfe7b7b4ff9936dd10caf2493f830bad70426 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 20 Aug 2020 15:29:15 -0700 Subject: [PATCH 17/35] preact app integration --- webroot/index2.html | 1 + webroot/js/app2.js | 304 ++++++++++++++++++++++++---------- webroot/js/chat/chat-input.js | 4 +- webroot/js/chat/standalone.js | 2 - webroot/js/chat/username.js | 53 +++--- webroot/js/player.js | 19 +-- webroot/js/social.js | 79 +++------ webroot/js/utils.js | 22 ++- 8 files changed, 297 insertions(+), 187 deletions(-) diff --git a/webroot/index2.html b/webroot/index2.html index 5ef518b28..8f2c70227 100644 --- a/webroot/index2.html +++ b/webroot/index2.html @@ -30,6 +30,7 @@ + diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 8ee9c1aaf..05d672952 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -3,11 +3,28 @@ import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); +import SocialIcon from './social.js'; import UsernameForm from './chat/username.js'; import Chat from './chat/chat.js'; import Websocket from './websocket.js'; +import { OwncastPlayer } from './player.js'; -import { getLocalStorage, generateAvatar, generateUsername, URL_OWNCAST, URL_CONFIG, URL_STATUS, addNewlines } from './utils.js'; +import { + getLocalStorage, + generateAvatar, + generateUsername, + URL_OWNCAST, + URL_CONFIG, + URL_STATUS, + addNewlines, + pluralize, + TIMER_STATUS_UPDATE, + TIMER_DISABLE_CHAT_AFTER_OFFLINE, + TIMER_STREAM_DURATION_COUNTER, + TEMP_IMAGE, + MESSAGE_OFFLINE, + MESSAGE_ONLINE, +} from './utils.js'; import { KEY_USERNAME, KEY_AVATAR, } from './utils/chat.js'; export default class App extends Component { @@ -16,13 +33,22 @@ export default class App extends Component { this.state = { websocket: new Websocket(), - chatEnabled: true, // always true for standalone chat + displayChat: false, // chat panel state + chatEnabled: false, // chat input box state username: getLocalStorage(KEY_USERNAME) || generateUsername(), userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), - streamStatus: null, - player: null, configData: {}, + extraUserContent: '', + + playerActive: false, // player object is active + streamOnline: false, // stream is active/online + + //status + streamStatusMessage: MESSAGE_OFFLINE, + viewerCount: '', + sessionMaxViewerCount: '', + overallMaxViewerCount: '', }; // timers @@ -32,25 +58,48 @@ export default class App extends Component { this.disableChatTimer = null; this.streamDurationTimer = null; + // misc dom events + this.handleChatPanelToggle = this.handleChatPanelToggle.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this); + + this.handleOfflineMode = this.handleOfflineMode.bind(this); + this.handleOnlineMode = this.handleOnlineMode.bind(this); + this.disableChatInput = this.disableChatInput.bind(this); + + // player events + this.handlePlayerReady = this.handlePlayerReady.bind(this); + this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this); + this.handlePlayerEnded = this.handlePlayerEnded.bind(this); + this.handlePlayerError = this.handlePlayerError.bind(this); + + // fetch events this.getConfig = this.getConfig.bind(this); this.getStreamStatus = this.getStreamStatus.bind(this); this.getExtraUserContent = this.getExtraUserContent.bind(this); + } componentDidMount() { this.getConfig(); - // DO LATER.. - // this.player = new OwncastPlayer(); - // this.player.setupPlayerCallbacks({ - // onReady: this.handlePlayerReady, - // onPlaying: this.handlePlayerPlaying, - // onEnded: this.handlePlayerEnded, - // onError: this.handlePlayerError, - // }); - // this.player.init(); + this.player = new OwncastPlayer(); + this.player.setupPlayerCallbacks({ + onReady: this.handlePlayerReady, + onPlaying: this.handlePlayerPlaying, + onEnded: this.handlePlayerEnded, + onError: this.handlePlayerError, + }); + this.player.init(); + } + + componentWillUnmount() { + // clear all the timers + clearInterval(this.playerRestartTimer); + clearInterval(this.offlineTimer); + clearInterval(this.statusTimer); + clearTimeout(this.disableChatTimer); + clearInterval(this.streamDurationTimer); } // fetch /config data @@ -98,8 +147,9 @@ export default class App extends Component { return response.text(); }) .then(text => { - const descriptionHTML = new showdown.Converter().makeHtml(text); - this.vueApp.extraUserContent = descriptionHTML; + this.setState({ + extraUserContent: new showdown.Converter().makeHtml(text), + }); }) .catch(error => { this.handleNetworkingError(`Fetch extra content: ${error}`); @@ -128,66 +178,89 @@ export default class App extends Component { if (!status) { return; } - // update UI - this.vueApp.viewerCount = status.viewerCount; - this.vueApp.sessionMaxViewerCount = status.sessionMaxViewerCount; - this.vueApp.overallMaxViewerCount = status.overallMaxViewerCount; + const { + viewerCount, + sessionMaxViewerCount, + overallMaxViewerCount, + online, + } = status; + const { streamOnline: curStreamOnline } = this.state; this.lastDisconnectTime = status.lastDisconnectTime; - if (!this.streamStatus) { - // display offline mode the first time we get status, and it's offline. - if (!status.online) { - this.handleOfflineMode(); - } else { - this.handleOnlineMode(); - } - } else { - if (status.online && !this.streamStatus.online) { - // stream has just come online. - this.handleOnlineMode(); - } else if (!status.online && this.streamStatus.online) { - // stream has just flipped offline. - this.handleOfflineMode(); - } + if (status.online && !curStreamOnline) { + // stream has just come online. + this.handleOnlineMode(); + } else if (!status.online && curStreamOnline) { + // stream has just flipped offline. + this.handleOfflineMode(); } - - // keep a local copy - this.streamStatus = status; - if (status.online) { // only do this if video is paused, so no unnecessary img fetches if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { this.player.setPoster(); } } + this.setState({ + viewerCount, + sessionMaxViewerCount, + overallMaxViewerCount, + streamOnline: online, + }); + } + + // when videojs player is ready, start polling for stream + handlePlayerReady() { + this.getStreamStatus(); + this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); + } + + handlePlayerPlaying() { + // do something? + } + + + // likely called some time after stream status has gone offline. + // basically hide video and show underlying "poster" + handlePlayerEnded() { + this.setState({ + playerActive: false, + }); + } + + handlePlayerError() { + // do something? + this.handleOfflineMode(); + this.handlePlayerEnded(); } // stop status timer and disable chat after some time. handleOfflineMode() { - this.vueApp.isOnline = false; clearInterval(this.streamDurationTimer); - this.vueApp.streamStatus = MESSAGE_OFFLINE; - if (this.streamStatus) { - const remainingChatTime = TIMER_DISABLE_CHAT_AFTER_OFFLINE - (Date.now() - new Date(this.lastDisconnectTime)); - const countdown = (remainingChatTime < 0) ? 0 : remainingChatTime; - this.disableChatTimer = setTimeout(this.messagingInterface.disableChat, countdown); - } + const remainingChatTime = TIMER_DISABLE_CHAT_AFTER_OFFLINE - (Date.now() - new Date(this.lastDisconnectTime)); + const countdown = (remainingChatTime < 0) ? 0 : remainingChatTime; + this.disableChatTimer = setTimeout(this.disableChatInput, countdown); + this.setState({ + streamOnline: false, + streamStatusMessage: MESSAGE_OFFLINE, + }); } // play video! handleOnlineMode() { - this.vueApp.playerOn = true; - this.vueApp.isOnline = true; - this.vueApp.streamStatus = MESSAGE_ONLINE; - this.player.startPlayer(); clearTimeout(this.disableChatTimer); this.disableChatTimer = null; - this.messagingInterface.enableChat(); this.streamDurationTimer = setInterval(this.setCurrentStreamDuration, TIMER_STREAM_DURATION_COUNTER); + + this.setState({ + playerActive: true, + streamOnline: true, + chatEnabled: true, + streamStatusMessage: MESSAGE_ONLINE, + }); } @@ -198,10 +271,16 @@ export default class App extends Component { }); } - handleChatToggle() { - const { chatEnabled: curChatEnabled } = this.state; + handleChatPanelToggle() { + const { displayChat: curDisplayed } = this.state; this.setState({ - chatEnabled: !curChatEnabled, + displayChat: !curDisplayed, + }); + } + + disableChatInput() { + this.setState({ + chatEnabled: false, }); } @@ -210,49 +289,81 @@ export default class App extends Component { } render(props, state) { - const { username, userAvatarImage, websocket, configData } = state; + const { + username, + userAvatarImage, + websocket, + configData, + extraUserContent, + displayChat, + viewerCount, + sessionMaxViewerCount, + overallMaxViewerCount, + playerActive, + streamOnline, + streamStatusMessage, + chatEnabled, + } = state; const { version: appVersion, logo = {}, - socialHandles, - name: streamnerName, + socialHandles = [], + name: streamerName, summary, - tags, + tags = [], title, } = configData; - const { small: smallLogo, large: largeLogo } = logo; + const { small: smallLogo = TEMP_IMAGE, large: largeLogo = TEMP_IMAGE } = logo; const bgLogo = { backgroundImage: `url(${smallLogo})` }; const bgLogoLarge = { backgroundImage: `url(${largeLogo})` }; - // not needed for standalone, just messages only. remove later. + const tagList = !tags.length ? + null : + tags.map((tag, index) => html` +
  • ${tag}
  • + `); + + const socialIconsList = + !socialHandles.length ? + null : + socialHandles.map((item, index) => html` +
  • + <${SocialIcon} platform=${item.platform} url=${item.url} /> +
  • + `); + + + const chatClass = displayChat ? 'chat' : 'no-chat'; + const mainClass = playerActive ? 'online' : ''; + const streamInfoClass = streamOnline ? 'online' : ''; return ( html` -
    +
    -

    +

    - + ${title}

    - - <${UsernameForm} - username=${username} - userAvatarImage=${userAvatarImage} - handleUsernameChange=${this.handleUsernameChange} - handleChatToggle=${this.handleChatToggle} - /> - +
    + <${UsernameForm} + username=${username} + userAvatarImage=${userAvatarImage} + handleUsernameChange=${this.handleUsernameChange} + /> + +
    -
    +
    -
    - {{ streamStatus }} - {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. - Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}. - {{ overallMaxViewerCount }} overall. +
    + ${streamStatusMessage} + ${viewerCount} ${pluralize('viewer', viewerCount)}. + Max ${pluralize('viewer', sessionMaxViewerCount)}. + ${overallMaxViewerCount} overall.
    - {{streamerName}} - -
    {{extraUserContent}}
    +
    +
    + +
    +
    +

    + About + ${streamerName} +

    + +
    +
      + ${tagList} +
    +
    +
    +
    @@ -300,7 +434,7 @@ export default class App extends Component { websocket=${websocket} username=${username} userAvatarImage=${userAvatarImage} - chatEnabled + chatEnabled=${chatEnabled} />
    diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index 7d9fed2ca..dcc904ad5 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -240,8 +240,8 @@ export default class ChatInput extends Component { <${ContentEditable} 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" - placeholderText=${placeholderText} + placeholderText=${placeholderText} innerRef=${this.formMessageInput} html=${inputHTML} disabled=${!inputEnabled} @@ -251,7 +251,7 @@ export default class ChatInput extends Component { onBlur=${this.handleMessageInputBlur} onPaste=${this.handlePaste} - /> + />
    ${inputWarning} diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index 14f0fd951..115982d63 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -48,7 +48,6 @@ export default class StandaloneChat extends Component { websocket=${websocket} username=${username} userAvatarImage=${userAvatarImage} - chatEnabled messagesOnly /> `); @@ -69,7 +68,6 @@ export default class StandaloneChat extends Component { websocket=${websocket} username=${username} userAvatarImage=${userAvatarImage} - chatEnabled /> `); diff --git a/webroot/js/chat/username.js b/webroot/js/chat/username.js index c18eb93de..95d82259b 100644 --- a/webroot/js/chat/username.js +++ b/webroot/js/chat/username.js @@ -59,7 +59,7 @@ export default class UsernameForm extends Component { } render(props, state) { - const { username, userAvatarImage, handleChatToggle } = props; + const { username, userAvatarImage } = props; const { displayForm } = state; const narrowSpace = document.body.clientWidth < 640; @@ -76,34 +76,31 @@ export default class UsernameForm extends Component { } return ( html` -
    -
    -
    - - ${username} -
    - -
    - - - - -
    +
    +
    + + ${username} +
    + +
    + + + +
    -
    `); } diff --git a/webroot/js/player.js b/webroot/js/player.js index 986e3566d..6e88d875e 100644 --- a/webroot/js/player.js +++ b/webroot/js/player.js @@ -17,7 +17,6 @@ const VIDEO_OPTIONS = { vhs: { // used to select the lowest bitrate playlist initially. This helps to decrease playback start time. This setting is false by default. enableLowInitialPlaylist: true, - } }, liveTracker: { @@ -128,27 +127,25 @@ class OwncastPlayer { if (window.WebKitPlaybackTargetAvailabilityEvent) { var videoJsButtonClass = videojs.getComponent('Button'); var concreteButtonClass = videojs.extend(videoJsButtonClass, { - - // The `init()` method will also work for constructor logic here, but it is + + // The `init()` method will also work for constructor logic here, but it is // deprecated. If you provide an `init()` method, it will override the // `constructor()` method! constructor: function () { videoJsButtonClass.call(this, player); - }, // notice the comma - + }, + handleClick: function () { const videoElement = document.getElementsByTagName('video')[0]; videoElement.webkitShowPlaybackTargetPicker(); - } + }, }); - + var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass()); concreteButtonInstance.addClass("vjs-airplay"); } - }); + }); } - - } -export { OwncastPlayer }; \ No newline at end of file +export { OwncastPlayer }; diff --git a/webroot/js/social.js b/webroot/js/social.js index a36c822b4..ec37ad558 100644 --- a/webroot/js/social.js +++ b/webroot/js/social.js @@ -1,3 +1,5 @@ +import { html } from "https://unpkg.com/htm/preact/index.mjs?module"; + const SOCIAL_PLATFORMS = { default: { name: "default", @@ -70,58 +72,31 @@ const SOCIAL_PLATFORMS = { }, }; -Vue.component('social-list', { - props: ['platforms'], +export default function SocialIcon(props) { + const { platform, url } = props; + const platformInfo = SOCIAL_PLATFORMS[platform.toLowerCase()]; + const inList = !!platformInfo; + const imgRow = inList ? platformInfo.imgPos[0] : 0; + const imgCol = inList ? platformInfo.imgPos[1] : 0; - template: ` - - `, + const name = inList ? platformInfo.name : platform; -}); - -Vue.component('user-social-icon', { - props: ['platform', 'url'], - data: function() { - const platformInfo = SOCIAL_PLATFORMS[this.platform.toLowerCase()]; - const inList = !!platformInfo; - const imgRow = inList ? platformInfo.imgPos[0] : 0; - const imgCol = inList ? platformInfo.imgPos[1] : 0; - return { - name: inList ? platformInfo.name : this.platform, - link: this.url, - - style: `--imgRow: -${imgRow}; --imgCol: -${imgCol};`, - itemClass: { - "user-social-item": true, - "flex": true, - "use-default": !inList, - }, - labelClass: { - "platform-label": true, - "visually-hidden": inList, - "text-indigo-800": true, - }, - }; - }, - template: ` -
  • - - - Find me on {{platform}} + const style = `--imgRow: -${imgRow}; --imgCol: -${imgCol};`; + const itemClass = { + "user-social-item": true, + "flex": true, + "use-default": !inList, + }; + const labelClass = { + "platform-label": true, + "visually-hidden": inList, + "text-indigo-800": true, + }; + return ( + html` + + + Find me on ${name} -
  • - `, -}); + `); +} diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 238bd8f58..bd9f12d08 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -7,6 +7,14 @@ export const URL_CONFIG = `/config`; export const URL_STREAM = `/hls/stream.m3u8`; export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; +export const TIMER_STATUS_UPDATE = 5000; // ms +export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins +export const TIMER_STREAM_DURATION_COUNTER = 1000; +export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +export const MESSAGE_OFFLINE = 'Stream is offline.'; +export const MESSAGE_ONLINE = 'Stream is online'; + export const POSTER_DEFAULT = `/img/logo.png`; export const POSTER_THUMB = `/thumbnail.jpg`; @@ -66,27 +74,27 @@ export function pluralize(string, count) { // Trying to determine if browser is mobile/tablet. // Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent export function hasTouchScreen() { - var hasTouchScreen = false; + let hasTouch = false; if ("maxTouchPoints" in navigator) { - hasTouchScreen = navigator.maxTouchPoints > 0; + hasTouch = navigator.maxTouchPoints > 0; } else if ("msMaxTouchPoints" in navigator) { - hasTouchScreen = navigator.msMaxTouchPoints > 0; + hasTouch = navigator.msMaxTouchPoints > 0; } else { var mQ = window.matchMedia && matchMedia("(pointer:coarse)"); if (mQ && mQ.media === "(pointer:coarse)") { - hasTouchScreen = !!mQ.matches; + hasTouch = !!mQ.matches; } else if ('orientation' in window) { - hasTouchScreen = true; // deprecated, but good fallback + hasTouch = true; // deprecated, but good fallback } else { // Only as a last resort, fall back to user agent sniffing var UA = navigator.userAgent; - hasTouchScreen = ( + hasTouch = ( /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA) ); } } - return hasTouchScreen; + return hasTouch; } // generate random avatar from https://robohash.org From 60c04c02c4db81a0c0bfd736ee77693fc416fca5 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 20 Aug 2020 15:33:51 -0700 Subject: [PATCH 18/35] update chat component to enable formfield via both streamstatus and socket status --- webroot/js/chat/chat.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index 54caabc4c..27ffb899a 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -125,7 +125,7 @@ export default class Chat extends Component { } websocketDisconnected() { // this.websocket = null; - this.disableChat() + this.disableChat(); } submitChat(content) { @@ -156,7 +156,6 @@ export default class Chat extends Component { updateAuthorList(message) { const { type } = message; - const username = ''; const nameList = this.state.chatUserNames; if ( @@ -174,7 +173,7 @@ export default class Chat extends Component { render(props, state) { - const { username, messagesOnly } = props; + const { username, messagesOnly, chatEnabled } = props; const { messages, inputEnabled, chatUserNames } = state; const messageList = messages.map((message) => (html`<${Message} message=${message} username=${username} key=${message.id} />`)); @@ -197,7 +196,7 @@ export default class Chat extends Component {
    <${ChatInput} chatUserNames=${chatUserNames} - inputEnabled=${inputEnabled} + inputEnabled=${chatEnabled && inputEnabled} handleSendMessage=${this.submitChat} />
    From 42a34df63eca9a58432aa1ee2354255b9aa856af Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 20 Aug 2020 15:51:11 -0700 Subject: [PATCH 19/35] move consts over --- webroot/js/app2.js | 4 +++- webroot/js/player.js | 2 ++ webroot/js/utils.js | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 05d672952..894178093 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -175,6 +175,8 @@ export default class App extends Component { // handle UI things from stream status result updateStreamStatus(status = {}) { + const { streamOnline: curStreamOnline } = this.state; + if (!status) { return; } @@ -184,7 +186,6 @@ export default class App extends Component { overallMaxViewerCount, online, } = status; - const { streamOnline: curStreamOnline } = this.state; this.lastDisconnectTime = status.lastDisconnectTime; @@ -304,6 +305,7 @@ export default class App extends Component { streamStatusMessage, chatEnabled, } = state; + const { version: appVersion, logo = {}, diff --git a/webroot/js/player.js b/webroot/js/player.js index 6e88d875e..7a933ad4e 100644 --- a/webroot/js/player.js +++ b/webroot/js/player.js @@ -25,6 +25,8 @@ const VIDEO_OPTIONS = { sources: [VIDEO_SRC], }; +export const POSTER_DEFAULT = `/img/logo.png`; +export const POSTER_THUMB = `/thumbnail.jpg`; class OwncastPlayer { constructor() { diff --git a/webroot/js/utils.js b/webroot/js/utils.js index bd9f12d08..7beda7a1e 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -15,8 +15,7 @@ export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAE export const MESSAGE_OFFLINE = 'Stream is offline.'; export const MESSAGE_ONLINE = 'Stream is online'; -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 From 4632ba4247e925e55c5785a7097d323396d478e6 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Thu, 20 Aug 2020 17:57:22 -0700 Subject: [PATCH 20/35] Reorganize the favicons --- static/metadata.html | 36 +++++++++--------- .../favicon}/android-icon-144x144.png | Bin .../favicon}/android-icon-192x192.png | Bin .../{ => img/favicon}/android-icon-36x36.png | Bin .../{ => img/favicon}/android-icon-48x48.png | Bin .../{ => img/favicon}/android-icon-72x72.png | Bin .../{ => img/favicon}/android-icon-96x96.png | Bin .../{ => img/favicon}/apple-icon-114x114.png | Bin .../{ => img/favicon}/apple-icon-120x120.png | Bin .../{ => img/favicon}/apple-icon-144x144.png | Bin .../{ => img/favicon}/apple-icon-152x152.png | Bin .../{ => img/favicon}/apple-icon-180x180.png | Bin .../{ => img/favicon}/apple-icon-57x57.png | Bin .../{ => img/favicon}/apple-icon-60x60.png | Bin .../{ => img/favicon}/apple-icon-72x72.png | Bin .../{ => img/favicon}/apple-icon-76x76.png | Bin .../favicon}/apple-icon-precomposed.png | Bin webroot/{ => img/favicon}/apple-icon.png | Bin webroot/{ => img/favicon}/browserconfig.xml | 0 webroot/{ => img/favicon}/favicon-16x16.png | Bin webroot/{ => img/favicon}/favicon-32x32.png | Bin webroot/{ => img/favicon}/favicon-96x96.png | Bin webroot/{ => img/favicon}/ms-icon-144x144.png | Bin webroot/{ => img/favicon}/ms-icon-150x150.png | Bin webroot/{ => img/favicon}/ms-icon-310x310.png | Bin webroot/{ => img/favicon}/ms-icon-70x70.png | Bin webroot/index2.html | 28 +++++++------- webroot/manifest.json | 14 +++---- 28 files changed, 39 insertions(+), 39 deletions(-) rename webroot/{ => img/favicon}/android-icon-144x144.png (100%) rename webroot/{ => img/favicon}/android-icon-192x192.png (100%) rename webroot/{ => img/favicon}/android-icon-36x36.png (100%) rename webroot/{ => img/favicon}/android-icon-48x48.png (100%) rename webroot/{ => img/favicon}/android-icon-72x72.png (100%) rename webroot/{ => img/favicon}/android-icon-96x96.png (100%) rename webroot/{ => img/favicon}/apple-icon-114x114.png (100%) rename webroot/{ => img/favicon}/apple-icon-120x120.png (100%) rename webroot/{ => img/favicon}/apple-icon-144x144.png (100%) rename webroot/{ => img/favicon}/apple-icon-152x152.png (100%) rename webroot/{ => img/favicon}/apple-icon-180x180.png (100%) rename webroot/{ => img/favicon}/apple-icon-57x57.png (100%) rename webroot/{ => img/favicon}/apple-icon-60x60.png (100%) rename webroot/{ => img/favicon}/apple-icon-72x72.png (100%) rename webroot/{ => img/favicon}/apple-icon-76x76.png (100%) rename webroot/{ => img/favicon}/apple-icon-precomposed.png (100%) rename webroot/{ => img/favicon}/apple-icon.png (100%) rename webroot/{ => img/favicon}/browserconfig.xml (100%) rename webroot/{ => img/favicon}/favicon-16x16.png (100%) rename webroot/{ => img/favicon}/favicon-32x32.png (100%) rename webroot/{ => img/favicon}/favicon-96x96.png (100%) rename webroot/{ => img/favicon}/ms-icon-144x144.png (100%) rename webroot/{ => img/favicon}/ms-icon-150x150.png (100%) rename webroot/{ => img/favicon}/ms-icon-310x310.png (100%) rename webroot/{ => img/favicon}/ms-icon-70x70.png (100%) diff --git a/static/metadata.html b/static/metadata.html index 3c95aae45..64706746e 100644 --- a/static/metadata.html +++ b/static/metadata.html @@ -29,23 +29,23 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + - + - + @@ -67,11 +67,11 @@

    Connect with {{.Config.Name}} elsewhere by visiting:

    - + {{range .Config.SocialHandles}}
  • {{.Platform}}
  • {{end}} - + - \ No newline at end of file + diff --git a/webroot/android-icon-144x144.png b/webroot/img/favicon/android-icon-144x144.png similarity index 100% rename from webroot/android-icon-144x144.png rename to webroot/img/favicon/android-icon-144x144.png diff --git a/webroot/android-icon-192x192.png b/webroot/img/favicon/android-icon-192x192.png similarity index 100% rename from webroot/android-icon-192x192.png rename to webroot/img/favicon/android-icon-192x192.png diff --git a/webroot/android-icon-36x36.png b/webroot/img/favicon/android-icon-36x36.png similarity index 100% rename from webroot/android-icon-36x36.png rename to webroot/img/favicon/android-icon-36x36.png diff --git a/webroot/android-icon-48x48.png b/webroot/img/favicon/android-icon-48x48.png similarity index 100% rename from webroot/android-icon-48x48.png rename to webroot/img/favicon/android-icon-48x48.png diff --git a/webroot/android-icon-72x72.png b/webroot/img/favicon/android-icon-72x72.png similarity index 100% rename from webroot/android-icon-72x72.png rename to webroot/img/favicon/android-icon-72x72.png diff --git a/webroot/android-icon-96x96.png b/webroot/img/favicon/android-icon-96x96.png similarity index 100% rename from webroot/android-icon-96x96.png rename to webroot/img/favicon/android-icon-96x96.png diff --git a/webroot/apple-icon-114x114.png b/webroot/img/favicon/apple-icon-114x114.png similarity index 100% rename from webroot/apple-icon-114x114.png rename to webroot/img/favicon/apple-icon-114x114.png diff --git a/webroot/apple-icon-120x120.png b/webroot/img/favicon/apple-icon-120x120.png similarity index 100% rename from webroot/apple-icon-120x120.png rename to webroot/img/favicon/apple-icon-120x120.png diff --git a/webroot/apple-icon-144x144.png b/webroot/img/favicon/apple-icon-144x144.png similarity index 100% rename from webroot/apple-icon-144x144.png rename to webroot/img/favicon/apple-icon-144x144.png diff --git a/webroot/apple-icon-152x152.png b/webroot/img/favicon/apple-icon-152x152.png similarity index 100% rename from webroot/apple-icon-152x152.png rename to webroot/img/favicon/apple-icon-152x152.png diff --git a/webroot/apple-icon-180x180.png b/webroot/img/favicon/apple-icon-180x180.png similarity index 100% rename from webroot/apple-icon-180x180.png rename to webroot/img/favicon/apple-icon-180x180.png diff --git a/webroot/apple-icon-57x57.png b/webroot/img/favicon/apple-icon-57x57.png similarity index 100% rename from webroot/apple-icon-57x57.png rename to webroot/img/favicon/apple-icon-57x57.png diff --git a/webroot/apple-icon-60x60.png b/webroot/img/favicon/apple-icon-60x60.png similarity index 100% rename from webroot/apple-icon-60x60.png rename to webroot/img/favicon/apple-icon-60x60.png diff --git a/webroot/apple-icon-72x72.png b/webroot/img/favicon/apple-icon-72x72.png similarity index 100% rename from webroot/apple-icon-72x72.png rename to webroot/img/favicon/apple-icon-72x72.png diff --git a/webroot/apple-icon-76x76.png b/webroot/img/favicon/apple-icon-76x76.png similarity index 100% rename from webroot/apple-icon-76x76.png rename to webroot/img/favicon/apple-icon-76x76.png diff --git a/webroot/apple-icon-precomposed.png b/webroot/img/favicon/apple-icon-precomposed.png similarity index 100% rename from webroot/apple-icon-precomposed.png rename to webroot/img/favicon/apple-icon-precomposed.png diff --git a/webroot/apple-icon.png b/webroot/img/favicon/apple-icon.png similarity index 100% rename from webroot/apple-icon.png rename to webroot/img/favicon/apple-icon.png diff --git a/webroot/browserconfig.xml b/webroot/img/favicon/browserconfig.xml similarity index 100% rename from webroot/browserconfig.xml rename to webroot/img/favicon/browserconfig.xml diff --git a/webroot/favicon-16x16.png b/webroot/img/favicon/favicon-16x16.png similarity index 100% rename from webroot/favicon-16x16.png rename to webroot/img/favicon/favicon-16x16.png diff --git a/webroot/favicon-32x32.png b/webroot/img/favicon/favicon-32x32.png similarity index 100% rename from webroot/favicon-32x32.png rename to webroot/img/favicon/favicon-32x32.png diff --git a/webroot/favicon-96x96.png b/webroot/img/favicon/favicon-96x96.png similarity index 100% rename from webroot/favicon-96x96.png rename to webroot/img/favicon/favicon-96x96.png diff --git a/webroot/ms-icon-144x144.png b/webroot/img/favicon/ms-icon-144x144.png similarity index 100% rename from webroot/ms-icon-144x144.png rename to webroot/img/favicon/ms-icon-144x144.png diff --git a/webroot/ms-icon-150x150.png b/webroot/img/favicon/ms-icon-150x150.png similarity index 100% rename from webroot/ms-icon-150x150.png rename to webroot/img/favicon/ms-icon-150x150.png diff --git a/webroot/ms-icon-310x310.png b/webroot/img/favicon/ms-icon-310x310.png similarity index 100% rename from webroot/ms-icon-310x310.png rename to webroot/img/favicon/ms-icon-310x310.png diff --git a/webroot/ms-icon-70x70.png b/webroot/img/favicon/ms-icon-70x70.png similarity index 100% rename from webroot/ms-icon-70x70.png rename to webroot/img/favicon/ms-icon-70x70.png diff --git a/webroot/index2.html b/webroot/index2.html index 8f2c70227..c79000245 100644 --- a/webroot/index2.html +++ b/webroot/index2.html @@ -3,22 +3,22 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + - + diff --git a/webroot/manifest.json b/webroot/manifest.json index 013d4a6a5..b724ebe23 100644 --- a/webroot/manifest.json +++ b/webroot/manifest.json @@ -2,40 +2,40 @@ "name": "App", "icons": [ { - "src": "\/android-icon-36x36.png", + "src": "\/img\/favicon\/android-icon-36x36.png", "sizes": "36x36", "type": "image\/png", "density": "0.75" }, { - "src": "\/android-icon-48x48.png", + "src": "\/img\/favicon\/android-icon-48x48.png", "sizes": "48x48", "type": "image\/png", "density": "1.0" }, { - "src": "\/android-icon-72x72.png", + "src": "\/img\/favicon\/android-icon-72x72.png", "sizes": "72x72", "type": "image\/png", "density": "1.5" }, { - "src": "\/android-icon-96x96.png", + "src": "\/img\/favicon\/android-icon-96x96.png", "sizes": "96x96", "type": "image\/png", "density": "2.0" }, { - "src": "\/android-icon-144x144.png", + "src": "\/img\/favicon\/android-icon-144x144.png", "sizes": "144x144", "type": "image\/png", "density": "3.0" }, { - "src": "\/android-icon-192x192.png", + "src": "\/img\/favicon\/android-icon-192x192.png", "sizes": "192x192", "type": "image\/png", "density": "4.0" } ] -} \ No newline at end of file +} From 66dc2f84c9fddb613b02b8a16378db83627dfe9b Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Fri, 21 Aug 2020 15:55:52 -0700 Subject: [PATCH 21/35] fix placehodler style, fix chat panel cookieing --- webroot/js/app2.js | 17 ++++- webroot/js/chat/chat-input.js | 1 + webroot/js/chat/message.js | 12 +-- webroot/js/utils.js | 2 - webroot/styles/chat.css | 98 +++++++++++------------- webroot/styles/layout.css | 1 - webroot/styles/user-content.css | 128 ++++++++++++++++++++------------ 7 files changed, 146 insertions(+), 113 deletions(-) diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 894178093..04ef2e5ac 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -1,4 +1,4 @@ -import { h, Component, Fragment } from 'https://unpkg.com/preact?module'; +import { h, Component } from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); @@ -11,6 +11,8 @@ import { OwncastPlayer } from './player.js'; import { getLocalStorage, + setLocalStorage, + clearLocalStorage, generateAvatar, generateUsername, URL_OWNCAST, @@ -25,7 +27,7 @@ import { MESSAGE_OFFLINE, MESSAGE_ONLINE, } from './utils.js'; -import { KEY_USERNAME, KEY_AVATAR, } from './utils/chat.js'; +import { KEY_USERNAME, KEY_AVATAR, KEY_CHAT_DISPLAYED } from './utils/chat.js'; export default class App extends Component { constructor(props, context) { @@ -33,7 +35,7 @@ export default class App extends Component { this.state = { websocket: new Websocket(), - displayChat: false, // chat panel state + displayChat: getLocalStorage(KEY_CHAT_DISPLAYED), // chat panel state chatEnabled: false, // chat input box state username: getLocalStorage(KEY_USERNAME) || generateUsername(), userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), @@ -274,8 +276,15 @@ export default class App extends Component { handleChatPanelToggle() { const { displayChat: curDisplayed } = this.state; + + const displayChat = !curDisplayed; + if (displayChat) { + setLocalStorage(KEY_CHAT_DISPLAYED, displayChat); + } else { + clearLocalStorage(KEY_CHAT_DISPLAYED); + } this.setState({ - displayChat: !curDisplayed, + displayChat, }); } diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index dcc904ad5..c42eea08d 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -239,6 +239,7 @@ export default class ChatInput extends Component {
    <${ContentEditable} + id="message-input" 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" placeholderText=${placeholderText} diff --git a/webroot/js/chat/message.js b/webroot/js/chat/message.js index 1e52a76d3..985725581 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/chat/message.js @@ -27,10 +27,10 @@ export default class Message extends Component { >
    -
    +

    ${author}

    - -
    - ${oldName} is now known as ${newName}. +
    + +
    + ${oldName} is now known as ${newName}. +
    `); diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 7beda7a1e..da32a79db 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -15,8 +15,6 @@ export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAE export const MESSAGE_OFFLINE = 'Stream is offline.'; export const MESSAGE_ONLINE = 'Stream is online'; - - export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer export function getLocalStorage(key) { diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index c291a9a8d..18e026409 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -9,11 +9,54 @@ padding: 1em; } -#message-form { +/******************************/ +/******************************/ +#message-input { + height: 5rem; + font-size: .85em; +} +#message-input img { + display: inline; + vertical-align: middle; + padding: 5px; +} + +#message-input .emoji { + width: 2.2em; +} + + +/* If the div is empty then show the placeholder */ +#message-input:empty:before{ + content: attr(placeholderText); + pointer-events: none; + display: block; /* For Firefox */ + color: rgba(0, 0, 0, 0.5); +} + +/* When chat is enabled (contenteditable=true) */ +#message-input[contenteditable=true]:before { + opacity: 1.0; +} + + +/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ +#message-input:disabled, +#message-input[contenteditable=false] { + opacity: 0.6; +} +/******************************/ +/******************************/ + + + + + +/* #message-form { flex-direction: column; align-items: flex-end; margin-bottom: 0; -} +} */ @@ -122,56 +165,5 @@ -/* -The chat input has a fake placeholder that is styled below. -It pulls the placeholder text from the div's placeholder attribute. -But really it's just the innerHTML content. -*/ - - - - - - - -#message-body-form { - font-size: 1em; - height: 60px; -} -#message-body-form:disabled{ - opacity: .5; -} -#message-body-form img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} - -#message-body-form .emoji { - width: 40px; -} - - -/* If the div is empty then show the placeholder */ -#message-body-form:empty:before{ - content: attr(placeholder); - pointer-events: none; - display: block; /* For Firefox */ - - /* Style the div's placeholder text color */ - color: rgba(0, 0, 0, 0.5); -} - -/* When chat is enabled (contenteditable=true) */ -#message-body-form[contenteditable=true]:before { - opacity: 1.0; -} - - -/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ -#message-body-form[contenteditable=false] { - opacity: 0.6; -} - diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index 9eb6a6384..ca914b538 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -242,7 +242,6 @@ h2 { } .vjs-airplay .vjs-icon-placeholder::before { - /* content: 'AP'; */ content: url("../img/airplay.png"); } diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css index dea858f08..32a4d99b4 100644 --- a/webroot/styles/user-content.css +++ b/webroot/styles/user-content.css @@ -55,7 +55,7 @@ --icon-width: 40px; height: var(--icon-width); width: var(--icon-width); - background-image: url(../img/social-icons.gif); + background-image: url(/img/social-icons.gif); background-repeat: no-repeat; background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); transform: scale(.65); @@ -69,105 +69,137 @@ } +/* +EXTRA CUSTOM CONTENT STYLES +Assumes markup converted from markdown input. +*/ - -.extra-user-content { +#extra-user-content { padding: 1em 3em 3em 3em; } -.extra-user-content ol { +#extra-user-content ol { list-style: decimal; } -.extra-user-content ul { +#extra-user-content ul { list-style: unset; } -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 { +#extra-user-content h1, +#extra-user-content h2, +#extra-user-content h3, +#extra-user-content h4 { color: #111111; - font-weight: 400; } + font-weight: 400; +} -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p { +#extra-user-content h1, +#extra-user-content h2, +#extra-user-content h3, +#extra-user-content h4, +#extra-user-content h5, +#extra-user-content p { margin-bottom: 24px; - padding: 0; } + padding: 0; +} -.extra-user-content h1 { - font-size: 48px; } +#extra-user-content h1 { + font-size: 48px; +} -.extra-user-content h2 { +#extra-user-content h2 { font-size: 36px; - margin: 24px 0 6px; } + margin: 24px 0 6px; +} -.extra-user-content h3 { - font-size: 24px; } +#extra-user-content h3 { + font-size: 24px; +} -.extra-user-content h4 { - font-size: 21px; } +#extra-user-content h4 { + font-size: 21px; +} -.extra-user-content h5 { - font-size: 18px; } +#extra-user-content h5 { + font-size: 18px; +} -.extra-user-content a { +#extra-user-content a { color: #0099ff; margin: 0; padding: 0; - vertical-align: baseline; } + vertical-align: baseline; +} -.extra-user-content ul, .extra-user-content ol { +#extra-user-content ul, #extra-user-content ol { padding: 0; - margin: 0; } + margin: 0; +} -.extra-user-content li { - line-height: 24px; } +#extra-user-content li { + line-height: 24px; +} -.extra-user-content li ul, .extra-user-content li ul { +#extra-user-content li ul, #extra-user-content li ul { margin-left: 24px; } -.extra-user-content p, .extra-user-content ul, .extra-user-content ol { +#extra-user-content p, #extra-user-content ul, #extra-user-content ol { font-size: 16px; line-height: 24px; - } -.extra-user-content pre { +} + +#extra-user-content pre { padding: 0px 24px; max-width: 800px; - white-space: pre-wrap; } + white-space: pre-wrap; +} -.extra-user-content code { +#extra-user-content code { font-family: Consolas, Monaco, Andale Mono, monospace; line-height: 1.5; - font-size: 13px; } + font-size: 13px; +} -.extra-user-content aside { +#extra-user-content aside { display: block; float: right; - width: 390px; } + width: 390px; +} -.extra-user-content blockquote { +#extra-user-content blockquote { margin: 1em 2em; - max-width: 476px; } + max-width: 476px; +} -.extra-user-content blockquote p { +#extra-user-content blockquote p { color: #666; - max-width: 460px; } + max-width: 460px; +} -.extra-user-content hr { +#extra-user-content hr { width: 540px; text-align: left; margin: 0 auto 0 0; - color: #999; } + color: #999; +} -.extra-user-content table { +#extra-user-content table { border-collapse: collapse; margin: 1em 1em; - border: 1px solid #CCC; } + border: 1px solid #CCC; +} -.extra-user-content table thead { - background-color: #EEE; } +#extra-user-content table thead { + background-color: #EEE; +} -.extra-user-content table thead td { - color: #666; } +#extra-user-content table thead td { + color: #666; +} -.extra-user-content table td { +#extra-user-content table td { padding: 0.5em 1em; - border: 1px solid #CCC; } + border: 1px solid #CCC; +} From 13cfd112b7662e7f6a8fbf25834fb479c890714b Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Fri, 21 Aug 2020 23:44:10 -0700 Subject: [PATCH 22/35] - reduced custom styles, use mostly tailwind classes - updated markdown css for extrausercontent --- webroot/index2.html | 2 +- webroot/js/app2.js | 59 ++++++----- webroot/js/chat/chat-input.js | 9 +- webroot/js/chat/chat.js | 14 +-- webroot/js/chat/message.js | 17 ++-- webroot/js/chat/username.js | 14 +-- webroot/js/emoji.js | 43 -------- webroot/js/social.js | 92 +++-------------- webroot/js/utils.js | 16 ++- webroot/js/utils/chat.js | 5 +- webroot/js/utils/social.js | 73 ++++++++++++++ webroot/styles/chat.css | 112 +++++++++++++-------- webroot/styles/layout.css | 159 +++++++++++++---------------- webroot/styles/user-content.css | 172 +++++++++++++++++--------------- 14 files changed, 392 insertions(+), 395 deletions(-) delete mode 100644 webroot/js/emoji.js create mode 100644 webroot/js/utils/social.js diff --git a/webroot/index2.html b/webroot/index2.html index 8f2c70227..59d44b4e0 100644 --- a/webroot/index2.html +++ b/webroot/index2.html @@ -66,7 +66,7 @@ }
    - +

    This Owncast stream requires Javascript to play. diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 04ef2e5ac..551693292 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -332,7 +332,7 @@ export default class App extends Component { const tagList = !tags.length ? null : tags.map((tag, index) => html` -

  • ${tag}
  • +
  • ${tag}
  • `); const socialIconsList = @@ -350,26 +350,26 @@ export default class App extends Component { const streamInfoClass = streamOnline ? 'online' : ''; return ( html` -
    +
    -
    -

    +
    +

    - + - ${title} + ${title}

    -
    +
    <${UsernameForm} username=${username} userAvatarImage=${userAvatarImage} handleUsernameChange=${this.handleUsernameChange} /> - +

    @@ -377,21 +377,20 @@ export default class App extends Component {
    + >
    -
    +
    ${streamStatusMessage} ${viewerCount} ${pluralize('viewer', viewerCount)}. Max ${pluralize('viewer', sessionMaxViewerCount)}. @@ -399,47 +398,45 @@ export default class App extends Component {
    -
    -
    +
    +
    + src=${largeLogo}/>
    -

    - About - ${streamerName} +

    + About ${streamerName}

    -
    -
    - +
    + About Owncast - Version ${appVersion} + Version ${appVersion}
    -
    + <${Chat} websocket=${websocket} diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index c42eea08d..0a8d3717f 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -236,11 +236,11 @@ export default class ChatInput extends Component { const placeholderText = generatePlaceholderText(inputEnabled, hasSentFirstChatMessage); return ( html` -
    +
    <${ContentEditable} id="message-input" - 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-sm text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white h-20 overflow-auto" placeholderText=${placeholderText} innerRef=${this.formMessageInput} @@ -254,13 +254,14 @@ export default class ChatInput extends Component { onPaste=${this.handlePaste} /> -
    +
    ${inputWarning} -
    +
    + - +
    `); diff --git a/webroot/js/emoji.js b/webroot/js/emoji.js deleted file mode 100644 index 23a212828..000000000 --- a/webroot/js/emoji.js +++ /dev/null @@ -1,43 +0,0 @@ -// DEPRECATE. -import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button' - -fetch('/emoji') - .then(response => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then(json => { - setupEmojiPickerWithCustomEmoji(json); - }) - .catch(error => { - this.handleNetworkingError(`Emoji Fetch: ${error}`); - }); - -function setupEmojiPickerWithCustomEmoji(customEmoji) { - const picker = new EmojiButton({ - zIndex: 100, - theme: 'dark', - custom: customEmoji, - initialCategory: 'custom', - showPreview: false, - position: { - top: '50%', - right: '100' - } - }); - 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; - const name = url.split('\\').pop().split('/').pop(); - document.querySelector('#message-body-form').innerHTML += "\"""; - } else { - document.querySelector('#message-body-form').innerHTML += emoji.emoji; - } - }); -} diff --git a/webroot/js/social.js b/webroot/js/social.js index ec37ad558..dc46920a2 100644 --- a/webroot/js/social.js +++ b/webroot/js/social.js @@ -1,76 +1,6 @@ import { html } from "https://unpkg.com/htm/preact/index.mjs?module"; - -const SOCIAL_PLATFORMS = { - default: { - name: "default", - imgPos: [0,0], // [row,col] - }, - - facebook: { - name: "Facebook", - imgPos: [0,1], - }, - twitter: { - name: "Twitter", - imgPos: [0,2], - }, - instagram: { - name: "Instagram", - imgPos: [0,3], - }, - snapchat: { - name: "Snapchat", - imgPos: [0,4], - }, - tiktok: { - name: "TikTok", - imgPos: [0,5], - }, - soundcloud: { - name: "Soundcloud", - imgPos: [0,6], - }, - bandcamp: { - name: "Bandcamp", - imgPos: [0,7], - }, - patreon: { - name: "Patreon", - imgPos: [0,1], - }, - youtube: { - name: "YouTube", - imgPos: [0,9 ], - }, - spotify: { - name: "Spotify", - imgPos: [0,10], - }, - twitch: { - name: "Twitch", - imgPos: [0,11], - }, - paypal: { - name: "Paypal", - imgPos: [0,12], - }, - github: { - name: "Github", - imgPos: [0,13], - }, - linkedin: { - name: "LinkedIn", - imgPos: [0,14], - }, - discord: { - name: "Discord", - imgPos: [0,15], - }, - mastodon: { - name: "Mastodon", - imgPos: [0,16], - }, -}; +import { SOCIAL_PLATFORMS } from './utils/social.js'; +import { classNames } from './utils.js'; export default function SocialIcon(props) { const { platform, url } = props; @@ -82,20 +12,28 @@ export default function SocialIcon(props) { const name = inList ? platformInfo.name : platform; const style = `--imgRow: -${imgRow}; --imgCol: -${imgCol};`; - const itemClass = { + const itemClass = classNames({ "user-social-item": true, "flex": true, + "justify-start": true, + "items-center": true, + "-mr-1": true, "use-default": !inList, - }; - const labelClass = { + }); + const labelClass = classNames({ "platform-label": true, "visually-hidden": inList, "text-indigo-800": true, - }; + "text-xs": true, + "uppercase": true, + "max-w-xs": true, + "inline-block": true, + }); + return ( html` - + Find me on ${name} `); diff --git a/webroot/js/utils.js b/webroot/js/utils.js index da32a79db..2be01ff7f 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -98,7 +98,7 @@ export function hasTouchScreen() { export function generateAvatar(hash) { const avatarSource = 'https://robohash.org/'; const optionSize = '?size=80x80'; - const optionSet = '&set=set3'; + const optionSet = '&set=set2'; const optionBg = ''; // or &bgset=bg1 or bg2 return avatarSource + hash + optionSize + optionSet + optionBg; @@ -133,3 +133,17 @@ export function setVHvar() { export function doesObjectSupportFunction(object, functionName) { return typeof object[functionName] === "function"; } + +// return a string of css classes +export function classNames(json) { + const classes = []; + + Object.entries(json).map(function(item) { + const [ key, value ] = item; + if (value) { + classes.push(key); + } + return null; + }); + return classes.join(' '); +} diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 49ea94925..067af1c50 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -28,7 +28,7 @@ export function formatMessageText(message, username) { function highlightUsername(message, username) { const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); - return message.replace(pattern, '$&'); + return message.replace(pattern, '$&'); } function linkify(text, rawText) { @@ -126,8 +126,7 @@ function getInstagramEmbedFromURL(url) { function isImage(url) { const re = /\.(jpe?g|png|gif)$/i; - const isImage = re.test(url); - return isImage; + return re.test(url); } function getImageForURL(url) { diff --git a/webroot/js/utils/social.js b/webroot/js/utils/social.js new file mode 100644 index 000000000..dad57a546 --- /dev/null +++ b/webroot/js/utils/social.js @@ -0,0 +1,73 @@ + +// x, y pixel psitions of /img/social.gif image. +export const SOCIAL_PLATFORMS = { + default: { + name: "default", + imgPos: [0,0], // [row,col] + }, + + facebook: { + name: "Facebook", + imgPos: [0,1], + }, + twitter: { + name: "Twitter", + imgPos: [0,2], + }, + instagram: { + name: "Instagram", + imgPos: [0,3], + }, + snapchat: { + name: "Snapchat", + imgPos: [0,4], + }, + tiktok: { + name: "TikTok", + imgPos: [0,5], + }, + soundcloud: { + name: "Soundcloud", + imgPos: [0,6], + }, + bandcamp: { + name: "Bandcamp", + imgPos: [0,7], + }, + patreon: { + name: "Patreon", + imgPos: [0,1], + }, + youtube: { + name: "YouTube", + imgPos: [0,9 ], + }, + spotify: { + name: "Spotify", + imgPos: [0,10], + }, + twitch: { + name: "Twitch", + imgPos: [0,11], + }, + paypal: { + name: "Paypal", + imgPos: [0,12], + }, + github: { + name: "Github", + imgPos: [0,13], + }, + linkedin: { + name: "LinkedIn", + imgPos: [0,14], + }, + discord: { + name: "Discord", + imgPos: [0,15], + }, + mastodon: { + name: "Mastodon", + imgPos: [0,16], + }, +}; diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index 18e026409..f308a29b4 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -1,28 +1,58 @@ +#chat-container { + position: fixed; + z-index: 9; + top: var(--header-height); + right: 0; + width: var(--right-col-width); + + height: calc(100vh - var(--header-height)); + + /* overflow: hidden; */ + /* display: flex; + flex-direction: column; + justify-content: flex-end; */ +} + +.touch-screen #chat-container { + height: calc(100vh - var(--header-height) - 3vh); +} + + +.no-chat #chat-container-wrap { + display: none; +} + +.chat #chat-container-wrap { + display: block; +} + + #messages-container { - overflow: auto; - padding: 1em 0; + /* overflow: auto; + padding: 1em 0; */ } #message-input-container { - width: 100%; - padding: 1em; + /* width: 100%; */ + /* padding: 1em; */ } /******************************/ /******************************/ #message-input { - height: 5rem; - font-size: .85em; + /* height: 5rem; */ + /* font-size: .85em; */ } #message-input img { display: inline; vertical-align: middle; - padding: 5px; + padding: .25rem; } #message-input .emoji { - width: 2.2em; + width: 2.2rem; + padding: .25rem; } @@ -61,23 +91,23 @@ #message-form-actions { - flex-direction: row; + /* flex-direction: row; justify-content: space-between; align-items: center; - width: 100%; + width: 100%; */ } #message-form-actions-buttons { - flex-direction: row; + /* flex-direction: row; justify-content: flex-end; - align-items: center; + align-items: center; */ } /* Emoji picker button */ #emoji-button { - font-size: 1.75em; - cursor: pointer; - margin-right: .5em; + /* font-size: 1.75em; */ + /* cursor: pointer; + margin-right: .5em; */ } .emoji-picker__emoji { border-radius: 10px; @@ -85,11 +115,13 @@ .message { - padding: .85em; - align-items: flex-start; + /* padding: .85em; */ + /* align-items: flex-start; */ } .message-avatar { - margin-right: .75em; + height: 3.0em; + width: 3.0em; + /* margin-right: .75em; */ } .message-avatar img { max-width: unset; @@ -99,17 +131,17 @@ } .message-content { - font-size: .85em; + /* font-size: .85em; */ max-width: 85%; - word-wrap: break-word; + /* word-wrap: break-word; */ } -/* MESSAGE TEXT CONTENT */ -/* MESSAGE TEXT CONTENT */ -/* MESSAGE TEXT CONTENT */ +/* MESSAGE TEXT HTML */ +/* MESSAGE TEXT HTML */ +/* MESSAGE TEXT HTML */ .message-text a { color: #7F9CF5; /* indigo-400 */ } @@ -119,41 +151,43 @@ .message-text img { display: inline; - padding-left: 5px; - padding-right: 5px; + padding-left: 0 .25rem; +} + + + +.message-text .emoji { + width: 3rem; + padding: .25rem } .message-text code { + font-family: monospace; background-color:darkslategrey; - padding: 3px; + padding: .25rem; } -.message-text .emoji { - width: 60px; -} - - .message-text iframe { width: 100%; - height: 170px; - border-radius: 15px; + height: 12rem; + border-radius: 1rem; } .message-text .instagram-embed { - height: 314px; + height: 22em; } .message-text .embedded-image { width: 100%; - height: 170px; - border-radius: 15px; + display: block; + height: 15rem; + border-radius: 1rem; } .message-text .highlighted { - color: orange; + /* color: orange; font-weight: 400; - font-size: 14px; -} +} */ /* MESSAGE TEXT CONTENT */ /* MESSAGE TEXT CONTENT */ /* MESSAGE TEXT CONTENT */ diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index ca914b538..56474415c 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -39,90 +39,90 @@ a:hover { } #app-container { - width: 100%; + /* width: 100%; flex-direction: column; justify-content: flex-start; - position: relative; + position: relative; */ } header { - position: fixed; - width: 100%; + /* position: fixed; + width: 100%; */ height: var(--header-height); - top: 0; - left: 0; + /* top: 0; + left: 0; */ background-color: var(--header-bg-color); - z-index: 10; - flex-direction: row; - justify-content: space-between; - flex-wrap: nowrap; + /* z-index: 10; */ + /* flex-direction: row; + justify-content: space-between; */ + /* flex-wrap: nowrap; */ } header h1 { - font-size: 1.25em; - font-weight: 100; - letter-spacing: 1.2; - text-transform: uppercase; - padding: .5em; - white-space: nowrap; - justify-content: flex-start; + /* font-size: 1.25em; */ + /* font-weight: 100; + letter-spacing: 1.2; */ + /* text-transform: uppercase; */ + /* padding: .5em; */ + /* white-space: nowrap; */ + /* justify-content: flex-start; align-items: center; - flex-direction: row; - overflow: hidden; + flex-direction: row; */ + /* overflow: hidden; */ } #logo-container{ - height: 1.75em; - width: 1.75em; - min-height: 1.75em; - min-width: 1.75em; - margin-right: .5em; - display: inline-block; - background-repeat: no-repeat; - background-position: center center; + /* height: 1.75em; + width: 1.75em; */ + /* min-height: 1.75em; + min-width: 1.75em; */ + /* margin-right: .5em; */ + /* display: inline-block; */ + /* background-repeat: no-repeat; */ + /* background-position: center center; */ background-size: 1.35em; } header .instance-title { - overflow: hidden; - text-overflow: ellipsis; + /* overflow: hidden; + text-overflow: ellipsis; */ } #chat-toggle { - cursor: pointer; - text-align: center; - height: 100%; - min-width: 3em; - justify-content: center; - align-items: center; + /* cursor: pointer; + text-align: center; */ + /* height: 100%;*/ + min-width: 3rem; +/* justify-content: center; + align-items: center; */ } footer { - flex-direction: row; + /* flex-direction: row; justify-content: flex-start; font-size: .75em; padding: 2em; - opacity: .5; + opacity: .5; */ } footer span { - display: inline-block; - margin: 0 1em; + /* display: inline-block; + margin: 0 1em; */ } /* ************************************************8 */ #stream-info { - padding: .5em 2em; - text-align: center; + /* padding: .5em 2em; */ + /* text-align: center; width: 100%; flex-direction: row; - justify-content: space-between; + justify-content: space-between; */ } #stream-info span { - font-size: .7em; + /* font-size: .7em; */ } .user-content { - padding: 2em; + /* padding: 2em; */ } /* #user-content { display: block; @@ -141,73 +141,73 @@ footer span { height: 100%; } */ .stream-summary { - margin: 1em 0; + /* margin: 1em 0; */ } h2 { - font-size: 3em; + /* font-size: 3em; */ } /* ************************************************8 */ #user-options-container { - flex-direction: row; + /* flex-direction: row; justify-content: flex-end; align-items: center; - flex-wrap: nowrap; + flex-wrap: nowrap; */ } #user-info-display { - display: flex; + /* display: flex; flex-direction: row; justify-content: flex-end; align-items: center; cursor: pointer; padding: .5em 1em; overflow: hidden; - width: 100%; + width: 100%; */ } #username-avatar { - height: 2.1em; + /* height: 2.1em; width: 2.1em; - margin-right: .5em; + margin-right: .5em; */ } #username-display { - font-weight: 600; - font-size: .75em; - white-space: nowrap; + /* font-weight: 600; */ + /* font-size: .75em; */ + /* white-space: nowrap; text-overflow: ellipsis; - overflow: hidden; + overflow: hidden; */ } #user-info-display:hover { - transition: opacity .2s; - opacity: .75; + /* transition: opacity .2s; + opacity: .75; */ } #user-info-change { display: none; - justify-content: flex-end; + /* justify-content: flex-end; align-items: center; - padding: .25em; + padding: .25em; */ } #username-change-input { - font-size: .75em; + /* font-size: .75em; */ } #button-update-username { - font-size: .65em; - text-transform: uppercase; - height: 2.5em; + /* font-size: .65em; */ + /* text-transform: uppercase; */ + /* height: 2.5em; */ } #button-cancel-change { - cursor: pointer; - height: 2.5em; - font-size: .65em; + /* cursor: pointer; */ + /* height: 2.5em; */ + /* font-size: .65em; */ } .user-btn { - margin: 0 .25em; + /* margin: 0 .25em; */ } /* ************************************************8 */ @@ -260,16 +260,10 @@ h2 { /* ************************************************8 */ -.no-chat #chat-container-wrap { - display: none; -} .no-chat footer { justify-content: center; } -.chat #chat-container-wrap { - display: block; -} .chat #video-container, .chat #stream-info, @@ -278,23 +272,6 @@ h2 { } -#chat-container { - position: fixed; - z-index: 9; - top: var(--header-height); - right: 0; - width: var(--right-col-width); - - height: calc(100vh - var(--header-height)); - - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: flex-end; -} -.touch-screen #chat-container { - height: calc(100vh - var(--header-height) - 3vh); -} diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css index 32a4d99b4..b2671a503 100644 --- a/webroot/styles/user-content.css +++ b/webroot/styles/user-content.css @@ -1,71 +1,71 @@ .user-content { - padding: 3em; + /* padding: 3rem; */ - display: flex; - flex-direction: row; + /* display: flex; + flex-direction: row; */ } .user-content .user-image { - padding: 1em; - margin-right: 2em; + /* padding: 1rem; + margin-right: 2rem; */ min-width: var(--user-image-width); width: var(--user-image-width); height: var(--user-image-width); max-height: var(--user-image-width); - background-repeat: no-repeat; - background-position: center center; + /* background-repeat: no-repeat; + background-position: center center; */ background-size: calc(var(--user-image-width) - 1em); } .user-content-header { - margin-bottom: 2em; + /* margin-bottom: 2rem; */ } .tag-list { - flex-direction: row; - margin: 1em 0; + /* flex-direction: row; */ + /* margin: 1em 0; */ } .tag-list li { - font-size: .75em; - text-transform: uppercase; - margin-right: .75em; - padding: .5em; + /* font-size: .75rem; */ + /* text-transform: uppercase; */ + /* margin-right: .75rem; */ + /* padding: .5rem; */ } .social-list { - flex-direction: row; - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; + /* flex-direction: row; */ + /* align-items: center; */ + /* justify-content: flex-start; */ + /* flex-wrap: wrap; */ } .social-list .follow-label { - font-weight: bold; - font-size: .75em; - margin-right: .5em; - text-transform: uppercase; + /* font-weight: bold; */ + /* font-size: .75rem; */ + /* margin-right: .5rem; */ + /* text-transform: uppercase; */ } .user-social-item { - display: flex; + /* display: flex; justify-content: flex-start; align-items: center; - margin-right: -.25em; + margin-right: -.25rem; */ } .user-social-item .platform-icon { --icon-width: 40px; height: var(--icon-width); width: var(--icon-width); background-image: url(/img/social-icons.gif); - background-repeat: no-repeat; + /* background-repeat: no-repeat; */ background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); transform: scale(.65); } .user-social-item.use-default .platform-label { - font-size: .7em; - text-transform: uppercase; - display: inline-block; - max-width: 10em; + /* font-size: .7rem; */ + /* text-transform: uppercase; */ + /* display: inline-block; */ + /* max-width: 10rem; */ } @@ -75,124 +75,134 @@ Assumes markup converted from markdown input. */ #extra-user-content { - padding: 1em 3em 3em 3em; + /* padding: 1rem 3rem 3rem 3rem; */ +} + +#extra-user-content ul, +#extra-user-content ol { + margin: 0; + padding: 0; } #extra-user-content ol { list-style: decimal; + margin-left: 1.5rem; } #extra-user-content ul { list-style: unset; + margin-left: 1.5rem; } -#extra-user-content h1, +/* #extra-user-content h1, #extra-user-content h2, #extra-user-content h3, #extra-user-content h4 { color: #111111; font-weight: 400; -} +} */ #extra-user-content h1, #extra-user-content h2, #extra-user-content h3, #extra-user-content h4, #extra-user-content h5, -#extra-user-content p { - margin-bottom: 24px; +#extra-user-content h6 { + margin: 0; padding: 0; + margin: 1.5rem 0 .5rem; + font-weight: 600; + line-height: 1.2; } + + #extra-user-content h1 { - font-size: 48px; + font-size: 2.1rem; } #extra-user-content h2 { - font-size: 36px; - margin: 24px 0 6px; + font-size: 1.8rem; } #extra-user-content h3 { - font-size: 24px; + font-size: 1.5rem; } #extra-user-content h4 { - font-size: 21px; + font-size: 1.2rem; } #extra-user-content h5 { - font-size: 18px; + font-size: 1.25rem; +} +#extra-user-content h6 { + font-weight: 400; + font-size: 1rem; +} + +#extra-user-content p { + margin-top: 0; + margin-bottom: 1rem; } #extra-user-content a { color: #0099ff; - margin: 0; - padding: 0; - vertical-align: baseline; -} - -#extra-user-content ul, #extra-user-content ol { - padding: 0; - margin: 0; } #extra-user-content li { - line-height: 24px; + line-height: 1.5rem; } -#extra-user-content li ul, #extra-user-content li ul { - margin-left: 24px; } - -#extra-user-content p, #extra-user-content ul, #extra-user-content ol { - font-size: 16px; - line-height: 24px; - +#extra-user-content li ul, +#extra-user-content li ul { + margin-left: 1.5rem; } -#extra-user-content pre { - padding: 0px 24px; - max-width: 800px; - white-space: pre-wrap; + + +#extra-user-content blockquote { + border-left: .25rem solid #bbc; + padding: 0 1rem; +} +#extra-user-content blockquote p { + margin: 1rem 0; } +#extra-user-content pre, #extra-user-content code { - font-family: Consolas, Monaco, Andale Mono, monospace; - line-height: 1.5; - font-size: 13px; + font-family: monospace; + font-size: .85rem; + background-color: #eee; + color: #900; +} +#extra-user-content pre { + margin: 1rem 0; + padding: 1rem; + max-width: 80%; + white-space: pre-wrap; } #extra-user-content aside { display: block; float: right; - width: 390px; -} - -#extra-user-content blockquote { - margin: 1em 2em; - max-width: 476px; -} - -#extra-user-content blockquote p { - color: #666; - max-width: 460px; + width: 35%; } #extra-user-content hr { - width: 540px; - text-align: left; - margin: 0 auto 0 0; - color: #999; + width: 100%; + border-top: 1px solid #666; + margin-bottom: 1rem; } #extra-user-content table { border-collapse: collapse; - margin: 1em 1em; + margin: 1em 1rem; border: 1px solid #CCC; } #extra-user-content table thead { - background-color: #EEE; + background-color: #eee; } #extra-user-content table thead td { @@ -200,6 +210,6 @@ Assumes markup converted from markdown input. } #extra-user-content table td { - padding: 0.5em 1em; + padding: 0.5rem 1rem; border: 1px solid #CCC; } From 9b4c07d3bad3e2ae4dfe6d3e338bd1c51259a285 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Sat, 22 Aug 2020 00:09:01 -0700 Subject: [PATCH 23/35] stylin' --- webroot/js/utils/chat.js | 6 +++--- webroot/styles/chat.css | 18 ++++++++++++------ webroot/styles/layout.css | 10 +++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 067af1c50..4ce504f43 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -115,13 +115,13 @@ function getYoutubeIdFromURL(url) { } function getYoutubeEmbedFromID(id) { - return ``; + return ``; } function getInstagramEmbedFromURL(url) { const urlObject = new URL(url.replace(/\/$/, "")); urlObject.pathname += "/embed"; - return ``; + return ``; } function isImage(url) { @@ -130,7 +130,7 @@ function isImage(url) { } function getImageForURL(url) { - return ``; + return ``; } // Taken from https://stackoverflow.com/questions/3972014/get-contenteditable-caret-index-position diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index f308a29b4..85b7eaaa4 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -167,21 +167,27 @@ padding: .25rem; } -.message-text iframe { + + +.message-text .chat-embed { width: 100%; - height: 12rem; - border-radius: 1rem; + border-radius: .25rem; } .message-text .instagram-embed { - height: 22em; + height: 24rem; } + .message-text .embedded-image { width: 100%; display: block; - height: 15rem; - border-radius: 1rem; + /* height: 15rem; */ +} + +.message-text .youtube-embed { + width: 100%; + height: 12rem; } .message-text .highlighted { diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index 56474415c..aaa50fe29 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -215,20 +215,20 @@ h2 { #video-container { height: calc(var(--video-container-height)); - width: 100%; + /* width: 100%; */ margin-top: var(--header-height); - background-position: center center; - background-repeat: no-repeat; + /* background-position: center center; + background-repeat: no-repeat; */ background-size: 30%; } .owncast-video-container { height: auto; - display: flex; + /* display: flex; flex-direction: column; justify-content: flex-start; - align-items: center; + align-items: center; */ } .owncast-video-container .video-js { width: 100%; From 786b4c04fa0ad6bca901eb39aa983f11302a6066 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Sun, 23 Aug 2020 18:33:19 -0700 Subject: [PATCH 24/35] cleanup commented out styles --- webroot/index2.html | 14 ++- webroot/js/app2.js | 2 +- webroot/js/chat/username.js | 2 +- webroot/styles/chat.css | 63 +---------- webroot/styles/layout.css | 178 ++------------------------------ webroot/styles/user-content.css | 65 ------------ 6 files changed, 24 insertions(+), 300 deletions(-) diff --git a/webroot/index2.html b/webroot/index2.html index 63920763b..babc44c3f 100644 --- a/webroot/index2.html +++ b/webroot/index2.html @@ -1,3 +1,14 @@ + + @@ -28,7 +39,8 @@ - + + diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 551693292..35e6bd6bd 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -390,7 +390,7 @@ export default class App extends Component {
    -
    +
    ${streamStatusMessage} ${viewerCount} ${pluralize('viewer', viewerCount)}. Max ${pluralize('viewer', sessionMaxViewerCount)}. diff --git a/webroot/js/chat/username.js b/webroot/js/chat/username.js index babf6d300..bb67ba260 100644 --- a/webroot/js/chat/username.js +++ b/webroot/js/chat/username.js @@ -87,7 +87,7 @@ export default class UsernameForm extends Component { ${username}
    -
    +
    Date: Sun, 23 Aug 2020 19:06:58 -0700 Subject: [PATCH 25/35] reorganize js files --- webroot/js/app2.js | 23 +- webroot/js/components.js | 64 --- .../js/{ => components}/chat/chat-input.js | 11 +- webroot/js/{ => components}/chat/chat.js | 12 +- .../{ => components}/chat/content-editable.js | 1 - webroot/js/{ => components}/chat/message.js | 8 +- webroot/js/{ => components}/chat/username.js | 4 +- webroot/js/{ => components}/player.js | 0 webroot/js/{ => components}/social.js | 4 +- webroot/js/message.js | 440 ------------------ .../standalone.js => standalone-chat-app.js} | 11 +- webroot/js/utils/chat.js | 19 +- webroot/js/utils/constants.js | 29 ++ webroot/js/{utils.js => utils/helpers.js} | 19 - webroot/js/utils/social.js | 1 - webroot/js/utils/socket-message-types.js | 11 - webroot/js/{ => utils}/websocket.js | 24 +- webroot/standalone-chat.html | 2 +- webroot/styles/app.css | 172 +++++++ webroot/styles/video.css | 55 +++ 20 files changed, 316 insertions(+), 594 deletions(-) delete mode 100644 webroot/js/components.js rename webroot/js/{ => components}/chat/chat-input.js (97%) rename webroot/js/{ => components}/chat/chat.js (94%) rename webroot/js/{ => components}/chat/content-editable.js (99%) rename webroot/js/{ => components}/chat/message.js (88%) rename webroot/js/{ => components}/chat/username.js (96%) rename webroot/js/{ => components}/player.js (100%) rename webroot/js/{ => components}/social.js (91%) delete mode 100644 webroot/js/message.js rename webroot/js/{chat/standalone.js => standalone-chat-app.js} (88%) create mode 100644 webroot/js/utils/constants.js rename webroot/js/{utils.js => utils/helpers.js} (79%) delete mode 100644 webroot/js/utils/socket-message-types.js rename webroot/js/{ => utils}/websocket.js (91%) create mode 100644 webroot/styles/app.css create mode 100644 webroot/styles/video.css diff --git a/webroot/js/app2.js b/webroot/js/app2.js index 35e6bd6bd..d0151b8f8 100644 --- a/webroot/js/app2.js +++ b/webroot/js/app2.js @@ -2,12 +2,11 @@ import { h, Component } from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); - -import SocialIcon from './social.js'; -import UsernameForm from './chat/username.js'; -import Chat from './chat/chat.js'; -import Websocket from './websocket.js'; -import { OwncastPlayer } from './player.js'; +import { OwncastPlayer } from './components/player.js'; +import SocialIcon from './components/social.js'; +import UsernameForm from './components/chat/username.js'; +import Chat from './components/chat/chat.js'; +import Websocket from './utils/websocket.js'; import { getLocalStorage, @@ -15,19 +14,23 @@ import { clearLocalStorage, generateAvatar, generateUsername, + addNewlines, + pluralize, +} from './utils/helpers.js'; +import { URL_OWNCAST, URL_CONFIG, URL_STATUS, - addNewlines, - pluralize, TIMER_STATUS_UPDATE, TIMER_DISABLE_CHAT_AFTER_OFFLINE, TIMER_STREAM_DURATION_COUNTER, TEMP_IMAGE, MESSAGE_OFFLINE, MESSAGE_ONLINE, -} from './utils.js'; -import { KEY_USERNAME, KEY_AVATAR, KEY_CHAT_DISPLAYED } from './utils/chat.js'; + KEY_USERNAME, + KEY_AVATAR, + KEY_CHAT_DISPLAYED, +} from './utils/constants.js'; export default class App extends Component { constructor(props, context) { diff --git a/webroot/js/components.js b/webroot/js/components.js deleted file mode 100644 index 959e2aa1d..000000000 --- a/webroot/js/components.js +++ /dev/null @@ -1,64 +0,0 @@ -// DELETE THIS FILE LATER. - -Vue.component('owncast-footer', { - props: { - appVersion: { - type: String, - default: '0.1', - }, - }, - - template: ` - - `, -}); - - -Vue.component('stream-tags', { - props: ['tags'], - template: ` -
      -
    • - {{tag}} -
    • -
    - `, -}); - -Vue.component('user-details', { - props: ['logo', 'platforms', 'summary', 'tags'], - template: ` -
    -
    - -
    -
    -

    - About - - -

    - -
    - -
    -
    - `, -}); diff --git a/webroot/js/chat/chat-input.js b/webroot/js/components/chat/chat-input.js similarity index 97% rename from webroot/js/chat/chat-input.js rename to webroot/js/components/chat/chat-input.js index 0a8d3717f..fd472626b 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -3,15 +3,10 @@ import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); import { EmojiButton } from 'https://cdn.skypack.dev/@joeattardi/emoji-button'; - -import { URL_CUSTOM_EMOJIS, getLocalStorage, setLocalStorage } from '../utils.js'; -import { - KEY_CHAT_FIRST_MESSAGE_SENT, - generatePlaceholderText, - getCaretPosition, -} from '../utils/chat.js'; - import ContentEditable from './content-editable.js'; +import { generatePlaceholderText, getCaretPosition } from '../../utils/chat.js'; +import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js'; +import { URL_CUSTOM_EMOJIS, KEY_CHAT_FIRST_MESSAGE_SENT } from '../../utils/constants.js'; export default class ChatInput extends Component { diff --git a/webroot/js/chat/chat.js b/webroot/js/components/chat/chat.js similarity index 94% rename from webroot/js/chat/chat.js rename to webroot/js/components/chat/chat.js index b248c48d1..119f3ea26 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -3,15 +3,12 @@ import htm from 'https://unpkg.com/htm?module'; // Initialize htm with Preact const html = htm.bind(h); - -import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; import Message from './message.js'; import ChatInput from './chat-input.js'; -import { CALLBACKS } from '../websocket.js'; - - -import { URL_CHAT_HISTORY, setVHvar, hasTouchScreen } from '../utils.js'; -import { extraUserNamesFromMessageHistory } from '../utils/chat.js'; +import { CALLBACKS, SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; +import { setVHvar, hasTouchScreen } from '../../utils/helpers.js'; +import { extraUserNamesFromMessageHistory } from '../../utils/chat.js'; +import { URL_CHAT_HISTORY } from '../../utils/constants.js'; export default class Chat extends Component { constructor(props, context) { @@ -199,3 +196,4 @@ export default class Chat extends Component { } } + diff --git a/webroot/js/chat/content-editable.js b/webroot/js/components/chat/content-editable.js similarity index 99% rename from webroot/js/chat/content-editable.js rename to webroot/js/components/chat/content-editable.js index 40514b8b0..08c41cf19 100644 --- a/webroot/js/chat/content-editable.js +++ b/webroot/js/components/chat/content-editable.js @@ -6,7 +6,6 @@ and here: https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103 */ - import { Component, createRef, createElement } from 'https://unpkg.com/preact?module'; function replaceCaret(el) { diff --git a/webroot/js/chat/message.js b/webroot/js/components/chat/message.js similarity index 88% rename from webroot/js/chat/message.js rename to webroot/js/components/chat/message.js index 78cecfe81..b4e10f53f 100644 --- a/webroot/js/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -1,9 +1,9 @@ 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'; -import { generateAvatar } from '../utils.js'; -import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; +import { messageBubbleColorForString } from '../../utils/user-colors.js'; +import { formatMessageText } from '../../utils/chat.js'; +import { generateAvatar } from '../../utils/helpers.js'; +import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; export default class Message extends Component { render(props) { diff --git a/webroot/js/chat/username.js b/webroot/js/components/chat/username.js similarity index 96% rename from webroot/js/chat/username.js rename to webroot/js/components/chat/username.js index bb67ba260..013b2abe6 100644 --- a/webroot/js/chat/username.js +++ b/webroot/js/components/chat/username.js @@ -3,8 +3,8 @@ import htm from 'https://unpkg.com/htm?module'; // Initialize htm with Preact const html = htm.bind(h); -import { generateAvatar, setLocalStorage } from '../utils.js'; -import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; +import { generateAvatar, setLocalStorage } from '../../utils/helpers.js'; +import { KEY_USERNAME, KEY_AVATAR } from '../../utils/constants.js'; export default class UsernameForm extends Component { constructor(props, context) { diff --git a/webroot/js/player.js b/webroot/js/components/player.js similarity index 100% rename from webroot/js/player.js rename to webroot/js/components/player.js diff --git a/webroot/js/social.js b/webroot/js/components/social.js similarity index 91% rename from webroot/js/social.js rename to webroot/js/components/social.js index dc46920a2..0bdf94102 100644 --- a/webroot/js/social.js +++ b/webroot/js/components/social.js @@ -1,6 +1,6 @@ import { html } from "https://unpkg.com/htm/preact/index.mjs?module"; -import { SOCIAL_PLATFORMS } from './utils/social.js'; -import { classNames } from './utils.js'; +import { SOCIAL_PLATFORMS } from '../utils/social.js'; +import { classNames } from '../utils/helpers.js'; export default function SocialIcon(props) { const { platform, url } = props; diff --git a/webroot/js/message.js b/webroot/js/message.js deleted file mode 100644 index 65733bd27..000000000 --- a/webroot/js/message.js +++ /dev/null @@ -1,440 +0,0 @@ -// DELETE THIS FILE LATER. - -import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; - -const KEY_USERNAME = 'owncast_username'; -const KEY_AVATAR = 'owncast_avatar'; -const KEY_CHAT_DISPLAYED = 'owncast_chat'; -const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; -const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; -const CHAT_PLACEHOLDER_TEXT = 'Message'; -const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; - -class Message { - constructor(model) { - this.author = model.author; - this.body = model.body; - this.image = model.image || generateAvatar(model.author); - this.id = model.id; - this.type = model.type; - } - - formatText() { - showdown.setFlavor('github'); - let formattedText = new showdown.Converter({ - emoji: true, - openLinksInNewWindow: true, - tables: false, - simplifiedAutoLink: false, - literalMidWordUnderscores: true, - strikethrough: true, - ghMentions: false, - }).makeHtml(this.body); - - formattedText = this.linkify(formattedText, this.body); - formattedText = this.highlightUsername(formattedText); - - return addNewlines(formattedText); - } - - // TODO: Move this into a util function once we can organize code - // and split things up. - linkify(text, rawText) { - const urls = getURLs(stripTags(rawText)); - if (urls) { - urls.forEach(function (url) { - let linkURL = url; - - // Add http prefix if none exist in the URL so it actually - // will work in an anchor tag. - if (linkURL.indexOf('http') === -1) { - linkURL = 'http://' + linkURL; - } - - // Remove the protocol prefix in the display URLs just to make - // things look a little nicer. - const displayURL = url.replace(/(^\w+:|^)\/\//, ''); - const link = `${displayURL}`; - text = text.replace(url, link); - - if (getYoutubeIdFromURL(url)) { - if (this.isTextJustURLs(text, [url, displayURL])) { - text = ''; - } else { - text += '
    '; - } - - const youtubeID = getYoutubeIdFromURL(url); - text += getYoutubeEmbedFromID(youtubeID); - } else if (url.indexOf('instagram.com/p/') > -1) { - if (this.isTextJustURLs(text, [url, displayURL])) { - text = ''; - } else { - text += `
    `; - } - text += getInstagramEmbedFromURL(url); - } else if (isImage(url)) { - if (this.isTextJustURLs(text, [url, displayURL])) { - text = ''; - } else { - text += `
    `; - } - text += getImageForURL(url); - } - }.bind(this)); - } - return text; - } - - isTextJustURLs(text, urls) { - for (var i = 0; i < urls.length; i++) { - const url = urls[i]; - if (stripTags(text) === url) { - return true; - } - } - - return false; - } - - userColor() { - return messageBubbleColorForString(this.author); - } - - highlightUsername(message) { - const username = document.getElementById('self-message-author').value; - const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); - return message.replace(pattern, '$&'); - } -} - - - -class MessagingInterface { - constructor() { - this.chatDisplayed = false; - this.username = ''; - this.messageCharCount = 0; - this.maxMessageLength = 500; - this.maxMessageBuffer = 20; - this.chatUsernames = []; - - this.onReceivedMessages = this.onReceivedMessages.bind(this); - this.disableChat = this.disableChat.bind(this); - this.enableChat = this.enableChat.bind(this); - } - - init() { - this.tagAppContainer = document.getElementById('app-container'); - this.tagChatToggle = document.getElementById('chat-toggle'); - this.tagUserInfoChanger = document.getElementById('user-info-change'); - this.tagUsernameDisplay = document.getElementById('username-display'); - this.tagMessageFormWarning = document.getElementById('message-form-warning'); - - 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.btnSubmitMessage = document.getElementById('button-submit-message'); - - this.formMessageInput = document.getElementById('message-body-form'); - - this.imgUsernameAvatar = document.getElementById('username-avatar'); - this.textUserInfoDisplay = document.getElementById('user-info-display'); - - this.scrollableMessagesContainer = document.getElementById('messages-container'); - - // 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)); - this.formMessageInput.addEventListener('blur', this.handleMessageInputBlur.bind(this)); - this.btnSubmitMessage.addEventListener('click', this.handleSubmitChatButton.bind(this)); - - this.initLocalStates(); - - if (hasTouchScreen()) { - setVHvar(); - window.addEventListener("orientationchange", setVHvar); - this.tagAppContainer.classList.add('touch-screen'); - } - } - - initLocalStates() { - this.username = getLocalStorage(KEY_USERNAME) || generateUsername(); - this.imgUsernameAvatar.src = - getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`); - this.updateUsernameFields(this.username); - - this.chatDisplayed = getLocalStorage(KEY_CHAT_DISPLAYED) || true; - this.displayChat(); - this.disableChat(); // Disabled by default. - } - - updateUsernameFields(username) { - this.tagUsernameDisplay.innerText = username; - this.inputChangeUserName.value = username; - this.inputMessageAuthor.value = username; - } - - displayChat() { - if (this.chatDisplayed) { - this.tagAppContainer.classList.add('chat'); - this.tagAppContainer.classList.remove('no-chat'); - jumpToBottom(this.scrollableMessagesContainer); - } else { - this.tagAppContainer.classList.add('no-chat'); - this.tagAppContainer.classList.remove('chat'); - } - this.setChatPlaceholderText(); - } - - - handleChatToggle() { - this.chatDisplayed = !this.chatDisplayed; - if (this.chatDisplayed) { - setLocalStorage(KEY_CHAT_DISPLAYED, this.chatDisplayed); - } else { - clearLocalStorage(KEY_CHAT_DISPLAYED); - } - this.displayChat(); - } - - handleShowChangeNameForm() { - this.textUserInfoDisplay.style.display = 'none'; - this.tagUserInfoChanger.style.display = 'flex'; - if (document.body.clientWidth < 640) { - this.tagChatToggle.style.display = 'none'; - } - } - - handleHideChangeNameForm() { - this.textUserInfoDisplay.style.display = 'flex'; - this.tagUserInfoChanger.style.display = 'none'; - if (document.body.clientWidth < 640) { - this.tagChatToggle.style.display = 'inline-block'; - } - } - - handleUpdateUsername() { - const oldName = this.username; - var newValue = this.inputChangeUserName.value; - newValue = newValue.trim(); - // do other string cleanup? - - if (newValue) { - this.username = newValue; - this.updateUsernameFields(newValue); - this.imgUsernameAvatar.src = generateAvatar(`${newValue}${Date.now()}`); - setLocalStorage(KEY_USERNAME, newValue); - setLocalStorage(KEY_AVATAR, this.imgUsernameAvatar.src); - } - this.handleHideChangeNameForm(); - - if (oldName !== newValue) { - this.sendUsernameChange(oldName, newValue, this.imgUsernameAvatar.src); - } - } - - handleUsernameKeydown(event) { - if (event.keyCode === 13) { // enter - this.handleUpdateUsername(); - } else if (event.keyCode === 27) { // esc - this.handleHideChangeNameForm(); - } - } - - sendUsernameChange(oldName, newName, image) { - const nameChange = { - type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, - oldName: oldName, - newName: newName, - image: image, - }; - - this.send(nameChange); - } - - tryToComplete() { - const rawValue = this.formMessageInput.innerHTML; - const position = getCaretPosition(this.formMessageInput); - const at = rawValue.lastIndexOf('@', position - 1); - - if (at === -1) { - return false; - } - - var partial = rawValue.substring(at + 1, position).trim(); - - if (partial === this.suggestion) { - partial = this.partial; - } else { - this.partial = partial; - } - - const possibilities = this.chatUsernames.filter(function (username) { - return username.toLowerCase().startsWith(partial.toLowerCase()); - }); - - if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { - this.completionIndex = 0; - } - - if (possibilities.length > 0) { - this.suggestion = possibilities[this.completionIndex]; - - // TODO: Fix the space not working. I'm guessing because the DOM ignores spaces and it requires a nbsp or something? - this.formMessageInput.innerHTML = rawValue.substring(0, at + 1) + this.suggestion + ' ' + rawValue.substring(position); - setCaretPosition(this.formMessageInput, at + this.suggestion.length + 2); - } - - return true; - } - - handleMessageInputKeydown(event) { - var okCodes = [37,38,39,40,16,91,18,46,8]; - var value = this.formMessageInput.innerHTML.trim(); - var numCharsLeft = this.maxMessageLength - value.length; - if (event.keyCode === 13) { // enter - if (!this.prepNewLine) { - this.submitChat(value); - event.preventDefault(); - this.prepNewLine = false; - - return; - } - } - if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift - this.prepNewLine = true; - } - if (event.keyCode === 9) { // tab - if (this.tryToComplete()) { - event.preventDefault(); - - // value could have been changed, update variables - value = this.formMessageInput.innerHTML.trim(); - numCharsLeft = this.maxMessageLength - value.length; - } - } - - if (numCharsLeft <= this.maxMessageBuffer) { - this.tagMessageFormWarning.innerText = `${numCharsLeft} chars left`; - if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) { - event.preventDefault(); - return; - } - } else { - this.tagMessageFormWarning.innerText = ''; - } - } - - handleMessageInputKeyup(event) { - if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift - this.prepNewLine = false; - } - } - - handleMessageInputBlur(event) { - this.prepNewLine = false; - } - - handleSubmitChatButton(event) { - var value = this.formMessageInput.innerHTML.trim(); - if (value) { - this.submitChat(value); - event.preventDefault(); - return false; - } - event.preventDefault(); - return false; - } - - submitChat(content) { - if (!content) { - return; - } - var message = new Message({ - body: content, - author: this.username, - image: this.imgUsernameAvatar.src, - type: SOCKET_MESSAGE_TYPES.CHAT, - }); - this.send(message); - - // clear out things. - this.formMessageInput.innerHTML = ''; - this.tagMessageFormWarning.innerText = ''; - - const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); - if (!hasSentFirstChatMessage) { - setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); - this.setChatPlaceholderText(); - } - } - - disableChat() { - if (this.formMessageInput) { - this.formMessageInput.contentEditable = false; - this.formMessageInput.innerHTML = ''; - this.formMessageInput.setAttribute("placeholder", CHAT_PLACEHOLDER_OFFLINE); - } - } - - enableChat() { - if (this.formMessageInput) { - this.formMessageInput.contentEditable = true; - this.setChatPlaceholderText(); - } - } - - setChatPlaceholderText() { - // NOTE: This is a fake placeholder that is being styled via CSS. - // You can't just set the .placeholder property because it's not a form element. - const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); - const placeholderText = hasSentFirstChatMessage ? CHAT_PLACEHOLDER_TEXT : CHAT_INITIAL_PLACEHOLDER_TEXT; - this.formMessageInput.setAttribute("placeholder", placeholderText); - } - - // handle Vue.js message display - onReceivedMessages(newMessages, oldMessages) { - // update the list of chat usernames - newMessages.slice(oldMessages.length).forEach(function (message) { - var username; - - switch (message.type) { - case SOCKET_MESSAGE_TYPES.CHAT: - username = message.author; - break; - - case SOCKET_MESSAGE_TYPES.NAME_CHANGE: - username = message.newName; - break; - - default: - return; - } - - if (!this.chatUsernames.includes(username)) { - this.chatUsernames.push(username); - } - }, this); - - if (newMessages.length !== oldMessages.length) { - // jump to bottom - jumpToBottom(this.scrollableMessagesContainer); - } - } - - send(messageJSON) { - console.error('MessagingInterface send() is not linked to the websocket component.'); - } -} - -export { Message, MessagingInterface } diff --git a/webroot/js/chat/standalone.js b/webroot/js/standalone-chat-app.js similarity index 88% rename from webroot/js/chat/standalone.js rename to webroot/js/standalone-chat-app.js index 115982d63..221083466 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/standalone-chat-app.js @@ -2,13 +2,12 @@ import { h, Component, Fragment } from 'https://unpkg.com/preact?module'; import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); +import UsernameForm from './components/chat/username.js'; +import Chat from './components/chat.js'; +import Websocket from './utils/websocket.js'; -import UsernameForm from './username.js'; -import Chat from './chat.js'; -import Websocket from '../websocket.js'; - -import { getLocalStorage, generateAvatar, generateUsername } from '../utils.js'; -import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; +import { getLocalStorage, generateAvatar, generateUsername } from './utils/helpers.js'; +import { KEY_USERNAME, KEY_AVATAR } from '../utils/constants.js'; export default class StandaloneChat extends Component { constructor(props, context) { diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 4ce504f43..b7db2659c 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -1,12 +1,13 @@ -import { addNewlines } from '../utils.js'; - -export const KEY_USERNAME = 'owncast_username'; -export const KEY_AVATAR = 'owncast_avatar'; -export const KEY_CHAT_DISPLAYED = 'owncast_chat'; -export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; -export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; -export const CHAT_PLACEHOLDER_TEXT = 'Message'; -export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; +import { addNewlines } from './helpers.js'; +import { + KEY_USERNAME, + KEY_AVATAR, + KEY_CHAT_DISPLAYED, + KEY_CHAT_FIRST_MESSAGE_SENT, + CHAT_INITIAL_PLACEHOLDER_TEXT, + CHAT_PLACEHOLDER_TEXT, + CHAT_PLACEHOLDER_OFFLINE, +} from './constants.js'; export function formatMessageText(message, username) { showdown.setFlavor('github'); diff --git a/webroot/js/utils/constants.js b/webroot/js/utils/constants.js new file mode 100644 index 000000000..a21d32997 --- /dev/null +++ b/webroot/js/utils/constants.js @@ -0,0 +1,29 @@ +// misc constants used throughout the app + +export const URL_STATUS = `/status`; +export const URL_CHAT_HISTORY = `/chat`; +export const URL_CUSTOM_EMOJIS = `/emoji`; +export const URL_CONFIG = `/config`; + +// TODO: This directory is customizable in the config. So we should expose this via the config API. +export const URL_STREAM = `/hls/stream.m3u8`; +export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; + +export const TIMER_STATUS_UPDATE = 5000; // ms +export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins +export const TIMER_STREAM_DURATION_COUNTER = 1000; +export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +export const MESSAGE_OFFLINE = 'Stream is offline.'; +export const MESSAGE_ONLINE = 'Stream is online'; + +export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer + + +export const KEY_USERNAME = 'owncast_username'; +export const KEY_AVATAR = 'owncast_avatar'; +export const KEY_CHAT_DISPLAYED = 'owncast_chat'; +export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; +export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; +export const CHAT_PLACEHOLDER_TEXT = 'Message'; +export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; diff --git a/webroot/js/utils.js b/webroot/js/utils/helpers.js similarity index 79% rename from webroot/js/utils.js rename to webroot/js/utils/helpers.js index 2be01ff7f..6962dfd61 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils/helpers.js @@ -1,22 +1,3 @@ -export const URL_STATUS = `/status`; -export const URL_CHAT_HISTORY = `/chat`; -export const URL_CUSTOM_EMOJIS = `/emoji`; -export const URL_CONFIG = `/config`; - -// TODO: This directory is customizable in the config. So we should expose this via the config API. -export const URL_STREAM = `/hls/stream.m3u8`; -export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; - -export const TIMER_STATUS_UPDATE = 5000; // ms -export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins -export const TIMER_STREAM_DURATION_COUNTER = 1000; -export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - -export const MESSAGE_OFFLINE = 'Stream is offline.'; -export const MESSAGE_ONLINE = 'Stream is online'; - -export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer - export function getLocalStorage(key) { try { return localStorage.getItem(key); diff --git a/webroot/js/utils/social.js b/webroot/js/utils/social.js index dad57a546..9f42063e5 100644 --- a/webroot/js/utils/social.js +++ b/webroot/js/utils/social.js @@ -1,4 +1,3 @@ - // x, y pixel psitions of /img/social.gif image. export const SOCIAL_PLATFORMS = { default: { diff --git a/webroot/js/utils/socket-message-types.js b/webroot/js/utils/socket-message-types.js deleted file mode 100644 index f52b57a3e..000000000 --- a/webroot/js/utils/socket-message-types.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * These are the types of messages that we can handle with the websocket. - * Mostly used by `websocket.js` but if other components need to handle - * different types then it can import this file. - */ -export default { - CHAT: 'CHAT', - PING: 'PING', - NAME_CHANGE: 'NAME_CHANGE', - PONG: 'PONG' -}; diff --git a/webroot/js/websocket.js b/webroot/js/utils/websocket.js similarity index 91% rename from webroot/js/websocket.js rename to webroot/js/utils/websocket.js index 3b558f99f..bc392faa6 100644 --- a/webroot/js/websocket.js +++ b/webroot/js/utils/websocket.js @@ -1,8 +1,14 @@ -import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; - -const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; - -const TIMER_WEBSOCKET_RECONNECT = 5000; // ms +/** + * These are the types of messages that we can handle with the websocket. + * Mostly used by `websocket.js` but if other components need to handle + * different types then it can import this file. + */ +export const SOCKET_MESSAGE_TYPES = { + CHAT: 'CHAT', + PING: 'PING', + NAME_CHANGE: 'NAME_CHANGE', + PONG: 'PONG' +}; export const CALLBACKS = { RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived', @@ -10,8 +16,10 @@ export const CALLBACKS = { WEBSOCKET_DISCONNECTED: 'websocketDisconnected', } -class Websocket { +const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; +const TIMER_WEBSOCKET_RECONNECT = 5000; // ms +export default class Websocket { constructor() { this.websocket = null; this.websocketReconnectTimer = null; @@ -133,7 +141,5 @@ class Websocket { handleNetworkingError(error) { console.error(`Websocket Error: ${error}`) - }; + } } - -export default Websocket; diff --git a/webroot/standalone-chat.html b/webroot/standalone-chat.html index fcb2001ea..ec3899684 100644 --- a/webroot/standalone-chat.html +++ b/webroot/standalone-chat.html @@ -19,7 +19,7 @@ @@ -21,10 +19,8 @@ import { render, html } from "https://unpkg.com/htm/preact/index.mjs?module"; import StandaloneChat from './js/standalone-chat-app.js'; - const messagesOnly = false; - (function () { - render(html`<${StandaloneChat} messagesOnly=${messagesOnly} />`, document.getElementById("chat-container")); + render(html`<${StandaloneChat} messagesOnly />`, document.getElementById("chat-container")); })(); diff --git a/webroot/styles/app.css b/webroot/styles/app.css index ea9fa9873..e42f6e507 100644 --- a/webroot/styles/app.css +++ b/webroot/styles/app.css @@ -25,6 +25,11 @@ a:hover { background: transparent; } +button[disabled] { + opacity: .5; + pointer-events: none; +} + .visually-hidden { position: absolute !important; height: 1px; @@ -72,7 +77,7 @@ header { opacity: 0; pointer-events: none; } -.online #video { +.online #video-container #video { opacity: 1; pointer-events: auto; } diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index 2db8094ea..66334a20c 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -61,7 +61,7 @@ .emoji-picker__emoji { - border-radius: 10px; + border-radius: 5px; } From a07ad8d693ec57787925e1318d28940c482fb997 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Sun, 23 Aug 2020 19:38:58 -0700 Subject: [PATCH 27/35] rename standalone assets so they're easier to find --- ...e-chat.html => index-standalone-chat.html} | 2 +- webroot/index.html | 274 +++++------------- webroot/index2.html | 85 ------ ...one-chat-app.js => app-standalone-chat.js} | 0 4 files changed, 70 insertions(+), 291 deletions(-) rename webroot/{standalone-chat.html => index-standalone-chat.html} (92%) delete mode 100644 webroot/index2.html rename webroot/js/{standalone-chat-app.js => app-standalone-chat.js} (100%) diff --git a/webroot/standalone-chat.html b/webroot/index-standalone-chat.html similarity index 92% rename from webroot/standalone-chat.html rename to webroot/index-standalone-chat.html index 96fd8aa76..fe334a600 100644 --- a/webroot/standalone-chat.html +++ b/webroot/index-standalone-chat.html @@ -17,7 +17,7 @@ + - - - - + + + + + + + + + + + - - +
    -
    -
    -
    -

    - - - - {{title}} -

    - -
    -
    -
    - - -
    - -
    - - - -
    -
    - -
    - -
    - -
    -
    - -
    - - -
    - {{ streamStatus }} - {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. - Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}. - {{ overallMaxViewerCount }} overall. -
    -
    - -
    - {{streamerName}} - -
    {{extraUserContent}}
    - -
    - - - -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -

    {{ message.author }}

    -

    -
    -
    - - -
    - -
    - {{ message.oldName }} is now known as {{ message.newName }}. -
    -
    - -
    -
    - - -
    -
    - - -
    -
    😏
    - -
    - - -
    -
    -
    -
    -
    - -
    - - - - - - - + diff --git a/webroot/index2.html b/webroot/index2.html deleted file mode 100644 index a751cacfb..000000000 --- a/webroot/index2.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - diff --git a/webroot/js/standalone-chat-app.js b/webroot/js/app-standalone-chat.js similarity index 100% rename from webroot/js/standalone-chat-app.js rename to webroot/js/app-standalone-chat.js From 22e16b67d75b0754426bb2fbdf74e435b3aa8a3d Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Sun, 23 Aug 2020 21:23:16 -0700 Subject: [PATCH 28/35] start a video-only page; fix some styles --- webroot/index-standalone-chat.html | 8 +- webroot/index-video-only.html | 31 ++ webroot/index.html | 3 +- webroot/js/app-video-only.js | 265 ++++++++++++ webroot/js/app.js | 541 +++++++++++++++---------- webroot/js/app2.js | 455 --------------------- webroot/js/components/chat/username.js | 7 +- webroot/styles/app.css | 6 +- webroot/styles/standalone-chat.css | 22 +- webroot/styles/video-only.css | 30 ++ 10 files changed, 681 insertions(+), 687 deletions(-) create mode 100644 webroot/index-video-only.html create mode 100644 webroot/js/app-video-only.js delete mode 100644 webroot/js/app2.js create mode 100644 webroot/styles/video-only.css diff --git a/webroot/index-standalone-chat.html b/webroot/index-standalone-chat.html index fe334a600..6d47137d6 100644 --- a/webroot/index-standalone-chat.html +++ b/webroot/index-standalone-chat.html @@ -11,16 +11,16 @@ - + -
    +
    diff --git a/webroot/index-video-only.html b/webroot/index-video-only.html new file mode 100644 index 000000000..259677894 --- /dev/null +++ b/webroot/index-video-only.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + +
    + + + + diff --git a/webroot/index.html b/webroot/index.html index a751cacfb..380ef490c 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -33,6 +33,7 @@ + @@ -48,7 +49,7 @@ - + diff --git a/webroot/js/app.js b/webroot/js/app.js index d0151b8f8..56ff53b86 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -9,27 +9,31 @@ import Chat from './components/chat/chat.js'; import Websocket from './utils/websocket.js'; import { - getLocalStorage, - setLocalStorage, + addNewlines, + classNames, clearLocalStorage, + debounce, generateAvatar, generateUsername, - addNewlines, + getLocalStorage, pluralize, + setLocalStorage, } from './utils/helpers.js'; import { - URL_OWNCAST, - URL_CONFIG, - URL_STATUS, - TIMER_STATUS_UPDATE, - TIMER_DISABLE_CHAT_AFTER_OFFLINE, - TIMER_STREAM_DURATION_COUNTER, - TEMP_IMAGE, - MESSAGE_OFFLINE, - MESSAGE_ONLINE, - KEY_USERNAME, + HEIGHT_SHORT_WIDE, KEY_AVATAR, KEY_CHAT_DISPLAYED, + KEY_USERNAME, + MESSAGE_OFFLINE, + MESSAGE_ONLINE, + TEMP_IMAGE, + TIMER_DISABLE_CHAT_AFTER_OFFLINE, + TIMER_STATUS_UPDATE, + TIMER_STREAM_DURATION_COUNTER, + URL_CONFIG, + URL_OWNCAST, + URL_STATUS, + WIDTH_SINGLE_COL, } from './utils/constants.js'; export default class App extends Component { @@ -41,7 +45,8 @@ export default class App extends Component { displayChat: getLocalStorage(KEY_CHAT_DISPLAYED), // chat panel state chatEnabled: false, // chat input box state username: getLocalStorage(KEY_USERNAME) || generateUsername(), - userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), + userAvatarImage: + getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), configData: {}, extraUserContent: '', @@ -49,11 +54,15 @@ export default class App extends Component { playerActive: false, // player object is active streamOnline: false, // stream is active/online - //status + // status streamStatusMessage: MESSAGE_OFFLINE, viewerCount: '', sessionMaxViewerCount: '', overallMaxViewerCount: '', + + // dom + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, }; // timers @@ -66,6 +75,7 @@ export default class App extends Component { // misc dom events this.handleChatPanelToggle = this.handleChatPanelToggle.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this); + this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 400); this.handleOfflineMode = this.handleOfflineMode.bind(this); this.handleOnlineMode = this.handleOnlineMode.bind(this); @@ -81,12 +91,11 @@ export default class App extends Component { this.getConfig = this.getConfig.bind(this); this.getStreamStatus = this.getStreamStatus.bind(this); this.getExtraUserContent = this.getExtraUserContent.bind(this); - - } componentDidMount() { this.getConfig(); + window.addEventListener('resize', this.handleWindowResize); this.player = new OwncastPlayer(); this.player.setupPlayerCallbacks({ @@ -161,7 +170,6 @@ export default class App extends Component { }); } - setConfigData(data = {}) { const { title, extraUserInfoFileName, summary } = data; @@ -301,21 +309,30 @@ export default class App extends Component { console.log(`>>> App Error: ${error}`); } + handleWindowResize() { + this.setState({ + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + }); + } + render(props, state) { const { - username, - userAvatarImage, - websocket, + chatEnabled, configData, - extraUserContent, displayChat, - viewerCount, - sessionMaxViewerCount, + extraUserContent, overallMaxViewerCount, playerActive, + sessionMaxViewerCount, streamOnline, streamStatusMessage, - chatEnabled, + userAvatarImage, + username, + viewerCount, + websocket, + windowHeight, + windowWidth, } = state; const { @@ -347,14 +364,23 @@ export default class App extends Component { `); - - const chatClass = displayChat ? 'chat' : 'no-chat'; const mainClass = playerActive ? 'online' : ''; - const streamInfoClass = streamOnline ? 'online' : ''; + const streamInfoClass = streamOnline ? 'online' : ''; // need? + + const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE; + const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight; + const extraAppClasses = classNames({ + 'chat': displayChat, + 'no-chat': !displayChat, + 'single-col': singleColMode, + 'bg-gray-800': singleColMode && displayChat, + 'short-wide': shortHeight, + }) + return ( html` -
    -
    +
    +

    -
    ${streamStatusMessage} ${viewerCount} ${pluralize('viewer', viewerCount)}. @@ -440,16 +465,15 @@ export default class App extends Component { Version ${appVersion} - - <${Chat} - websocket=${websocket} - username=${username} - userAvatarImage=${userAvatarImage} - chatEnabled=${chatEnabled} - /> - -
    - `); + <${Chat} + websocket=${websocket} + username=${username} + userAvatarImage=${userAvatarImage} + chatEnabled //=${chatEnabled} + /> +
    + ` + ); } } diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js index 0ba1e1cc1..b2482105a 100644 --- a/webroot/js/components/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -232,7 +232,7 @@ export default class ChatInput extends Component { const placeholderText = generatePlaceholderText(inputEnabled, hasSentFirstChatMessage); return ( html` -
    +
    <${ContentEditable} id="message-input" diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index 4c4b486a3..2953de1b3 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -21,14 +21,11 @@ export default class Chat extends Component { this.scrollableMessagesContainer = createRef(); - this.websocket = null; this.getChatHistory = this.getChatHistory.bind(this); this.receivedWebsocketMessage = this.receivedWebsocketMessage.bind(this); this.websocketDisconnected = this.websocketDisconnected.bind(this); - - // this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this); this.submitChat = this.submitChat.bind(this); } @@ -39,7 +36,6 @@ export default class Chat extends Component { if (hasTouchScreen()) { setVHvar(); window.addEventListener("orientationchange", setVHvar); - // this.tagAppContainer.classList.add('touch-screen'); } } diff --git a/webroot/js/components/chat/username.js b/webroot/js/components/chat/username.js index f18d37c4a..a2ba51711 100644 --- a/webroot/js/components/chat/username.js +++ b/webroot/js/components/chat/username.js @@ -22,8 +22,9 @@ export default class UsernameForm extends Component { } handleDisplayForm() { + const { displayForm: curDisplay } = this.state; this.setState({ - displayForm: true, + displayForm: !curDisplay, }); } @@ -65,7 +66,7 @@ export default class UsernameForm extends Component { const formDisplayStyle = narrowSpace ? 'inline-block' : 'flex'; const styles = { info: { - display: displayForm || narrowSpace ? 'none' : 'flex', + display: displayForm ? 'none' : 'flex', }, form: { display: displayForm ? formDisplayStyle : 'none', diff --git a/webroot/js/utils/constants.js b/webroot/js/utils/constants.js index a01cdb6c1..c83de5acd 100644 --- a/webroot/js/utils/constants.js +++ b/webroot/js/utils/constants.js @@ -27,3 +27,8 @@ export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; export const CHAT_PLACEHOLDER_TEXT = 'Message'; export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; + + +// app styling +export const WIDTH_SINGLE_COL = 730; +export const HEIGHT_SHORT_WIDE = 500; diff --git a/webroot/js/utils/helpers.js b/webroot/js/utils/helpers.js index 6962dfd61..1f913e8f9 100644 --- a/webroot/js/utils/helpers.js +++ b/webroot/js/utils/helpers.js @@ -128,3 +128,32 @@ export function classNames(json) { }); return classes.join(' '); } + + +// taken from +// https://medium.com/@TCAS3/debounce-deep-dive-javascript-es6-e6f8d983b7a1 +export function debounce(fn, time) { + let timeout; + + return function() { + const functionCall = () => fn.apply(this, arguments); + + clearTimeout(timeout); + timeout = setTimeout(functionCall, time); + } +} + +/* +const debouncedHandleResize = debounce(function handleResize() { + setDimensions({ + height: window.innerHeight, + width: window.innerWidth + }) +}, 1000) +window.addEventListener('resize', debouncedHandleResize) +window.addEventListener('keyup', debounce((e) => { + console.log(e); +}, 1000)); + +*/ + diff --git a/webroot/styles/app.css b/webroot/styles/app.css index c0b6d60c5..c361a19a6 100644 --- a/webroot/styles/app.css +++ b/webroot/styles/app.css @@ -1,6 +1,6 @@ /* -Spefici styles for app layout - +Specific styles for main app layout. +May have overrides for other components with own stylesheets. */ /* variables */ @@ -25,6 +25,10 @@ a:hover { background: transparent; } +* { + transition: all .25s; +} + button[disabled] { opacity: .5; pointer-events: none; @@ -68,11 +72,11 @@ header { /* ************************************************ */ #video-container { - height: calc(var(--video-container-height)); + height: var(--video-container-height); margin-top: var(--header-height); position: relative; width: 100%; - /* height: calc((9 / 16) * 100vw); */ + min-height: 480px; background-size: 30%; } @@ -97,13 +101,79 @@ header { opacity: .75; } +.no-chat #chat-container-wrap { + display: none; +} /* *********** overrides when chat is on ***************************** */ +.chat { + --content-width: calc(100vw - var(--right-col-width)); +} +.chat #chat-container-wrap { + display: block; +} + .chat #video-container, .chat #stream-info, .chat #user-content { - width: calc(100% - var(--right-col-width)); + width: var(--content-width); +} + +.chat #video-container { + height: calc((9 / 16) * var(--content-width)); +} + + + +.short-wide.chat #video-container { + height: calc(100vh - var(--header-height) - 3rem); + min-height: auto; +} + +.short-wide #message-input { + height: 3rem; +} + + + + +/* *********** single col layout ***************************** */ + +.single-col { + --right-col-width: 0px; +} +.single-col main { + position: fixed; + width: 100%; + z-index: 40; +} +.single-col #chat-container { + position: relative; + width: 100%; + height: auto; +} +.single-col #video-container { + min-height: auto; + width: 100%; +} +.single-col #user-content, +.single-col #chat-container-wrap { + margin-top: calc(var(--video-container-height) + var(--header-height) + 1rem); +} +.single-col #user-content .user-content { + flex-wrap: wrap; + justify-content: center; +} +.single-col.chat #user-content { + display: none; +} +.single-col #message-input-container { + width: 100%; +} + +.single-col #message-input { + height: 3rem; } @@ -116,15 +186,13 @@ header { --right-col-width: 20em; --user-image-width: 6em; } - - #chat-container { - width: var(--right-col-width); - } - } +/* ************************************************8 */ + + /* single col layout */ -@media screen and (max-width: 640px ) { +/* @media screen and (max-width: 640px ) { :root { --right-col-width: 0; --video-container-height: 40vh; @@ -142,7 +210,6 @@ header { #chat-container { width: 100%; position: static; - /* min-height: calc(100vh - var(--header-height)); */ height: calc(100vh - var(--header-height) - var(--video-container-height) - 3vh) } #messages-container { @@ -166,16 +233,16 @@ header { .chat footer { display: none; } -} +} */ -@media screen and (max-height: 860px ) { +/* @media screen and (max-height: 860px ) { :root { --video-container-height: 40vh; } .user-content { flex-direction: column; } -} +} */ diff --git a/webroot/styles/chat.css b/webroot/styles/chat.css index 66334a20c..7dcf9af46 100644 --- a/webroot/styles/chat.css +++ b/webroot/styles/chat.css @@ -1,3 +1,5 @@ +/* some base styles for chat and messaging components */ + #chat-container { position: fixed; z-index: 9; @@ -8,20 +10,14 @@ height: calc(100vh - var(--header-height)); } -.touch-screen #chat-container { - height: calc(100vh - var(--header-height) - 3vh); +#message-input-container { + width: var(--right-col-width); } - -.no-chat #chat-container-wrap { - display: none; +#messages-container { + padding-bottom: 10rem; } -.chat #chat-container-wrap { - display: block; -} - - /******************************/ /******************************/ @@ -76,10 +72,6 @@ padding: 5px; } -.message-content { -} - - /* MESSAGE TEXT HTML */ From 64570154063171ef09965979b8fe995425ea1935 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Mon, 24 Aug 2020 12:34:36 -0700 Subject: [PATCH 33/35] cleanup --- webroot/js/app.js | 1 + webroot/js/components/chat/chat.js | 6 ++++++ webroot/js/utils/helpers.js | 15 --------------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/webroot/js/app.js b/webroot/js/app.js index 56ff53b86..29e355f28 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -114,6 +114,7 @@ export default class App extends Component { clearInterval(this.statusTimer); clearTimeout(this.disableChatTimer); clearInterval(this.streamDurationTimer); + window.removeEventListener('resize', this.handleWindowResize); } // fetch /config data diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index 2953de1b3..e6a24de32 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -56,6 +56,12 @@ export default class Chat extends Component { } } + componentWillUnmount() { + if (hasTouchScreen()) { + window.removeEventListener("orientationchange", setVHvar); + } + } + setupWebSocketCallbacks() { this.websocket = this.props.websocket; if (this.websocket) { diff --git a/webroot/js/utils/helpers.js b/webroot/js/utils/helpers.js index 1f913e8f9..06aefecb8 100644 --- a/webroot/js/utils/helpers.js +++ b/webroot/js/utils/helpers.js @@ -142,18 +142,3 @@ export function debounce(fn, time) { timeout = setTimeout(functionCall, time); } } - -/* -const debouncedHandleResize = debounce(function handleResize() { - setDimensions({ - height: window.innerHeight, - width: window.innerWidth - }) -}, 1000) -window.addEventListener('resize', debouncedHandleResize) -window.addEventListener('keyup', debounce((e) => { - console.log(e); -}, 1000)); - -*/ - From b399fbba22caad9f7a47ef09c51a8ce4eb35b16e Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Wed, 26 Aug 2020 00:51:40 -0700 Subject: [PATCH 34/35] address MR comments --- webroot/index-standalone-chat.html | 13 +-- webroot/index-video-only.html | 10 +- webroot/index.html | 73 ++++++------- webroot/js/app-standalone-chat.js | 4 - webroot/js/components/chat/chat-input.js | 133 +++++++++++++---------- webroot/js/components/chat/chat.js | 10 +- webroot/js/utils/chat.js | 5 +- 7 files changed, 122 insertions(+), 126 deletions(-) diff --git a/webroot/index-standalone-chat.html b/webroot/index-standalone-chat.html index ad2a7e483..463eb3c1c 100644 --- a/webroot/index-standalone-chat.html +++ b/webroot/index-standalone-chat.html @@ -8,22 +8,17 @@ - -
    diff --git a/webroot/index-video-only.html b/webroot/index-video-only.html index cecd5583c..748bd4bdc 100644 --- a/webroot/index-video-only.html +++ b/webroot/index-video-only.html @@ -20,15 +20,9 @@
    diff --git a/webroot/index.html b/webroot/index.html index 4263caa4c..40a1eb1fd 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -28,54 +28,47 @@ + + - + +
    - - -
    - - + + - + .noscript a { + display: inline; + color: blue; + text-decoration: underline; + } + +
    + +
    +

    + This Owncast stream requires Javascript to play. +

    +
    + + diff --git a/webroot/js/app-standalone-chat.js b/webroot/js/app-standalone-chat.js index a5c4f1cb0..d2a2d0d0f 100644 --- a/webroot/js/app-standalone-chat.js +++ b/webroot/js/app-standalone-chat.js @@ -29,10 +29,6 @@ export default class StandaloneChat extends Component { }); } - handleChatToggle() { - return; - } - render(props, state) { const { username, userAvatarImage, websocket } = state; diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js index b2482105a..665195d84 100644 --- a/webroot/js/components/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -16,7 +16,7 @@ export default class ChatInput extends Component { this.emojiPickerButton = createRef(); this.messageCharCount = 0; - this.maxMessageLength = 500; + this.maxMessageLength = 500; this.maxMessageBuffer = 20; this.emojiPicker = null; @@ -98,94 +98,107 @@ export default class ChatInput extends Component { } // autocomplete user names - autoCompleteNames() { + autoCompleteNames() { const { chatUserNames } = this.props; - const { inputHTML } = this.state; - const position = getCaretPosition(this.formMessageInput.current); - const at = inputHTML.lastIndexOf('@', position - 1); - if (at === -1) { - return false; - } + const { inputHTML } = this.state; + const position = getCaretPosition(this.formMessageInput.current); + const at = inputHTML.lastIndexOf('@', position - 1); + if (at === -1) { + return false; + } - let partial = inputHTML.substring(at + 1, position).trim(); + let partial = inputHTML.substring(at + 1, position).trim(); - if (partial === this.suggestion) { - partial = this.partial; - } else { - this.partial = partial; - } + if (partial === this.suggestion) { + partial = this.partial; + } else { + this.partial = partial; + } - const possibilities = chatUserNames.filter(function (username) { - return username.toLowerCase().startsWith(partial.toLowerCase()); - }); + const possibilities = chatUserNames.filter(function (username) { + return username.toLowerCase().startsWith(partial.toLowerCase()); + }); - if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { - this.completionIndex = 0; - } + if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { + this.completionIndex = 0; + } - if (possibilities.length > 0) { - this.suggestion = possibilities[this.completionIndex]; + if (possibilities.length > 0) { + this.suggestion = possibilities[this.completionIndex]; this.setState({ inputHTML: inputHTML.substring(0, at + 1) + this.suggestion + ' ' + inputHTML.substring(position), }) - } + } - return true; + return true; } handleMessageInputKeydown(event) { - const okCodes = [37,38,39,40,16,91,18,46,8]; + const okCodes = [ + 'ArrowLeft', + 'ArrowUp', + 'ArrowRight', + 'ArrowDown', + 'Shift', + 'Meta', + 'Alt', + 'Delete', + 'Backspace', + ]; + // const okCodes = [37,38,39,40,16,91,18,46,8];//left, up , right , down , shift, left window key, alt, delete, backspace const formField = this.formMessageInput.current; let textValue = formField.innerText.trim(); // get this only to count chars let numCharsLeft = this.maxMessageLength - textValue.length; - if (event.keyCode === 13) { // enter - if (!this.prepNewLine) { - this.sendMessage(); - event.preventDefault(); - this.prepNewLine = false; - return; - } - } - if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift - this.prepNewLine = true; - } - if (event.keyCode === 9) { // tab - if (this.autoCompleteNames()) { - event.preventDefault(); + const key = event.key; - // value could have been changed, update char count - textValue = formField.innerText.trim(); - numCharsLeft = this.maxMessageLength - textValue.length; - } - } + if (key === 'Enter') { + if (!this.prepNewLine) { + this.sendMessage(); + event.preventDefault(); + this.prepNewLine = false; + return; + } + } + if (key === 'Control' || key === 'Shift') { + this.prepNewLine = true; + } + if (key === 'Tab') { + if (this.autoCompleteNames()) { + event.preventDefault(); + + // value could have been changed, update char count + textValue = formField.innerText.trim(); + numCharsLeft = this.maxMessageLength - textValue.length; + } + } // text count - if (numCharsLeft <= this.maxMessageBuffer) { - this.setState({ + if (numCharsLeft <= this.maxMessageBuffer) { + this.setState({ inputWarning: `${numCharsLeft} chars left`, }); - if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) { - event.preventDefault(); // prevent typing more - return; - } - } else { + if (numCharsLeft <= 0 && !okCodes.includes(key)) { + event.preventDefault(); // prevent typing more + return; + } + } else { this.setState({ inputWarning: '', }); - } - } + } + } - handleMessageInputKeyup(event) { - if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift - this.prepNewLine = false; - } - } + handleMessageInputKeyup(event) { + if (event.key === 'Control' || event.key === 'Shift') { + this.prepNewLine = false; + } + } - handleMessageInputBlur(event) { - this.prepNewLine = false; + handleMessageInputBlur(event) { + this.prepNewLine = false; } handlePaste(event) { diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index e6a24de32..37b60cec5 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -95,9 +95,9 @@ export default class Chat extends Component { sendUsernameChange(oldName, newName, image) { const nameChange = { type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, - oldName: oldName, - newName: newName, - image: image, + oldName, + newName, + image, }; this.websocket.send(nameChange); } @@ -106,12 +106,14 @@ export default class Chat extends Component { this.addMessage(message); } - // if incoming message has same id as existing message, don't add it addMessage(message) { const { messages: curMessages } = this.state; + + // if incoming message has same id as existing message, don't add it const existing = curMessages.filter(function (item) { return item.id === message.id; }) + if (existing.length === 0 || !existing) { const newState = { messages: [...curMessages, message], diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 896c2cc92..4b6ff89b1 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -112,7 +112,10 @@ function getYoutubeIdFromURL(url) { } function getYoutubeEmbedFromID(id) { - return ``; + return ` +
    + +
    `; } function getInstagramEmbedFromURL(url) { From cd5f71265a7798aaa9176af1718edb907b4f4d94 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Wed, 26 Aug 2020 00:57:15 -0700 Subject: [PATCH 35/35] more tab cleanup --- webroot/js/components/chat/chat-input.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js index 665195d84..99bbe83bd 100644 --- a/webroot/js/components/chat/chat-input.js +++ b/webroot/js/components/chat/chat-input.js @@ -8,7 +8,6 @@ import { generatePlaceholderText, getCaretPosition } from '../../utils/chat.js'; import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js'; import { URL_CUSTOM_EMOJIS, KEY_CHAT_FIRST_MESSAGE_SENT } from '../../utils/constants.js'; - export default class ChatInput extends Component { constructor(props, context) { super(props, context); @@ -62,7 +61,6 @@ export default class ChatInput extends Component { initialCategory: 'custom', showPreview: false, emojiSize: '30px', - // emojisPerRow: 6, position: 'right-start', strategy: 'absolute', }); @@ -146,7 +144,6 @@ export default class ChatInput extends Component { 'Delete', 'Backspace', ]; - // const okCodes = [37,38,39,40,16,91,18,46,8];//left, up , right , down , shift, left window key, alt, delete, backspace const formField = this.formMessageInput.current; let textValue = formField.innerText.trim(); // get this only to count chars @@ -193,7 +190,7 @@ export default class ChatInput extends Component { handleMessageInputKeyup(event) { if (event.key === 'Control' || event.key === 'Shift') { - this.prepNewLine = false; + this.prepNewLine = false; } } @@ -291,4 +288,4 @@ export default class ChatInput extends Component { `); } -} + }