Untitled
unknown
plain_text
a year ago
7.3 kB
3
Indexable
Never
import React, { useState, useEffect } from 'react'; import firebase from 'firebase/app'; import 'firebase/firestore'; import 'firebase/storage'; import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import DOMPurify from 'dompurify'; const ChatMessage = (props) => { const { text, uid, fileUrl, photoURL } = props.message; const messageClass = uid === firebase.auth().currentUser.uid ? 'sent' : 'received'; const [username, setUsername] = useState(null); const [taggedText, setTaggedText] = useState(null); const [avatarUrl, setAvatarUrl] = useState(null); const handleDeleteClick = async () => { const confirmed = window.confirm("Are you sure you want to delete this message?"); if (confirmed) { const db = firebase.firestore(); await db.collection("messages").doc(props.id).delete(); } }; useEffect(() => { const storageRef = firebase.storage().ref(); const avatarRef = storageRef.child(`profile-pictures/${uid}/0.png`); avatarRef.getDownloadURL() .then((url) => { setAvatarUrl(url); }) .catch(() => { if (firebase.auth().photoURL) { setAvatarUrl(firebase.auth().photoURL); } }); const fileRef = storageRef.child(fileUrl); fileRef.getMetadata() .then(() => { fileRef.getDownloadURL() .then((url) => { setTaggedText(`<a href="${url}" target="_blank">${fileUrl}</a>`); }); }) .catch((error) => { if (error.code === 'storage/object-not-found') { // File doesn't exist, don't attempt to download it if (text) { const linkRegex = /(?:^|[^"'])(https?:\/\/[^\s"]+)(?=["']|$)/g; const newLinkedText = text.replace(linkRegex, (match) => { return `<a href="${match}" class="link-preview" target="_blank">${match}</a>`; }); setTaggedText(newLinkedText); } else { setTaggedText(null); } } else { console.error(error); } }); const fetchUsernameAndTaggedText = async () => { const usernameSnapshot = await firebase.database().ref(`users/${uid}/username`).once('value'); const usernameData = usernameSnapshot.val(); if (usernameData) { setUsername(usernameData); } else { setUsername(firebase.auth().displayName); } const tagRegex = /@([\w\s]+)/g; const matches = text ? text.match(tagRegex) : null; if (matches) { const replacedMatches = await Promise.all(matches.map(async (match) => { const username = match.trim().substring(1); const userRef = firebase.database().ref(`users`).orderByChild('username').equalTo(username); const snapshot = await userRef.once('value'); const user = snapshot.val(); if (user) { const uid = Object.keys(user)[0]; return uid; } else { return null; } })); let currentIndex = 0; const newTaggedText = text.replace(tagRegex, (match) => { const uid = replacedMatches[currentIndex++]; if (uid) { return `<a href="/profile/${uid}" style="color: blue;">${match}</a>`; } else { return match; } }); const linkRegex = /(?:^|[^"'])(https?:\/\/[^\s"]+)(?=["']|$)/g; const newLinkedText = newTaggedText.replace(linkRegex, (match) => { return `<a href="${match}" class="link-preview" target="_blank">${match}</a>`; }); setTaggedText(newLinkedText); } else { const linkRegex = /(?:^|[^"'])(https?:\/\/[^\s"]+)(?=["']|$)/g; const newLinkedText = text ? text.replace(linkRegex, (match) => { return `<a href="${match}" class="link-preview" target="_blank">${match}</a>`; }) : null; if (newLinkedText !== text) { setTaggedText(newLinkedText); } else { setTaggedText(text); } } } fetchUsernameAndTaggedText(); }, [uid, text, taggedText, fileUrl]); const usernameClass = messageClass === 'sent' ? 'username-sent' : 'username-received'; const [showDelete, setShowDelete] = useState(false); const showDeleteButton = () => { if (uid === firebase.auth().currentUser.uid) { setShowDelete(true); } }; const hideDeleteButton = () => { setShowDelete(false); }; const [linkPreview, setLinkPreview] = useState(null); useEffect(() => { const linkRegex = /(?:^|[^"'])(https?:\/\/[^\s"]+)(?=["']|$)/g; const matches = text ? text.match(linkRegex) : null; if (matches) { const link = matches[0]; const url = new URL(link); const previewUrl = `https://api.linkpreview.net/?key=${process.env.REACT_APP_LINK_PREVIEW_API_KEY}&q=${url}`; fetch(previewUrl) .then(response => response.json()) .then(data => { if (data.title) { setLinkPreview({ url: url.href, title: data.title, description: data.description, image: data.image }); } }) .catch(error => console.error(error)); } }, [text]); const sanitizedText = DOMPurify.sanitize(taggedText, { ALLOWED_TAGS: ['a', 'img'], ALLOWED_ATTR: ['href', 'src'] }); const limitWords = (str, limit) => { const words = str.trim().split(/\s+/); return words.length <= limit ? str : words.slice(0, limit).join(' ') + '...'; }; const linkPreviewHTML = linkPreview ? ` <a href="${linkPreview.url}" target="_blank" rel="noopener noreferrer" class="link-preview"> ${linkPreview.image ? `<img src="${linkPreview.image}" alt="${linkPreview.title}" class="link-preview-image" />` : ''} <div class="link-preview-details"> ${linkPreview.title ? `<h4 class="link-preview-title">${linkPreview.title}</h4>` : ''} ${linkPreview.description ? `<p class="link-preview-description">${limitWords(linkPreview.description, 20)}</p>` : ''} </div> </a>` : ''; const safeHTML = `<span class="${usernameClass}">${username}: </span>${sanitizedText.replace(/<a /g, '<a style="color: blue;" ')}${linkPreviewHTML}`; return ( <div className={`message ${messageClass}`} onMouseEnter={showDeleteButton} onMouseLeave={hideDeleteButton} > {avatarUrl ? ( <img src={avatarUrl} alt="Avatar" /> ) : ( <img src={photoURL} alt="Avatar" /> )} <div className="message-content"> {username && taggedText && ( <p className="message-text" dangerouslySetInnerHTML={{ __html: safeHTML }} /> )} {fileUrl && ( <a href={fileUrl} target="_blank" rel="noopener noreferrer"> <img src={fileUrl} alt="chat attachment" className="message-image-preview" /> </a> )} {showDelete && ( <div className="delete-button" onClick={handleDeleteClick}> <FontAwesomeIcon icon={faTrash} /> </div> )} </div> </div> ); }; export default ChatMessage;