Skip to content

Commit

Permalink
GH-29: Improve Performance and fix some inconsistencies
Browse files Browse the repository at this point in the history
  • Loading branch information
SetZero committed Apr 21, 2024
1 parent 3420463 commit 6c9abd9
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 11 deletions.
24 changes: 21 additions & 3 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<HTMLButtonElement>(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: <SendIcon />,
label: t('Paste and Send', { ns: 'user_interaction' }),
shortcut: 'Ctrl+Shift+V',
handler: (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
navigator.clipboard.readText().then(clipText => {
chatMessageHandler.sendChatMessage(clipText, currentUser);
});
}
};

const deleteMessages = useCallback(() => {
chatMessageHandler.deleteMessages();
}, [chatMessageHandler]);
Expand Down Expand Up @@ -103,7 +120,7 @@ function ChatInput() {
<GifIcon />
</IconButton>
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
<IconButton sx={{ p: '10px' }} aria-label="Send Message" onClick={() => chatMessageHandler.sendChatMessage(chatMessage, currentUser)}>
<IconButton ref={sendElementRef} sx={{ p: '10px' }} aria-label="Send Message" onClick={() => chatMessageHandler.sendChatMessage(chatMessage, currentUser)}>
<SendIcon />
</IconButton>
</Paper>
Expand All @@ -127,6 +144,7 @@ function ChatInput() {
</Fade>
)}
</Popper>
<ContextMenu element={sendElementRef} options={[copy, paste, pasteAndSend]} />
</Box>
)
}
Expand Down
25 changes: 22 additions & 3 deletions src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -36,7 +36,7 @@ const parseMessage = (message: string | undefined) => {
return messageParser;
}

console.log(message);
console.log("msg", message);

return message;
}
Expand Down Expand Up @@ -71,10 +71,29 @@ const ChatMessage: React.FC<ChatMessageProps> = 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<HTMLVideoElement>('.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();
Expand Down
2 changes: 1 addition & 1 deletion src/components/LightBoxImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
82 changes: 82 additions & 0 deletions src/components/contextMenus/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>;
}

const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ options, element }) => {
const constexMenuMaxLength = 300;

const contextRef = useRef<HTMLDivElement>(null);
const [contextMenuInfo, setContextMenuInfo] = useState({
posX: 0,
posY: 0,
selectedText: "",
selectedElement: null as HTMLElement | null,
show: false
});

const handleElementClick = (event: React.MouseEvent<HTMLElement>, handler: (event: React.MouseEvent<HTMLElement>) => 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 (
<Box
ref={contextRef}
style={{
position: 'absolute',
top: `${top > 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',
}}>
<Paper sx={{ width: constexMenuMaxLength }}>
<MenuList>
{options.map((option, index) => (
<MenuItem key={index} onClick={(e) => handleElementClick(e, option.handler)}>
<ListItemIcon>
{option.icon}
</ListItemIcon>
<ListItemText sx={{ marginRight: 2 }}>{option.label}</ListItemText>
<Typography variant="body2" color="text.secondary">
{option.shortcut}
</Typography>
</MenuItem>
))}
</MenuList>
</Paper>
</Box>
);
});

export default ContextMenu;
68 changes: 68 additions & 0 deletions src/components/contextMenus/ContextMenuOptions.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>) => void;
}

export const paste = {
icon: <FileCopyOutlined />,
label: "Paste",
shortcut: "Ctrl+V",
handler: (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
navigator.clipboard.readText().then(clipText => {
console.log(clipText);
});
}
};

export const copy = {
icon: <FileCopyOutlined />,
label: "Copy",
shortcut: "Ctrl+C",
handler: (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
const selectedElement = event.target as HTMLElement;
copyElement(selectedElement);
}
};

export const showDeveloperTools = {
icon: <FileCopyOutlined />,
label: "Developer Tools",
shortcut: "Ctrl+Shift+I",
handler: (event: React.MouseEvent<HTMLElement>) => {
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]);
}
41 changes: 41 additions & 0 deletions src/components/settings/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
@@ -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 (<CircularProgress />)
} else {
return (<Typography>{text}</Typography>)
}
}

return (
<Box>
<Container className="settingsContainer">
<Typography variant="h4">{t("Appearance", { ns: "appearance" })}</Typography>
<Divider sx={{ marginBottom: 5 }} />
<Box sx={{
display: 'flex',
flexDirection: 'row',
alignContent: 'center',
maxWidth: '100%'
}}>
<UploadBox onUpload={(path) => showUploadBox(path, ImageType.Background, 3 / 1, "rect")}>{displayLoadingText(t("Background Image", { ns: "appearance" }))}</UploadBox>
</Box>
</Container>
</Box >
)
}

export default AppearanceSettings;
13 changes: 9 additions & 4 deletions src/helper/MessageParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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')) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -175,17 +181,16 @@ class MessageParser {

parseMarkdown() {
this.input = marked.parseInline(this.input);
this.input = DOMPurify.sanitize(this.input);

return this;
}

public build() {
return (<Box dangerouslySetInnerHTML={{ __html: this.input }}></Box>);
return (<Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.input) }}></Box>);
}

public buildString() {
return this.input;
return DOMPurify.sanitize(this.input);
}
}

Expand Down

0 comments on commit 6c9abd9

Please sign in to comment.