mirror of
https://github.com/owncast/owncast.git
synced 2024-11-25 06:12:23 +03:00
Add system chat message support to messages query
This commit is contained in:
parent
f4815e331c
commit
7dcc89a841
3 changed files with 181 additions and 2 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}()
|
||||
|
||||
|
|
179
webroot/js/components/chat/chat-message-view.js
Normal file
179
webroot/js/components/chat/chat-message-view.js
Normal file
|
@ -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`
|
||||
<div
|
||||
style=${backgroundStyle}
|
||||
class=${classString}
|
||||
title=${formattedTimestamp}
|
||||
>
|
||||
<div class="message-content break-words w-full">
|
||||
<div
|
||||
style=${authorTextColor}
|
||||
class="message-author text-white font-bold"
|
||||
>
|
||||
${author}
|
||||
</div>
|
||||
<div
|
||||
class="message-text text-gray-300 font-normal overflow-y-hidden pt-2"
|
||||
dangerouslySetInnerHTML=${{ __html: formattedMessage }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
'<span class="highlighted px-1 rounded font-bold bg-orange-500">$&</span>'
|
||||
);
|
||||
}
|
||||
|
||||
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 `<div class="chat-embed youtube-embed"><lite-youtube videoid="${id}" /></div>`;
|
||||
}
|
||||
|
||||
function getInstagramEmbedFromURL(url) {
|
||||
const urlObject = new URL(url.replace(/\/$/, ''));
|
||||
urlObject.pathname += '/embed';
|
||||
return `<iframe class="chat-embed instagram-embed" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
|
||||
}
|
||||
|
||||
function isImage(url) {
|
||||
const re = /\.(jpe?g|png|gif)$/i;
|
||||
return re.test(url);
|
||||
}
|
||||
|
||||
function getImageForURL(url) {
|
||||
return `<a target="_blank" href="${url}"><img class="chat-embed embedded-image" src="${url}" /></a>`;
|
||||
}
|
||||
|
||||
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, '<br>');
|
||||
}
|
||||
|
||||
function stripTags(str) {
|
||||
return str.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
}
|
||||
|
||||
|
Loading…
Reference in a new issue