diff --git a/core/chat/persistence.go b/core/chat/persistence.go index 65bb65930..dcf4c8d0e 100644 --- a/core/chat/persistence.go +++ b/core/chat/persistence.go @@ -70,7 +70,7 @@ func getChatHistory() []models.ChatMessage { history := make([]models.ChatMessage, 0) // Get all messages sent within the past day - rows, err := _db.Query("SELECT * FROM messages WHERE visible = 1 AND datetime(timestamp) >=datetime('now', '-1 Day')") + rows, err := _db.Query("SELECT * FROM messages WHERE visible = 1 AND messageType != 'SYSTEM' AND datetime(timestamp) >=datetime('now', '-1 Day')") if err != nil { log.Fatal(err) } diff --git a/core/chat/server.go b/core/chat/server.go index ad70972d5..15c6a37c2 100644 --- a/core/chat/server.go +++ b/core/chat/server.go @@ -140,7 +140,7 @@ func (s *server) sendWelcomeMessageToClient(c *Client) { time.Sleep(7 * time.Second) initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary) - initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, "initial-message-1", "CHAT", true, time.Now()} + initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, "initial-message-1", "SYSTEM", true, time.Now()} c.Write(initialMessage) }() diff --git a/test/userColorsTest.js b/test/userColorsTest.js index 1dcfaac32..7b8d39f55 100644 --- a/test/userColorsTest.js +++ b/test/userColorsTest.js @@ -11,8 +11,9 @@ for (var i = 0; i < 20; i++) { console.log(``); function generateElement(string) { - const color = messageBubbleColorForString(string); - return `
${string}
` + const bgColor = messageBubbleColorForString(string); + const fgColor = textColorForString(string); + return `
${string}
`; } function randomString(length) { @@ -27,9 +28,25 @@ function messageBubbleColorForString(str) { } // Tweak these to adjust the result of the color - const saturation = 75; - const lightness = 65; - const alpha = 1.0; + const saturation = 25; + const lightness = 45; + const alpha = 0.3; + const hue = parseInt(Math.abs(hash), 16) % 360; + + return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; +} + +function textColorForString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + // eslint-disable-next-line + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + + // Tweak these to adjust the result of the color + const saturation = 80; + const lightness = 80; + const alpha = 0.8; const hue = parseInt(Math.abs(hash), 16) % 360; return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; diff --git a/webroot/js/components/chat/chat-message-view.js b/webroot/js/components/chat/chat-message-view.js new file mode 100644 index 000000000..934ae6a33 --- /dev/null +++ b/webroot/js/components/chat/chat-message-view.js @@ -0,0 +1,182 @@ +import { h, Component } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; +const html = htm.bind(h); + +import { + messageBubbleColorForString, + textColorForString, +} from '../../utils/user-colors.js'; +import { convertToText } from '../../utils/chat.js'; +import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; + +export default class ChatMessageView extends Component { + render() { + const { message, username } = this.props; + const { author, body, timestamp } = message; + + const formattedMessage = formatMessageText(body, username); + const formattedTimestamp = formatTimestamp(timestamp); + + const isSystemMessage = message.type === SOCKET_MESSAGE_TYPES.SYSTEM; + + const authorTextColor = isSystemMessage + ? { color: '#fff' } + : { color: textColorForString(author) }; + const backgroundStyle = isSystemMessage + ? { backgroundColor: '#667eea' } + : { backgroundColor: messageBubbleColorForString(author) }; + const messageClassString = isSystemMessage + ? getSystemMessageClassString() + : getChatMessageClassString(); + + return html` +
+
+
+ ${author} +
+
+
+
+ `; + } +} + +function getSystemMessageClassString() { + return 'message flex flex-row items-start p-4 m-2 rounded-lg shadow-l border-solid border-indigo-700 border-2 border-opacity-60 text-l'; +} + +function getChatMessageClassString() { + return 'message flex flex-row items-start p-3 m-3 rounded-lg shadow-s text-sm'; +} + +export function formatMessageText(message, username) { + let formattedText = highlightUsername(message, username); + formattedText = getMessageWithEmbeds(formattedText); + return convertToMarkup(formattedText); +} + +function highlightUsername(message, username) { + const pattern = new RegExp( + '@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), + 'gi' + ); + return message.replace( + pattern, + '$&' + ); +} + +function getMessageWithEmbeds(message) { + var embedText = ''; + // Make a temporary element so we can actually parse the html and pull anchor tags from it. + // This is a better approach than regex. + var container = document.createElement('p'); + container.innerHTML = message; + + var anchors = container.getElementsByTagName('a'); + for (var i = 0; i < anchors.length; i++) { + const url = anchors[i].href; + if (getYoutubeIdFromURL(url)) { + const youtubeID = getYoutubeIdFromURL(url); + embedText += getYoutubeEmbedFromID(youtubeID); + } else if (url.indexOf('instagram.com/p/') > -1) { + embedText += getInstagramEmbedFromURL(url); + } else if (isImage(url)) { + embedText += getImageForURL(url); + } + } + + // If this message only consists of a single embeddable link + // then only return the embed and strip the link url from the text. + if ( + embedText !== '' && + anchors.length == 1 && + isMessageJustAnchor(message, anchors[0]) + ) { + return embedText; + } + return message + embedText; +} + +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)$/i; + return re.test(url); +} + +function getImageForURL(url) { + return ``; +} + +function isMessageJustAnchor(message, anchor) { + return stripTags(message) === stripTags(anchor.innerHTML); +} + +function formatTimestamp(sentAt) { + sentAt = new Date(sentAt); + if (isNaN(sentAt)) { + return ''; + } + + let diffInDays = (new Date() - sentAt) / (24 * 3600 * 1000); + if (diffInDays >= 1) { + return ( + `Sent at ${sentAt.toLocaleDateString('en-US', { + dateStyle: 'medium', + })} at ` + sentAt.toLocaleTimeString() + ); + } + + return `Sent at ${sentAt.toLocaleTimeString()}`; +} + +/* + You would call this when receiving a plain text + value back from an API, and before inserting the + text into the `contenteditable` area on a page. +*/ +function convertToMarkup(str = '') { + return convertToText(str).replace(/\n/g, '
'); +} + +function stripTags(str) { + return str.replace(/<\/?[^>]+(>|$)/g, ''); +} + + diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js index 7ca63e1b3..e0fbf21f9 100644 --- a/webroot/js/components/chat/message.js +++ b/webroot/js/components/chat/message.js @@ -2,38 +2,17 @@ import { h, Component } from '/js/web_modules/preact.js'; import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); +import ChatMessageView from './chat-message-view.js'; + import { messageBubbleColorForString } from '../../utils/user-colors.js'; -import { convertToText } from '../../utils/chat.js'; import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; export default class Message extends Component { render(props) { - const { message, username } = props; + const { message } = props; const { type } = message; - if (type === SOCKET_MESSAGE_TYPES.CHAT) { - const { author, body, timestamp } = message; - const formattedMessage = formatMessageText(body, username); - const formattedTimestamp = formatTimestamp(timestamp); - - const authorColor = messageBubbleColorForString(author); - const authorTextColor = { color: authorColor }; - return ( - html` -
-
-
- ${author} -
-
-
-
- `); + if (type === SOCKET_MESSAGE_TYPES.CHAT || type === SOCKET_MESSAGE_TYPES.SYSTEM) { + return html`<${ChatMessageView} ...${props} />`; } else if (type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { const { oldName, newName } = message; return ( @@ -47,121 +26,9 @@ export default class Message extends Component { ` ); - } - } -} - -export function formatMessageText(message, username) { - let formattedText = highlightUsername(message, username); - formattedText = getMessageWithEmbeds(formattedText); - return convertToMarkup(formattedText); -} - -function highlightUsername(message, username) { - const pattern = new RegExp( - '@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), - 'gi' - ); - return message.replace( - pattern, - '$&' - ); -} - -function getMessageWithEmbeds(message) { - var embedText = ''; - // Make a temporary element so we can actually parse the html and pull anchor tags from it. - // This is a better approach than regex. - var container = document.createElement('p'); - container.innerHTML = message; - - var anchors = container.getElementsByTagName('a'); - for (var i = 0; i < anchors.length; i++) { - const url = anchors[i].href; - if (getYoutubeIdFromURL(url)) { - const youtubeID = getYoutubeIdFromURL(url); - embedText += getYoutubeEmbedFromID(youtubeID); - } else if (url.indexOf('instagram.com/p/') > -1) { - embedText += getInstagramEmbedFromURL(url); - } else if (isImage(url)) { - embedText += getImageForURL(url); - } - } - - // If this message only consists of a single embeddable link - // then only return the embed and strip the link url from the text. - if (embedText !== '' && anchors.length == 1 && isMessageJustAnchor(message, anchors[0])) { - return embedText; - } - return message + embedText; -} - -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; + console.log("Unknown message type:", type); } - } 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)$/i; - return re.test(url); -} - -function getImageForURL(url) { - return ``; -} - -function isMessageJustAnchor(message, anchor) { - return stripTags(message) === stripTags(anchor.innerHTML); -} - -function formatTimestamp(sentAt) { - sentAt = new Date(sentAt); - if (isNaN(sentAt)) { - return ''; - } - - let diffInDays = (new Date() - sentAt) / (24 * 3600 * 1000); - if (diffInDays >= 1) { - return ( - `Sent at ${sentAt.toLocaleDateString('en-US', { - dateStyle: 'medium', - })} at ` + sentAt.toLocaleTimeString() - ); - } - - return `Sent at ${sentAt.toLocaleTimeString()}`; -} - -/* - You would call this when receiving a plain text - value back from an API, and before inserting the - text into the `contenteditable` area on a page. -*/ -function convertToMarkup(str = '') { - return convertToText(str).replace(/\n/g, '
'); -} - -function stripTags(str) { - return str.replace(/<\/?[^>]+(>|$)/g, ''); -} diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 4b0af9e0e..49590c66a 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -64,7 +64,6 @@ export function extraUserNamesFromMessageHistory(messages) { return list; } - // utils from https://gist.github.com/nathansmith/86b5d4b23ed968a92fd4 /* You would call this after getting an element's diff --git a/webroot/js/utils/user-colors.js b/webroot/js/utils/user-colors.js index 70b2e4832..331db934a 100644 --- a/webroot/js/utils/user-colors.js +++ b/webroot/js/utils/user-colors.js @@ -6,9 +6,25 @@ export function messageBubbleColorForString(str) { } // Tweak these to adjust the result of the color - const saturation = 75; - const lightness = 65; - const alpha = 1.0; + const saturation = 25; + const lightness = 45; + const alpha = 0.3; + const hue = parseInt(Math.abs(hash), 16) % 360; + + return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; +} + +export function textColorForString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + // eslint-disable-next-line + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + + // Tweak these to adjust the result of the color + const saturation = 80; + const lightness = 80; + const alpha = 0.8; const hue = parseInt(Math.abs(hash), 16) % 360; return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; diff --git a/webroot/js/utils/websocket.js b/webroot/js/utils/websocket.js index 893f552ea..32dd74515 100644 --- a/webroot/js/utils/websocket.js +++ b/webroot/js/utils/websocket.js @@ -7,7 +7,8 @@ export const SOCKET_MESSAGE_TYPES = { CHAT: 'CHAT', PING: 'PING', NAME_CHANGE: 'NAME_CHANGE', - PONG: 'PONG' + PONG: 'PONG', + SYSTEM: 'SYSTEM' }; export const CALLBACKS = {