This commit is contained in:
PhilReact 2024-09-27 02:03:44 +03:00
parent 9b14f5be19
commit c577b91dfe
4 changed files with 228 additions and 218 deletions

View File

@ -1,34 +1,35 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from "react";
import { EditorProvider, useCurrentEditor } from '@tiptap/react'; import { EditorProvider, useCurrentEditor } from "@tiptap/react";
import StarterKit from '@tiptap/starter-kit'; import StarterKit from "@tiptap/starter-kit";
import { Color } from '@tiptap/extension-color'; import { Color } from "@tiptap/extension-color";
import ListItem from '@tiptap/extension-list-item'; import ListItem from "@tiptap/extension-list-item";
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from "@tiptap/extension-text-style";
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from "@tiptap/extension-placeholder";
import Image from '@tiptap/extension-image'; import Image from "@tiptap/extension-image";
import IconButton from '@mui/material/IconButton'; import IconButton from "@mui/material/IconButton";
import FormatBoldIcon from '@mui/icons-material/FormatBold'; import FormatBoldIcon from "@mui/icons-material/FormatBold";
import FormatItalicIcon from '@mui/icons-material/FormatItalic'; import FormatItalicIcon from "@mui/icons-material/FormatItalic";
import StrikethroughSIcon from '@mui/icons-material/StrikethroughS'; import StrikethroughSIcon from "@mui/icons-material/StrikethroughS";
import FormatClearIcon from '@mui/icons-material/FormatClear'; import FormatClearIcon from "@mui/icons-material/FormatClear";
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
import CodeIcon from '@mui/icons-material/Code'; import CodeIcon from "@mui/icons-material/Code";
import ImageIcon from '@mui/icons-material/Image'; // Import Image icon import ImageIcon from "@mui/icons-material/Image"; // Import Image icon
import FormatQuoteIcon from '@mui/icons-material/FormatQuote'; import FormatQuoteIcon from "@mui/icons-material/FormatQuote";
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule";
import UndoIcon from '@mui/icons-material/Undo'; import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from '@mui/icons-material/Redo'; import RedoIcon from "@mui/icons-material/Redo";
import FormatHeadingIcon from '@mui/icons-material/FormatSize'; import FormatHeadingIcon from "@mui/icons-material/FormatSize";
import DeveloperModeIcon from '@mui/icons-material/DeveloperMode'; import DeveloperModeIcon from "@mui/icons-material/DeveloperMode";
import CustomImage from './CustomImage'; import Compressor from "compressorjs";
import Compressor from 'compressorjs'
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension
import { isMobile } from "../../App";
import ImageResize from 'tiptap-extension-resize-image'; // Import the ResizeImage extension
import { isMobile } from '../../App';
const MenuBar = ({ setEditorRef, isChat }) => { const MenuBar = ({ setEditorRef, isChat }) => {
const { editor } = useCurrentEditor(); const { editor } = useCurrentEditor();
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
if (!editor) { if (!editor) {
return null; return null;
} }
@ -39,33 +40,35 @@ const MenuBar = ({ setEditorRef, isChat }) => {
} }
}, [editor, setEditorRef]); }, [editor, setEditorRef]);
const handleImageUpload = async (event) => { const handleImageUpload = async (file) => {
let compressedFile;
const file = event.target.files[0];
let compressedFile
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
new Compressor(file, { new Compressor(file, {
quality: 0.6, quality: 0.6,
maxWidth: 1200, maxWidth: 1200,
mimeType: 'image/webp', mimeType: "image/webp",
success(result) { success(result) {
const file = new File([result], 'name', { compressedFile = new File([result], "image.webp", {
type: 'image/webp' type: "image/webp",
}) });
compressedFile = file resolve();
resolve()
}, },
error(err) {} error(err) {
}) console.error("Image compression error:", err);
}) },
});
});
if (compressedFile) { if (compressedFile) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const url = reader.result; const url = reader.result;
editor.chain().focus().setImage({ src: url , style: "width: auto"}).run(); editor
fileInputRef.current.value = ''; .chain()
.focus()
.setImage({ src: url, style: "width: auto" })
.run();
fileInputRef.current.value = "";
}; };
reader.readAsDataURL(compressedFile); reader.readAsDataURL(compressedFile);
} }
@ -75,200 +78,171 @@ const MenuBar = ({ setEditorRef, isChat }) => {
fileInputRef.current.click(); // Trigger the file input click fileInputRef.current.click(); // Trigger the file input click
}; };
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Prevent the default paste behavior
handleImageUpload(file); // Call the image upload function
}
}
}
};
useEffect(() => {
if (editor) {
editor.view.dom.addEventListener("paste", handlePaste);
return () => {
editor.view.dom.removeEventListener("paste", handlePaste);
};
}
}, [editor]);
return ( return (
<div className="control-group"> <div className="control-group">
<div className="button-group"> <div className="button-group">
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
disabled={ disabled={!editor.can().chain().focus().toggleBold().run()}
!editor.can()
.chain()
.focus()
.toggleBold()
.run()
}
// color={editor.isActive('bold') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('bold') ? 'white' : 'gray', color: editor.isActive("bold") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatBoldIcon /> <FormatBoldIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={ disabled={!editor.can().chain().focus().toggleItalic().run()}
!editor.can()
.chain()
.focus()
.toggleItalic()
.run()
}
// color={editor.isActive('italic') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('italic') ? 'white' : 'gray', color: editor.isActive("italic") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatItalicIcon /> <FormatItalicIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={ disabled={!editor.can().chain().focus().toggleStrike().run()}
!editor.can()
.chain()
.focus()
.toggleStrike()
.run()
}
// color={editor.isActive('strike') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('strike') ? 'white' : 'gray', color: editor.isActive("strike") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<StrikethroughSIcon /> <StrikethroughSIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleCode().run()} onClick={() => editor.chain().focus().toggleCode().run()}
disabled={ disabled={!editor.can().chain().focus().toggleCode().run()}
!editor.can()
.chain()
.focus()
.toggleCode()
.run()
}
// color={editor.isActive('code') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('code') ? 'white' : 'gray', color: editor.isActive("code") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<CodeIcon /> <CodeIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().unsetAllMarks().run()} onClick={() => editor.chain().focus().unsetAllMarks().run()}
sx={{ sx={{
color: editor.isActive('bold') || editor.isActive('italic') || editor.isActive('strike') || editor.isActive('code') color:
? 'white' editor.isActive("bold") ||
: 'gray', editor.isActive("italic") ||
padding: isMobile ? '5px' : 'revert', editor.isActive("strike") ||
}} editor.isActive("code")
> ? "white"
<FormatClearIcon /> : "gray",
</IconButton> padding: isMobile ? "5px" : "revert",
}}
>
<FormatClearIcon />
</IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBulletList().run()} onClick={() => editor.chain().focus().toggleBulletList().run()}
// color={editor.isActive('bulletList') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('bulletList') ? 'white' : 'gray', color: editor.isActive("bulletList") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatListBulletedIcon /> <FormatListBulletedIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleOrderedList().run()} onClick={() => editor.chain().focus().toggleOrderedList().run()}
// color={editor.isActive('orderedList') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('orderedList') ? 'white' : 'gray', color: editor.isActive("orderedList") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatListNumberedIcon /> <FormatListNumberedIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
// color={editor.isActive('codeBlock') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('codeBlock') ? 'white' : 'gray', color: editor.isActive("codeBlock") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<DeveloperModeIcon /> <DeveloperModeIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
// color={editor.isActive('blockquote') ? 'white' : 'gray'}
sx={{ sx={{
color: editor.isActive('blockquote') ? 'white' : 'gray', color: editor.isActive("blockquote") ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatQuoteIcon /> <FormatQuoteIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().setHorizontalRule().run()} onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={!editor.can().chain().focus().setHorizontalRule().run()} disabled={!editor.can().chain().focus().setHorizontalRule().run()}
sx={{ sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
color: 'gray', >
padding: isMobile ? '5px' : 'revert', <HorizontalRuleIcon />
}} </IconButton>
>
<HorizontalRuleIcon />
</IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} onClick={() =>
// color={editor.isActive('heading', { level: 1 }) ? 'white' : 'gray'} editor.chain().focus().toggleHeading({ level: 1 }).run()
}
sx={{ sx={{
color: editor.isActive('heading', { level: 1 }) ? 'white' : 'gray', color: editor.isActive("heading", { level: 1 }) ? "white" : "gray",
padding: isMobile ? '5px' : 'revert' padding: isMobile ? "5px" : "revert",
}} }}
> >
<FormatHeadingIcon fontSize="small" /> <FormatHeadingIcon fontSize="small" />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().undo().run()} onClick={() => editor.chain().focus().undo().run()}
disabled={ disabled={!editor.can().chain().focus().undo().run()}
!editor.can() sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
.chain()
.focus()
.undo()
.run()
}
sx={{
color: 'gray',
padding: isMobile ? '5px' : 'revert'
}}
> >
<UndoIcon /> <UndoIcon />
</IconButton> </IconButton>
<IconButton <IconButton
sx={{
color: 'gray'
}}
onClick={() => editor.chain().focus().redo().run()} onClick={() => editor.chain().focus().redo().run()}
disabled={ disabled={!editor.can().chain().focus().redo().run()}
!editor.can() sx={{ color: "gray" }}
.chain()
.focus()
.redo()
.run()
}
> >
<RedoIcon /> <RedoIcon />
</IconButton> </IconButton>
{!isChat && ( {!isChat && (
<> <>
<IconButton <IconButton
onClick={triggerImageUpload} onClick={triggerImageUpload}
sx={{ sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
color: 'gray', >
padding: isMobile ? '5px' : 'revert' <ImageIcon />
}} </IconButton>
> <input
<ImageIcon /> type="file"
</IconButton> ref={fileInputRef}
<input style={{ display: "none" }}
type="file" onChange={(event) => handleImageUpload(event.target.files[0])}
ref={fileInputRef} accept="image/*"
style={{ display: 'none' }} />
onChange={handleImageUpload}
accept="image/*" // Limit file types to images only
/>
</> </>
)} )}
</div> </div>
</div> </div>
); );
@ -288,73 +262,91 @@ const extensions = [
}, },
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder: 'Start typing here...', // Add your placeholder text here placeholder: "Start typing here...",
}), }),
ImageResize, ImageResize,
]; ];
const content = ``; const content = ``;
export default ({ setEditorRef, onEnter, disableEnter, isChat, maxHeightOffset, setIsFocusedParent, isFocusedParent, overrideMobile, customEditorHeight }) => { export default ({
setEditorRef,
onEnter,
disableEnter,
isChat,
maxHeightOffset,
setIsFocusedParent,
isFocusedParent,
overrideMobile,
customEditorHeight,
}) => {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const extensionsFiltered = isChat ? extensions.filter((item)=> item?.name !== 'image') : extensions const extensionsFiltered = isChat
? extensions.filter((item) => item?.name !== "image")
: extensions;
const editorRef = useRef(null); const editorRef = useRef(null);
const setEditorRefFunc = (editorInstance) => { const setEditorRefFunc = (editorInstance) => {
editorRef.current = editorInstance; editorRef.current = editorInstance;
setEditorRef(editorInstance) setEditorRef(editorInstance);
}; };
const handleFocus = () => { const handleFocus = () => {
if(!isMobile) return if (!isMobile) return;
// setIsFocused(true); setIsFocusedParent(true);
setIsFocusedParent(true)
}; };
const handleBlur = () => { const handleBlur = () => {
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") {
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>"){ // Set focus state based on content
// setIsFocused(false); }
// setIsFocusedParent(false)
};
}; };
// useEffect(()=> {
// setIsFocused(isFocusedParent)
// },[isFocusedParent])
return ( return (
<EditorProvider <EditorProvider
slotBefore={(isFocusedParent || !isMobile || overrideMobile) && <MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />} slotBefore={
extensions={extensionsFiltered} (isFocusedParent || !isMobile || overrideMobile) && (
content={content} <MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
onCreate={({ editor }) => { )
editor.on('focus', handleFocus); // Listen for focus event }
editor.on('blur', handleBlur); // Listen for blur event extensions={extensionsFiltered}
}} content={content}
onUpdate={({ editor }) => { onCreate={({ editor }) => {
editor.on("focus", handleFocus); // Listen for focus event
editor.on("blur", handleBlur); // Listen for blur event
}}
onUpdate={({ editor }) => {
editor.on('focus', handleFocus); // Ensure focus is updated editor.on('focus', handleFocus); // Ensure focus is updated
editor.on('blur', handleBlur); // Ensure blur is updated editor.on('blur', handleBlur); // Ensure blur is updated
}} }}
editorProps={{ editorProps={{
attributes: { attributes: {
class: 'tiptap-prosemirror', class: "tiptap-prosemirror",
style: isMobile && `overflow: auto; min-height: ${customEditorHeight ? '200px' : '0px'}; 200px; max-height:calc(100svh - ${ customEditorHeight ? customEditorHeight : '140px'})`, style:
}, isMobile &&
handleKeyDown(view, event) { `overflow: auto; min-height: ${
if (!disableEnter && event.key === 'Enter') { customEditorHeight ? "200px" : "0px"
if (event.shiftKey) { }; max-height:calc(100svh - ${customEditorHeight || "140px"})`,
view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.hardBreak.create())); },
return true; handleKeyDown(view, event) {
} else { if (!disableEnter && event.key === "Enter") {
if (typeof onEnter === 'function') { if (event.shiftKey) {
onEnter(); view.dispatch(
view.state.tr.replaceSelectionWith(
view.state.schema.nodes.hardBreak.create()
)
);
return true;
} else {
if (typeof onEnter === "function") {
onEnter();
}
return true;
} }
return true;
} }
} return false;
return false; },
}, }}
}} />
/> );
)
}; };

