diff --git a/src/common/Portal.tsx b/src/common/Portal.tsx new file mode 100644 index 0000000..1e0cb26 --- /dev/null +++ b/src/common/Portal.tsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' + +interface PortalProps { + children: React.ReactNode +} + +const Portal: React.FC = ({ children }) => { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + + return () => setMounted(false) + }, []) + + return mounted + ? createPortal( + children, + document.querySelector('#modal-root') as HTMLElement + ) + : null +} + +export default Portal diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 390d7fa..673aa02 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -4,11 +4,11 @@ import './styles.css'; import { executeEvent } from '../../utils/events'; const extractComponents = (url) => { - if (!url.startsWith("qortal://")) { + if (!url || !url.startsWith("qortal://")) { // Check if url exists and starts with "qortal://" return null; } - url = url.replace(/^(qortal\:\/\/)/, ""); + url = url.replace(/^(qortal\:\/\/)/, ""); // Safe to use replace now if (url.includes("/")) { let parts = url.split("/"); const service = parts[0].toUpperCase(); @@ -23,6 +23,7 @@ const extractComponents = (url) => { return null; }; + function processText(input) { const linkRegex = /(qortal:\/\/\S+)/g; function processNode(node) { @@ -58,6 +59,8 @@ function processText(input) { export const MessageDisplay = ({ htmlContent, isReply }) => { const linkify = (text) => { + if (!text) return ""; // Return an empty string if text is null or undefined + let textFormatted = text; const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; textFormatted = text.replace(urlPattern, (url) => { @@ -66,6 +69,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { }); return processText(textFormatted); }; + const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), { ALLOWED_TAGS: [ diff --git a/src/components/ReactionPicker.css b/src/components/ReactionPicker.css index a07eef3..89dbe39 100644 --- a/src/components/ReactionPicker.css +++ b/src/components/ReactionPicker.css @@ -5,10 +5,23 @@ .emoji-picker { position: absolute; /* Picker positioned absolutely relative to the parent */ right: 0; - z-index: 1000; /* Ensure picker appears above other content */ + z-index: 9000000000; /* Ensure picker appears above other content */ } .message-container { overflow: visible; /* Ensure the message container doesn't cut off the picker */ } - \ No newline at end of file + + + .reaction-container { + position: relative; + } + + .emoji-picker { + overflow: hidden; + width: auto + } + + .EmojiPickerReact.epr-dark-theme { + --epr-emoji-size: 18px; /* Adjust emoji size for dark mode */ + } \ No newline at end of file diff --git a/src/components/ReactionPicker.tsx b/src/components/ReactionPicker.tsx index 91e5085..2f5f08e 100644 --- a/src/components/ReactionPicker.tsx +++ b/src/components/ReactionPicker.tsx @@ -1,39 +1,65 @@ import React, { useState, useRef, useEffect } from 'react'; -import Picker, { Theme } from 'emoji-picker-react'; -import './ReactionPicker.css'; // CSS for proper positioning +import ReactDOM from 'react-dom'; +import Picker, { EmojiStyle, Theme } from 'emoji-picker-react'; +import './ReactionPicker.css'; import { ButtonBase } from '@mui/material'; import { isMobile } from '../App'; export const ReactionPicker = ({ onReaction }) => { - const [showPicker, setShowPicker] = useState(false); // Manage picker visibility - const pickerRef = useRef(null); // Reference to the picker + const [showPicker, setShowPicker] = useState(false); + const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 }); + const pickerRef = useRef(null); + const buttonRef = useRef(null); const handleReaction = (emojiObject) => { - onReaction(emojiObject.emoji); // Handle the selected emoji reaction - setShowPicker(false); // Close picker after selection + onReaction(emojiObject.emoji); + setShowPicker(false); }; + const handlePicker = (emojiObject) => { - - onReaction(emojiObject.emoji); // Handle the selected emoji reaction - setShowPicker(false); // Close picker after selection + onReaction(emojiObject.emoji); + setShowPicker(false); + }; + + const togglePicker = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (showPicker) { + setShowPicker(false); + } else { + // Get the button's position + const buttonRect = buttonRef.current.getBoundingClientRect(); + const pickerWidth = isMobile ? 300 : 350; // Adjust based on picker width + + // Calculate position to align the right edge of the picker with the button's right edge + setPickerPosition({ + top: buttonRect.bottom + window.scrollY, // Position below the button + left: buttonRect.right + window.scrollX - pickerWidth, // Align right edges + }); + setShowPicker(true); + } }; // Close picker if clicked outside useEffect(() => { const handleClickOutside = (event) => { - if (pickerRef.current && !pickerRef.current.contains(event.target)) { - setShowPicker(false); // Close picker + if ( + pickerRef.current && + !pickerRef.current.contains(event.target) && + buttonRef.current && + !buttonRef.current.contains(event.target) + ) { + setShowPicker(false); } }; - // Add event listener when picker is shown if (showPicker) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } - // Clean up the event listener on unmount return () => { document.removeEventListener('mousedown', handleClickOutside); }; @@ -42,42 +68,41 @@ export const ReactionPicker = ({ onReaction }) => { return (
{/* Emoji CTA */} - { - // e.preventDefault(); // Prevent mobile keyboard - // e.stopPropagation(); - // if(!isMobile) return - // setShowPicker(!showPicker); - // }} - onClick={(e) => { - e.preventDefault(); // Prevents any focus issues - e.stopPropagation(); - // if(isMobile) return - - setShowPicker(!showPicker); - }} - + 😃 - {/* Emoji Picker with dark theme */} - {showPicker && ( -
e.preventDefault()}> - -
- )} + {/* Emoji Picker rendered in a portal with calculated position */} + {showPicker && + ReactDOM.createPortal( +
+ +
, + document.body + )}
); }; diff --git a/src/index.css b/src/index.css index 0b999f4..f79b1ef 100644 --- a/src/index.css +++ b/src/index.css @@ -107,4 +107,5 @@ html, body { .swiper { width: 100%; -} \ No newline at end of file +} +