From 6c9abd9f33afbc3c3f9f9f36bb7fe9aa74aa07ca Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 21 Apr 2024 21:20:43 +0200 Subject: [PATCH] GH-29: Improve Performance and fix some inconsistencies --- src/components/ChatInput.tsx | 24 +++++- src/components/ChatMessage.tsx | 25 +++++- src/components/LightBoxImage.tsx | 2 +- src/components/contextMenus/ContextMenu.tsx | 82 +++++++++++++++++++ .../contextMenus/ContextMenuOptions.tsx | 68 +++++++++++++++ .../settings/AppearanceSettings.tsx | 41 ++++++++++ src/helper/MessageParser.tsx | 13 ++- 7 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 src/components/contextMenus/ContextMenu.tsx create mode 100644 src/components/contextMenus/ContextMenuOptions.tsx create mode 100644 src/components/settings/AppearanceSettings.tsx diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index cdb4634..c130ebc 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -2,14 +2,17 @@ import { Box, Button, Divider, Fade, IconButton, InputBase, Paper, Popper, Toolt import SendIcon from '@mui/icons-material/Send'; import DeleteIcon from '@mui/icons-material/Delete'; import GifIcon from '@mui/icons-material/Gif'; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { ChatMessageHandler } from "../helper/ChatMessage"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../store/store"; import { formatBytes } from "../helper/Fomat"; import GifSearch, { GifResult } from "./GifSearch"; import { useTranslation } from "react-i18next"; -import { invoke } from "@tauri-apps/api"; +import { invoke } from "@tauri-apps/api/core"; +import ContextMenu from "./contextMenus/ContextMenu"; +import { copy, paste } from "./contextMenus/ContextMenuOptions"; + function ChatInput() { const dispatch = useDispatch(); @@ -25,9 +28,23 @@ function ChatInput() { const currentUser = useSelector((state: RootState) => state.reducer.userInfo?.currentUser); const channelInfo = useSelector((state: RootState) => state.reducer.channel); + const sendElementRef = useRef(null); + const currentChannel = useMemo(() => channelInfo.find(e => e.channel_id === currentUser?.channel_id)?.name, [channelInfo, currentUser]); const chatMessageHandler = useMemo(() => new ChatMessageHandler(dispatch, setChatMessage), [dispatch]); + const pasteAndSend = { + icon: , + label: t('Paste and Send', { ns: 'user_interaction' }), + shortcut: 'Ctrl+Shift+V', + handler: (event: React.MouseEvent) => { + event.preventDefault(); + navigator.clipboard.readText().then(clipText => { + chatMessageHandler.sendChatMessage(clipText, currentUser); + }); + } + }; + const deleteMessages = useCallback(() => { chatMessageHandler.deleteMessages(); }, [chatMessageHandler]); @@ -103,7 +120,7 @@ function ChatInput() { - chatMessageHandler.sendChatMessage(chatMessage, currentUser)}> + chatMessageHandler.sendChatMessage(chatMessage, currentUser)}> @@ -127,6 +144,7 @@ function ChatInput() { )} + ) } diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index d45c4cd..289a003 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -5,7 +5,7 @@ import 'dayjs/plugin/isToday'; import 'dayjs/plugin/isYesterday'; import MessageParser from "../helper/MessageParser"; import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; -import { invoke } from "@tauri-apps/api"; +import { invoke } from "@tauri-apps/api/core"; import { TextMessage, deleteChatMessage } from "../store/features/users/chatMessageSlice"; import ClearIcon from '@mui/icons-material/Clear'; import { useDispatch, useSelector } from "react-redux"; @@ -36,7 +36,7 @@ const parseMessage = (message: string | undefined) => { return messageParser; } - console.log(message); + console.log("msg", message); return message; } @@ -71,10 +71,29 @@ const ChatMessage: React.FC = React.memo(({ message, messageId const { t } = useTranslation(); useEffect(() => { + const videoRepeatLength = 10; // seconds console.log('Adding event listeners'); // yes, I know this is a bad practice, but I'm not sure how to do it better - document.querySelectorAll('.user-video-element').forEach(e => { + document.querySelectorAll('.user-video-element').forEach(e => { console.log(e); + e.preload = "metadata"; + e.addEventListener('loadedmetadata', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.target) { + const videoElement = e.target as HTMLVideoElement; + console.log(videoElement.duration); + const videoLength = videoRepeatLength * Math.ceil(videoElement.duration / videoRepeatLength); + + videoElement.loop = true; + videoElement.play(); + setTimeout(() => { + videoElement.pause(); + videoElement.loop = false; + }, videoLength * 1000); + } + }); + e.addEventListener('mouseenter', (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/src/components/LightBoxImage.tsx b/src/components/LightBoxImage.tsx index 229a741..4558bc6 100644 --- a/src/components/LightBoxImage.tsx +++ b/src/components/LightBoxImage.tsx @@ -47,7 +47,7 @@ function LightBoxImage(props: LightBoxImageProps) { additionalStyles = { width: '100%', minHeight: minHeight, height: '100%' }; } - setBackgroundImage({ backgroundImage: `url(${props.src})`, backgroundColor: '#000000', backgroundSize: 'cover', ...additionalStyles }); + setBackgroundImage({ backgroundImage: `url(${props.src.replace(/(\r\n|\n|\r|\s)/gm, "")})`, backgroundColor: '#000000', backgroundSize: 'cover', ...additionalStyles }); } }, [lightboxRef.current?.offsetWidth, lightboxRef.current?.offsetHeight, imageHeight, imageWidth]); diff --git a/src/components/contextMenus/ContextMenu.tsx b/src/components/contextMenus/ContextMenu.tsx new file mode 100644 index 0000000..e33ab21 --- /dev/null +++ b/src/components/contextMenus/ContextMenu.tsx @@ -0,0 +1,82 @@ +import { ContentCopy, ContentPaste, Gif } from '@mui/icons-material'; +import { Box, Divider, ListItemIcon, ListItemText, MenuItem, MenuList, Paper, Typography } from '@mui/material'; +import React, { RefObject, useEffect, useRef, useState } from 'react'; +import ContextMenuOptions from './ContextMenuOptions'; + +interface ContextMenuProps { + options: ContextMenuOptions[]; + element: RefObject; +} + +const ContextMenu: React.FC = React.memo(({ options, element }) => { + const constexMenuMaxLength = 300; + + const contextRef = useRef(null); + const [contextMenuInfo, setContextMenuInfo] = useState({ + posX: 0, + posY: 0, + selectedText: "", + selectedElement: null as HTMLElement | null, + show: false + }); + + const handleElementClick = (event: React.MouseEvent, handler: (event: React.MouseEvent) => void) => { + event.preventDefault(); + handler(event); + setContextMenuInfo({ ...contextMenuInfo, show: false }); + }; + + + useEffect(() => { + const contextMenuEventHandler = (e: MouseEvent) => { + e.preventDefault(); + const selectedText = window.getSelection()?.toString(); + const selectedElement = e.target as HTMLElement; + setContextMenuInfo({ posX: e.pageX, posY: e.pageY, selectedText: selectedText ?? "", selectedElement, show: true }); + } + + const offClickHandler = (event: MouseEvent) => { + if (contextRef.current && !contextRef.current.contains(event.target as Node)) { + setContextMenuInfo({ ...contextMenuInfo, show: false }) + } + } + + element.current?.addEventListener('contextmenu', contextMenuEventHandler); + document.addEventListener('click', offClickHandler); + return () => { + element.current?.removeEventListener('contextmenu', contextMenuEventHandler); + document.removeEventListener('click', offClickHandler); + } + }, [contextRef]); + + let { posX: left, posY: top, selectedText, selectedElement, show } = contextMenuInfo; + + return ( + window.innerHeight - window.innerHeight * 0.1 ? top - window.innerHeight * 0.1 : top}px`, + left: `${left > window.innerWidth - constexMenuMaxLength ? left - constexMenuMaxLength : left}px`, + display: show ? 'block' : 'none', + }}> + + + {options.map((option, index) => ( + handleElementClick(e, option.handler)}> + + {option.icon} + + {option.label} + + {option.shortcut} + + + ))} + + + + ); +}); + +export default ContextMenu; \ No newline at end of file diff --git a/src/components/contextMenus/ContextMenuOptions.tsx b/src/components/contextMenus/ContextMenuOptions.tsx new file mode 100644 index 0000000..3321c52 --- /dev/null +++ b/src/components/contextMenus/ContextMenuOptions.tsx @@ -0,0 +1,68 @@ +import { FileCopyOutlined } from "@mui/icons-material"; +import { invoke } from "@tauri-apps/api/core"; + +export default interface ContextMenuOptions { + icon: React.ReactNode; + label: string; + shortcut: string; + handler: (event: React.MouseEvent) => void; +} + +export const paste = { + icon: , + label: "Paste", + shortcut: "Ctrl+V", + handler: (event: React.MouseEvent) => { + event.preventDefault(); + navigator.clipboard.readText().then(clipText => { + console.log(clipText); + }); + } +}; + +export const copy = { + icon: , + label: "Copy", + shortcut: "Ctrl+C", + handler: (event: React.MouseEvent) => { + event.preventDefault(); + const selectedElement = event.target as HTMLElement; + copyElement(selectedElement); + } +}; + +export const showDeveloperTools = { + icon: , + label: "Developer Tools", + shortcut: "Ctrl+Shift+I", + handler: (event: React.MouseEvent) => { + event.preventDefault(); + invoke("dev_tools"); + } +}; + + +async function copyElement(element: HTMLElement) { + let clipboardItemData = {}; + + if (element instanceof HTMLImageElement) { + const response = await fetch(element.src); + const blob = await response.blob(); + clipboardItemData = { + 'image/png': blob + }; + } else if (element instanceof HTMLVideoElement) { + const response = await fetch(element.src); + const blob = await response.blob(); + clipboardItemData = { + 'video/mp4': blob + }; + } else if (element instanceof HTMLParagraphElement || element instanceof HTMLSpanElement) { + clipboardItemData = { + 'text/plain': new Blob([element.innerText], { type: 'text/plain' }) + }; + } + + const clipboardItem = new ClipboardItem(clipboardItemData); + await navigator.clipboard.write([clipboardItem]); +} \ No newline at end of file diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx new file mode 100644 index 0000000..a6fa1ff --- /dev/null +++ b/src/components/settings/AppearanceSettings.tsx @@ -0,0 +1,41 @@ +import { Box, CircularProgress, Container, Divider, Typography, } from "@mui/material"; +import { useDispatch } from "react-redux"; +import './styles/Profile.css' +import { useTranslation } from "react-i18next"; +import UploadBox from "../UploadBox"; +import { useState } from "react"; + + +function AppearanceSettings() { + const dispatch = useDispatch(); + const [t] = useTranslation(); + + let [loading, setLoading] = useState(false); + + function displayLoadingText(text: string) { + if (loading) { + return () + } else { + return ({text}) + } + } + + return ( + + + {t("Appearance", { ns: "appearance" })} + + + showUploadBox(path, ImageType.Background, 3 / 1, "rect")}>{displayLoadingText(t("Background Image", { ns: "appearance" }))} + + + + ) +} + +export default AppearanceSettings; \ No newline at end of file diff --git a/src/helper/MessageParser.tsx b/src/helper/MessageParser.tsx index c204613..bf63d7e 100644 --- a/src/helper/MessageParser.tsx +++ b/src/helper/MessageParser.tsx @@ -11,6 +11,7 @@ interface LinkReplacement { } class DOMMessageParser { + static sanitizingHookRegistered = false; private document: Document; private replacementUrl: LinkReplacement[] = [ { regex: /https:\/\/store\.steampowered\.com\/app\/([0-9]+)\/?.*/, replacement: 'steam://advertise/$1', inline: false }, @@ -27,7 +28,12 @@ class DOMMessageParser { const parser = new DOMParser(); this.document = parser.parseFromString(input, "text/html"); + if (DOMMessageParser.sanitizingHookRegistered) { + return; + } + DOMPurify.addHook('afterSanitizeAttributes', function (node) { + DOMMessageParser.sanitizingHookRegistered = true; console.log("Sanitizing attributes"); DOMPurify.addHook('afterSanitizeAttributes', function (node) { if (node.tagName && node.tagName === 'IMG' && node.hasAttribute('src')) { @@ -96,7 +102,7 @@ class MessageParser { private input: string; constructor(input: string) { - this.input = DOMPurify.sanitize(input); + this.input = input; } parseDOM(dom: (value: DOMMessageParser) => DOMMessageParser) { @@ -175,17 +181,16 @@ class MessageParser { parseMarkdown() { this.input = marked.parseInline(this.input); - this.input = DOMPurify.sanitize(this.input); return this; } public build() { - return (); + return (); } public buildString() { - return this.input; + return DOMPurify.sanitize(this.input); } }