View File

@ -53,7 +53,7 @@ import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg";
import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar";
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events";
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { getArbitraryEndpointReact, getBaseApiReact } from "../../../App"; import { getArbitraryEndpointReact, getBaseApiReact, isMobile } from "../../../App";
import { WrapperUserAction } from "../../WrapperUserAction"; import { WrapperUserAction } from "../../WrapperUserAction";
import { addDataPublishesFunc, getDataPublishesFunc } from "../Group"; import { addDataPublishesFunc, getDataPublishesFunc } from "../Group";
const filterOptions = ["Recently active", "Newest", "Oldest"]; const filterOptions = ["Recently active", "Newest", "Oldest"];
@ -105,12 +105,15 @@ export const GroupMail = ({
const setTempData = async ()=> { const setTempData = async ()=> {
try { try {
const getTempAnnouncements = await getTempPublish() const getTempAnnouncements = await getTempPublish()
if(getTempAnnouncements?.thread){ if(getTempAnnouncements?.thread){
let tempData = [] let tempData = []
Object.keys(getTempAnnouncements?.thread || {}).map((key)=> { Object.keys(getTempAnnouncements?.thread || {}).map((key)=> {
const value = getTempAnnouncements?.thread[key] const value = getTempAnnouncements?.thread[key]
tempData.push(value.data) if(value?.data?.groupId === groupIdRef?.current){
tempData.push(value.data)
}
}) })
setTempPublishedList(tempData) setTempPublishedList(tempData)
} }
@ -720,15 +723,18 @@ export const GroupMail = ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
width: '100%'
}} }}
> >
<ThreadSingleTitle <ThreadSingleTitle
sx={{ sx={{
fontWeight: shouldAppearLighter && 300, fontWeight: shouldAppearLighter && 300,
fontSize: isMobile && '18px'
}} }}
> >
{thread?.threadData?.title} {thread?.threadData?.title}
</ThreadSingleTitle> </ThreadSingleTitle>
<Spacer height="10px" />
{filterMode === "Recently active" && ( {filterMode === "Recently active" && (
<div <div
style={{ style={{

View File

@ -172,7 +172,10 @@ export const NewThread = ({
const closeModal = () => { const closeModal = () => {
setIsOpen(false); setIsOpen(false);
setValue(""); setValue("");
setPostReply(null) if(setPostReply){
setPostReply(null)
}
}; };
async function publishQDNResource() { async function publishQDNResource() {
@ -288,6 +291,7 @@ export const NewThread = ({
service: 'DOCUMENT', service: 'DOCUMENT',
tempData: threadObject, tempData: threadObject,
created: Date.now(), created: Date.now(),
groupId: groupInfo.groupId
} }
const dataToSaveToStoragePost = { const dataToSaveToStoragePost = {
name: myName, name: myName,
@ -313,15 +317,7 @@ export const NewThread = ({
// ); // );
if (publishCallback) { if (publishCallback) {
publishCallback() publishCallback()
// threadCallback({
// threadData: threadObject,
// threadOwner: name,
// name,
// threadId: identifierThread,
// created: Date.now(),
// service: 'MAIL_PRIVATE',
// identifier: identifier
// })
} }
closeModal(); closeModal();
} else { } else {

View File

@ -44,13 +44,28 @@ export const ReactionPicker = ({ onReaction }) => {
{/* Emoji CTA */} {/* Emoji CTA */}
<ButtonBase sx={{ <ButtonBase sx={{
fontSize: '22px' fontSize: '22px'
}} onClick={() => setShowPicker(!showPicker)}> }}
// onTouchStart={(e) => {
// 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);
}}
>
😃 😃
</ButtonBase> </ButtonBase>
{/* Emoji Picker with dark theme */} {/* Emoji Picker with dark theme */}
{showPicker && ( {showPicker && (
<div className="emoji-picker" ref={pickerRef}> <div className="emoji-picker" ref={pickerRef} onClick={(e) => e.preventDefault()}>
<Picker <Picker
height={isMobile ? 300 : 450} height={isMobile ? 300 : 450}
width={isMobile ? 250 : 350 } width={isMobile ? 250 : 350 }
@ -58,6 +73,7 @@ export const ReactionPicker = ({ onReaction }) => {
onReactionClick={handleReaction} onReactionClick={handleReaction}
onEmojiClick={handlePicker} onEmojiClick={handlePicker}
allowExpandReactions={true} allowExpandReactions={true}
autoFocusSearch={false}
theme={Theme.DARK} theme={Theme.DARK}
/> />
</div> </div>