|
|
|
@ -2,38 +2,17 @@ import { h, Component } from '/js/web_modules/preact.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` |
|
|
|
|
<div class="message flex flex-row items-start p-3"> |
|
|
|
|
<div class="message-content text-sm break-words w-full"> |
|
|
|
|
<div class="message-author text-white font-bold" style=${authorTextColor}> |
|
|
|
|
${author} |
|
|
|
|
</div> |
|
|
|
|
<div |
|
|
|
|
class="message-text text-gray-300 font-normal overflow-y-hidden" |
|
|
|
|
title=${formattedTimestamp} |
|
|
|
|
dangerouslySetInnerHTML=${ |
|
|
|
|
{ __html: formattedMessage } |
|
|
|
|
} |
|
|
|
|
></div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
`);
|
|
|
|
|
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 {
@@ -47,121 +26,9 @@ export default class Message extends Component {
|
|
|
|
|
</div> |
|
|
|
|
` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
console.log("Unknown message type:", type); |
|
|
|
|
} |
|
|
|
|
} 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, ''); |
|
|
|
|
} |
|
|
|
|