From 7dcc89a8413f6525150c6bebe9d3c11afa6fcb11 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 16 Oct 2020 17:50:00 -0700 Subject: [PATCH] Add system chat message support to messages query --- core/chat/persistence.go | 2 +- core/chat/server.go | 2 +- .../js/components/chat/chat-message-view.js | 179 ++++++++++++++++++ 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 webroot/js/components/chat/chat-message-view.js 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/webroot/js/components/chat/chat-message-view.js b/webroot/js/components/chat/chat-message-view.js new file mode 100644 index 000000000..04eb6bd84 --- /dev/null +++ b/webroot/js/components/chat/chat-message-view.js @@ -0,0 +1,179 @@ +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 authorColor = textColorForString(author); + const backgroundColor = messageBubbleColorForString(author); + const authorTextColor = isSystemMessage ? { color: 'white' } : { color: authorColor }; + const backgroundStyle = isSystemMessage + ? { backgroundColor: '#667eea' } + : { backgroundColor: backgroundColor }; + const classString = 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, ''); +} + +