refactor: markdown UX

This commit is contained in:
zerosoul
2022-03-28 20:47:17 +08:00
parent 774bf2c335
commit ffb53e14d4
30 changed files with 1485 additions and 574 deletions
+4
View File
@@ -1,5 +1,7 @@
// import React from 'react'
// import { NimblePicker } from "emoji-mart";
import { Picker } from "emoji-mart";
// import data from "emoji-mart/data/";
import "emoji-mart/css/emoji-mart.css";
import styled from "styled-components";
const StyledWrapper = styled.div`
@@ -14,6 +16,8 @@ export default function EmojiPicker({ onSelect, ...rest }) {
return (
<StyledWrapper>
<Picker
// set="twitter"
// data={data}
// set="twitter"
showPreview={false}
showSkinTones={false}
@@ -1,74 +0,0 @@
import React from "react";
import { cx, css } from "@emotion/css";
export const Button = React.forwardRef(
({ className, active, reversed, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
className,
css`
cursor: pointer;
color: ${reversed
? active
? "white"
: "#aaa"
: active
? "black"
: "#ccc"};
`
)}
/>
)
);
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
"material-icons",
className,
css`
font-size: 18px;
vertical-align: text-bottom;
`
)}
/>
));
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
<div
{...props}
ref={ref}
className={cx(
className,
css`
& > * {
display: inline-block;
}
& > * + * {
margin-left: 15px;
}
`
)}
/>
));
export const Toolbar = React.forwardRef(({ className, ...props }, ref) => (
<Menu
{...props}
ref={ref}
className={cx(
className,
css`
position: relative;
padding: 5px 16px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
margin-bottom: 20px;
`
)}
/>
));
@@ -1,31 +0,0 @@
import Editor from "rich-markdown-editor";
import styled from "styled-components";
const Styled = styled.div`
padding-top: 8px;
.mde {
padding: 4px;
height: 150px;
> div {
height: 100%;
.ProseMirror {
overflow: scroll;
height: 100%;
max-height: 300px;
padding: 5px 28px;
}
}
}
`;
export default function MarkdownEditer({ value, updateValue }) {
return (
<Styled>
<Editor
defaultValue={value}
onChange={updateValue}
autoFocus={true}
placeholder="Type '/' to insert..."
className="mde"
></Editor>
</Styled>
);
}
@@ -1,280 +0,0 @@
import { useCallback, useMemo, useState } from "react";
import isHotkey from "is-hotkey";
import { Editable, withReact, useSlate, Slate } from "slate-react";
import {
Editor,
Transforms,
createEditor,
Element as SlateElement,
} from "slate";
import { withHistory } from "slate-history";
import Styled from "./styled";
import { Button, Icon, Toolbar } from "./components";
const HOTKEYS = {
"mod+b": "bold",
"mod+i": "italic",
"mod+u": "underline",
"mod+`": "code",
};
const LIST_TYPES = ["numbered-list", "bulleted-list"];
const TEXT_ALIGN_TYPES = ["left", "center", "right", "justify"];
const MarkdownEditor = () => {
const [value, setValue] = useState(initialValue);
const renderElement = useCallback((props) => <Element {...props} />, []);
const renderLeaf = useCallback((props) => <Leaf {...props} />, []);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
return (
<Styled>
<Slate
editor={editor}
value={value}
onChange={(value) => setValue(value)}
>
<Toolbar>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
{/* <BlockButton format="heading-one" icon="looks_one" />
<BlockButton format="heading-two" icon="looks_two" /> */}
<BlockButton format="block-quote" icon="format_quote" />
<BlockButton format="numbered-list" icon="format_list_numbered" />
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
<BlockButton format="left" icon="format_align_left" />
<BlockButton format="center" icon="format_align_center" />
<BlockButton format="right" icon="format_align_right" />
<BlockButton format="justify" icon="format_align_justify" />
</Toolbar>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={(event) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event)) {
event.preventDefault();
const mark = HOTKEYS[hotkey];
toggleMark(editor, mark);
}
}
}}
/>
</Slate>
</Styled>
);
};
const toggleBlock = (editor, format) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
);
const isList = LIST_TYPES.includes(format);
Transforms.unwrapNodes(editor, {
match: (n) =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
split: true,
});
let newProperties;
if (TEXT_ALIGN_TYPES.includes(format)) {
newProperties = {
align: isActive ? undefined : format,
};
} else {
newProperties = {
type: isActive ? "paragraph" : isList ? "list-item" : format,
};
}
Transforms.setNodes < SlateElement > (editor, newProperties);
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
};
const isBlockActive = (editor, format, blockType = "type") => {
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: (n) =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
})
);
return !!match;
};
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
const Element = ({ attributes, children, element }) => {
const style = { textAlign: element.align };
switch (element.type) {
case "block-quote":
return (
<blockquote style={style} {...attributes}>
{children}
</blockquote>
);
case "bulleted-list":
return (
<ul style={style} {...attributes}>
{children}
</ul>
);
case "heading-one":
return (
<h1 style={style} {...attributes}>
{children}
</h1>
);
case "heading-two":
return (
<h2 style={style} {...attributes}>
{children}
</h2>
);
case "list-item":
return (
<li style={style} {...attributes}>
{children}
</li>
);
case "numbered-list":
return (
<ol style={style} {...attributes}>
{children}
</ol>
);
default:
return (
<p style={style} {...attributes}>
{children}
</p>
);
}
};
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
if (leaf.code) {
children = <code>{children}</code>;
}
if (leaf.italic) {
children = <em>{children}</em>;
}
if (leaf.underline) {
children = <u>{children}</u>;
}
return <span {...attributes}>{children}</span>;
};
const BlockButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
)}
onMouseDown={(event) => {
event.preventDefault();
toggleBlock(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
const MarkButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleMark(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
const initialValue = [
{
type: "paragraph",
children: [
{ text: "This is editable " },
{ text: "rich", bold: true },
{ text: " text, " },
{ text: "much", italic: true },
{ text: " better than a " },
{ text: "<textarea>", code: true },
{ text: "!" },
],
},
{
type: "paragraph",
children: [
{
text:
"Since it's rich text, you can do things like turn a selection of text ",
},
{ text: "bold", bold: true },
{
text:
", or add a semantically rendered block quote in the middle of the page, like this:",
},
],
},
{
type: "block-quote",
children: [{ text: "A wise quote." }],
},
{
type: "paragraph",
align: "center",
children: [{ text: "Try it out for yourself!" }],
},
];
export default MarkdownEditor;
@@ -1,62 +0,0 @@
import styled from "styled-components";
const Styled = styled.div`
* {
line-height: 1.4;
}
/* ul,
ol {
list-style-position: inside;
display: list-item;
padding-left: 10px;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
} */
strong {
font-weight: bold;
}
p {
margin: 0;
margin-bottom: 12px;
}
pre {
padding: 10px;
background-color: #eee;
white-space: pre-wrap;
}
:not(pre) > code {
font-family: monospace;
background-color: #eee;
padding: 3px;
}
code {
background-color: #ccc;
}
img {
max-width: 100%;
max-height: 20em;
}
blockquote {
border-left: 2px solid #ddd;
margin-left: 0;
margin-right: 0;
padding-left: 10px;
color: #aaa;
font-style: italic;
}
blockquote[dir="rtl"] {
border-left: none;
padding-left: 0;
padding-right: 10px;
border-right: 2px solid #ddd;
}
`;
export default Styled;
@@ -0,0 +1,88 @@
import { useRef, useEffect } from "react";
import "@toast-ui/editor/dist/toastui-editor.css";
import { Editor } from "@toast-ui/react-editor";
import { useUploadImageMutation } from "../../../app/services/message";
import styled from "styled-components";
import Button from "../../component/styled/Button";
const StyledWrapper = styled.div`
position: relative;
width: 100%;
width: -webkit-fill-available;
margin-top: 16px;
.toastui-editor-defaultUI {
border-bottom: none;
border-radius: 0;
border-top: 1px solid #d0d5dd;
border-left: none;
border-right: none;
}
.toastui-editor {
padding: 16px 0;
[contenteditable="true"] {
padding: 0;
}
}
.toastui-editor-md-preview {
padding-top: 16px;
.toastui-editor-contents {
padding: 0;
}
}
.toastui-editor-toolbar {
display: none;
}
.send {
position: absolute;
bottom: 15px;
right: 15px;
}
`;
export default function MarkdownEditor({ placeholder, sendMarkdown }) {
const editorRef = useRef(null);
const [uploadImage] = useUploadImageMutation();
// const handleChange = (wtf) => {
// const edtr = editorRef.current.getInstance();
// console.log("md", wtf, edtr.getMarkdown());
// updateMarkdown(edtr.getMarkdown());
// };
useEffect(() => {
if (editorRef) {
const editorInstance = editorRef.current.getInstance();
editorInstance.removeHook("addImageBlobHook");
editorInstance.addHook("addImageBlobHook", async (blob, callback) => {
const { data: url } = await uploadImage(blob);
callback(url);
});
}
}, []);
const send = () => {
const edtr = editorRef.current.getInstance();
const md = edtr.getMarkdown().trim();
if (md) {
sendMarkdown(edtr.getMarkdown());
edtr.reset();
}
};
return (
<StyledWrapper className="input">
<Editor
placeholder={placeholder}
// onChange={handleChange}
ref={editorRef}
toolbarItems={[]}
hideModeSwitch={true}
// initialValue="hello world!"
previewStyle="vertical"
height="50vh"
initialEditType="markdown"
useCommandShortcut={true}
/>
<Button className="send small" onClick={send}>
Send
</Button>
</StyledWrapper>
);
}
@@ -0,0 +1,54 @@
import "tippy.js/animations/scale.css";
import "tippy.js/dist/tippy.css";
import IconQuestion from "../../../../assets/icons/question.svg";
import {
BalloonToolbar,
getPluginType,
MARK_BOLD,
MARK_ITALIC,
MARK_UNDERLINE,
MarkToolbarButton,
usePlateEditorRef,
} from "@udecode/plate";
const MarkBallonToolbar = () => {
const editor = usePlateEditorRef();
const arrow = false;
const theme = "dark";
const tooltip = {
arrow: true,
delay: 0,
duration: [200, 0],
hideOnClick: false,
offset: [0, 17],
placement: "top",
};
return (
<BalloonToolbar
popperOptions={{
placement: "top",
}}
theme={theme}
arrow={arrow}
>
<MarkToolbarButton
type={getPluginType(editor, MARK_BOLD)}
icon={<IconQuestion />}
tooltip={{ content: "Bold (⌘B)", ...tooltip }}
/>
<MarkToolbarButton
type={getPluginType(editor, MARK_ITALIC)}
icon={<IconQuestion />}
tooltip={{ content: "Italic (⌘I)", ...tooltip }}
/>
<MarkToolbarButton
type={getPluginType(editor, MARK_UNDERLINE)}
icon={<IconQuestion />}
tooltip={{ content: "Underline (⌘U)", ...tooltip }}
/>
</BalloonToolbar>
);
};
export default MarkBallonToolbar;
@@ -0,0 +1,19 @@
import {
ELEMENT_H1,
ELEMENT_PARAGRAPH,
withPlaceholders,
} from "@udecode/plate";
export const withStyledPlaceHolders = (components) =>
withPlaceholders(components, [
{
key: ELEMENT_PARAGRAPH,
placeholder: "Type a paragraph",
hideOnBlur: true,
},
{
key: ELEMENT_H1,
placeholder: "Untitled",
hideOnBlur: false,
},
]);
@@ -0,0 +1,116 @@
import {
// AutoformatPlugin,
CodeBlockElement,
createPlateUI,
ELEMENT_BLOCKQUOTE,
ELEMENT_CODE_BLOCK,
ELEMENT_H1,
ELEMENT_H2,
ELEMENT_H3,
ELEMENT_H4,
ELEMENT_H5,
ELEMENT_H6,
ELEMENT_HR,
ELEMENT_IMAGE,
ELEMENT_PARAGRAPH,
ELEMENT_TODO_LI,
// ExitBreakPlugin,
// IndentPlugin,
isBlockAboveEmpty,
isSelectionAtBlockStart,
// NormalizeTypesPlugin,
// PlatePlugin,
// ResetNodePlugin,
// SelectOnBackspacePlugin,
// SoftBreakPlugin,
// TrailingBlockPlugin,
withProps,
} from "@udecode/plate";
// import { EditableProps } from 'slate-react/dist/components/editable'
import { css } from "styled-components";
const resetBlockTypesCommonRule = {
types: [ELEMENT_BLOCKQUOTE, ELEMENT_TODO_LI],
defaultType: ELEMENT_PARAGRAPH,
};
export const CONFIG = {
editableProps: {
spellCheck: false,
autoFocus: true,
placeholder: "Type…",
},
components: createPlateUI({
[ELEMENT_CODE_BLOCK]: withProps(CodeBlockElement, {
styles: {
root: [
css`
background-color: #111827;
code {
color: white;
}
`,
],
},
}),
}),
resetBlockType: {
options: {
rules: [
{
...resetBlockTypesCommonRule,
hotkey: "Enter",
predicate: isBlockAboveEmpty,
},
{
...resetBlockTypesCommonRule,
hotkey: "Backspace",
predicate: isSelectionAtBlockStart,
},
],
},
},
trailingBlock: { type: ELEMENT_PARAGRAPH },
softBreak: {
options: {
rules: [
// { hotkey: "shift+enter" },
// {
// hotkey: "enter",
// query: {
// allow: [ELEMENT_CODE_BLOCK, ELEMENT_BLOCKQUOTE, ELEMENT_TD],
// },
// },
],
},
},
exitBreak: {
options: {
rules: [
{
hotkey: "mod+enter",
},
// {
// hotkey: "mod+shift+enter",
// before: true,
// },
// {
// hotkey: "enter",
// query: {
// start: true,
// end: true,
// allow: KEYS_HEADING,
// },
// },
],
},
},
selectOnBackspace: {
options: {
query: {
allow: [ELEMENT_IMAGE, ELEMENT_HR],
},
},
},
};
+122
View File
@@ -0,0 +1,122 @@
import { useState, useRef } from "react";
// import { serialize } from "remark-slate";
import { useKey } from "rooks";
import { useUploadImageMutation } from "../../../app/services/message";
// import nodeTypes from "./values/nodeTypes";
import { PLUGINS } from "./plugins";
import {
createPlateUI,
// HeadingToolbar,
// MentionCombobox,
Plate,
createExitBreakPlugin,
// createMediaEmbedPlugin,
createNodeIdPlugin,
createParagraphPlugin,
createResetNodePlugin,
createSelectOnBackspacePlugin,
createSoftBreakPlugin,
// createDndPlugin,
createTrailingBlockPlugin,
// createComboboxPlugin,
// createMentionPlugin,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateActions,
platesActions,
eventEditorStore,
// usePlateStore
// usePlateStore
} from "@udecode/plate";
import Styled from "./styled";
import { CONFIG } from "./config";
// import { VALUES } from "./values/values";
const id = "rustchat_rich_editor";
let components = createPlateUI({
// customize your components by plugin key
});
// components = withStyledPlaceHolders(components);
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
const Plugins = ({ placeholder = "Write some markdown...", sendMessage }) => {
const editableRef = useRef(null);
const [uploadImage] = useUploadImageMutation();
useKey(
"Enter",
(evt) => {
console.log("enter keypress", evt);
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
return true;
}
evt.preventDefault();
sendMessage();
platesActions.unset(id);
platesActions.set(id, {
initialValue,
});
// eventEditorStore.set()
getPlateActions(id).enabled(false);
getPlateActions(id).enabled(true);
// eventEditorStore.get();
// getPlateActions(id).set;
// editor.reset
},
{
// eventTypes: ["keydown"],
target: editableRef,
// when: !shift,
}
);
// const plugins = createPlugins(
// [
// createParagraphPlugin(),
// // createImagePlugin({
// // options: {
// // uploadImage: (imageData) => {
// // return new Promise((resolve, reject) => {
// // fetch(imageData)
// // .then((res) => res.blob())
// // .then((blob) => {
// // uploadImage(blob).then(({ data }) => {
// // console.log("upload image ", data);
// // resolve(data);
// // });
// // });
// // });
// // },
// // },
// // }),
// createNodeIdPlugin(),
// createResetNodePlugin(CONFIG.resetBlockType),
// // createSoftBreakPlugin(CONFIG.softBreak),
// createExitBreakPlugin(CONFIG.exitBreak),
// createTrailingBlockPlugin(CONFIG.trailingBlock),
// createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
// ],
// {
// components,
// }
// );
const handleChange = (val) => {
console.log("changed", val);
};
return (
<Styled className="input" ref={editableRef}>
<Plate
onChange={handleChange}
id={id}
editableProps={{
...CONFIG.editableProps,
placeholder,
}}
initialValue={initialValue}
plugins={PLUGINS}
></Plate>
</Styled>
);
};
export default Plugins;
@@ -0,0 +1,44 @@
import {
createBasicElementsPlugin,
createBlockquotePlugin,
createBoldPlugin,
createCodeBlockPlugin,
createCodePlugin,
createHeadingPlugin,
createImagePlugin,
createItalicPlugin,
createParagraphPlugin,
createSelectOnBackspacePlugin,
createStrikethroughPlugin,
createUnderlinePlugin,
createPreviewPlugin,
} from "@udecode/plate";
import { CONFIG } from "./config";
const basicElements = [
createParagraphPlugin(), // paragraph element
createBlockquotePlugin(), // blockquote element
createCodeBlockPlugin(), // code block element
createHeadingPlugin(), // heading elements
];
const basicMarks = [
createBoldPlugin(), // bold mark
createItalicPlugin(), // italic mark
createUnderlinePlugin(), // underline mark
createStrikethroughPlugin(), // strikethrough mark
createCodePlugin(), // code mark
];
export const PLUGINS = {
basicElements,
basicMarks,
basicNodes: [...basicElements, ...basicMarks],
createPreviewPlugin,
image: [
createBasicElementsPlugin(),
...basicMarks,
createImagePlugin(),
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
],
};
@@ -0,0 +1,13 @@
import styled from "styled-components";
const Styled = styled.div`
max-height: 50vh;
overflow: auto;
p {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #475467;
}
`;
export default Styled;
@@ -0,0 +1,24 @@
const nodeTypes = {
heading: {
1: "h1",
2: "h2",
3: "h3",
4: "h4",
5: "h5",
6: "h6",
},
paragraph: "p",
link: "a",
block_quote: "blockquote",
ol_list: "ol",
ul_list: "ul",
listItem: "li",
strong_mark: "bold",
inline_code_mark: "code",
emphasis_mark: "italic",
delete_mark: "strikethrough",
image: "img",
code_block: "code_block",
};
export default nodeTypes;
@@ -0,0 +1,63 @@
import {
ELEMENT_LI,
ELEMENT_LIC,
ELEMENT_PARAGRAPH,
ELEMENT_UL,
// TElement,
} from "@udecode/plate";
// import { Text } from 'slate'
export const createElement = (
text = "",
{ type = ELEMENT_PARAGRAPH, mark } = {}
) => {
const leaf = { text };
if (mark) {
leaf[mark] = true;
}
return {
type,
children: [leaf],
};
};
export const createList = (items = [], { splitSeparator = "`" } = {}) => {
const children = items.map((item) => {
const texts = item.split(splitSeparator);
const marks = texts.map((text, index) => {
const res = { text };
if (index % 2 === 1) {
res.code = true;
}
return res;
});
return {
type: ELEMENT_LI,
children: [
{
type: ELEMENT_LIC,
children: marks,
},
],
};
});
return [
{
type: ELEMENT_UL,
children,
},
];
};
export const getNodesWithRandomId = (nodes = []) => {
let _id = 10000;
nodes.forEach((node) => {
node.id = _id;
_id++;
});
return nodes;
};
@@ -0,0 +1,275 @@
import { ELEMENT_HR } from "@udecode/plate";
// import { jsx } from "@udecode/plate-test-utils";
import { createList, getNodesWithRandomId } from "./utils";
const align = (
<>
<hh1 align="right">Alignment</hh1>
<hp align="right">This block text is aligned to the right.</hp>
<hh2 align="center">Center</hh2>
<hp align="justify">This block text is justified.</hp>
</>
);
const indent = (
<>
<hh1>Changing block indentation</hh1>
<hp indent={1}>
Use the toolbar buttons to control the indentation of specific blocks. You
can use these tools to highlight an important piece of information,
communicate a hierarchy or just give your content some room.
</hp>
<hp indent={2}>
For instance, this paragraph looks like it belongs to the previous one.
</hp>
</>
);
const horizontalRule = (
<>
<hp>This is a paragraph.</hp>
<element type={ELEMENT_HR}>
<htext />
</element>
<hp>And this is another paragraph.</hp>
<element type={ELEMENT_HR}>
<htext />
</element>
<hp>But between those paragraphs are horizontal rules.</hp>
</>
);
const mediaEmbed = (
<>
<hh2>🎥 Media Embed</hh2>
<hp>
In addition to simple image nodes, you can actually create complex
embedded nodes. For example, this one contains an input element that lets
you change the video being rendered!
</hp>
<hmediaembed url="https://player.vimeo.com/video/26689853">
<htext />
</hmediaembed>
<hp>
Try it out! This editor is built to handle Vimeo embeds, but you could
handle any type.
</hp>
</>
);
const image = (
<>
<hh2>📷 Image</hh2>
<hp>
In addition to nodes that contain editable text, you can also create other
types of nodes, like images or videos.
</hp>
<himg url="https://source.unsplash.com/kFrdX5IeQzI" width="75%">
<htext />
</himg>
<hp>
This example shows images in action. It features two ways to add images.
You can either add an image via the toolbar icon above, or if you want in
on a little secret, copy an image URL to your keyboard and paste it
anywhere in the editor! Additionally, you can customize the toolbar button
to load an url asynchronously, for example showing a file picker and
uploading a file to Amazon S3. You can also add a caption and resize the
image.
</hp>
</>
);
const link = (
<>
<hh2>🔗 Link</hh2>
<hp>
In addition to block nodes, you can create inline nodes, like{" "}
<ha url="https://en.wikipedia.org/wiki/Hypertext">hyperlinks</ha>!
</hp>
<hp>
This example shows hyperlinks in action. It features two ways to add
links. You can either add a link via the toolbar icon above, or if you
want in on a little secret, copy a URL to your keyboard and paste it while
a range of text is selected.
</hp>
</>
);
const autoformat = (
<>
<hh1>🏃 Autoformat</hh1>
<hp>
The editor gives you full control over the logic you can add. For example,
it's fairly common to want to add markdown-like shortcuts to editors.
</hp>
<hp>While typing, try these (mark rules):</hp>
<hul>
<hli>
<hlic>
Type <htext code>**</htext> or <htext code>__</htext> on either side
of your text to add **bold* mark.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>*</htext> or <htext code>_</htext> on either side of
your text to add *italic mark.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>`</htext> on either side of your text to add `inline
code mark.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>~~</htext> on either side of your text to add
~~strikethrough~ mark.
</hlic>
</hli>
<hli>
<hlic>
Note that nothing happens when there is a character before, try
on:*bold
</hlic>
</hli>
<hli>
<hlic>
We even support smart quotes, try typing{" "}
<htext code>"hello" 'world'</htext>.
</hlic>
</hli>
</hul>
<hp>
At the beginning of any new block or existing block, try these (block
rules):
</hp>
<hul>
<hli>
<hlic>
Type <htext code>*</htext>, <htext code>-</htext> or{" "}
<htext code>+</htext> followed by <htext code>space</htext> to create
a bulleted list.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>1.</htext> or <htext code>1)</htext> followed by{" "}
<htext code>space</htext> to create a numbered list.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>&gt;</htext> followed by <htext code>space</htext> to
create a block quote.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>```</htext> to create a code block.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>---</htext> to create a horizontal rule.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>#</htext> followed by <htext code>space</htext> to
create an H1 heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>##</htext> followed by <htext code>space</htext> to
create an H2 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>###</htext> followed by <htext code>space</htext> to
create an H3 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>####</htext> followed by <htext code>space</htext> to
create an H4 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>#####</htext> followed by <htext code>space</htext>{" "}
to create an H5 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>######</htext> followed by <htext code>space</htext>{" "}
to create an H6 sub-heading.
</hlic>
</hli>
</hul>
</>
);
const pasteHtml = (
<>
<hh1>🍪 Deserialize HTML</hh1>
<hp>
By default, pasting content into a Slate editor will use the clipboard's{" "}
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
sometimes you want users to be able to paste in content and have it
maintain its formatting. To do this, your editor needs to handle{" "}
<htext code>'text/html'</htext> data.
</hp>
<hp>This is an example of doing exactly that!</hp>
<hp>
Try it out for yourself! Copy and paste some rendered HTML rich text
content (not the source code) from another site into this editor and it's
formatting should be preserved.
</hp>
</>
);
const pasteMd = (
<>
<hh1>🍩 Deserialize Markdown</hh1>
<hp>
By default, pasting content into a Slate editor will use the clipboard's{" "}
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
sometimes you want users to be able to paste in content and have it
maintain its formatting. To do this, your editor needs to handle{" "}
<htext code>'text/html'</htext> data.
</hp>
<hp>This is an example of doing exactly that!</hp>
<hp>
Try it out for yourself! Copy and paste Markdown content from{" "}
<ha url="https://markdown-it.github.io/">
https://markdown-it.github.io/
</ha>
</hp>
</>
);
const basicMarks = [
<hp key="d">
The basic marks consist of text formatting such as bold, italic, underline,
strikethrough, subscript, superscript, and code.
</hp>,
];
const basicElements = [
<hblockquote key="3">Blockquote</hblockquote>,
<hcodeblock key="4" lang="javascript">
<hcodeline>const a = 'Hello';</hcodeline>
<hcodeline>const b = 'World';</hcodeline>
</hcodeblock>,
];
const playground = [...basicMarks, ...basicElements];
export const VALUES = {
playground,
};
@@ -0,0 +1,94 @@
// import React from 'react'
import { createGlobalStyle } from "styled-components";
const MarkdownOverrides = createGlobalStyle`
[class^='toastui-editor-']{
.toastui-editor-md-container{
border-bottom: none;
.toastui-editor-md-splitter{
background-color:#D0D5DD ;
}
}
*{
/* white-space: nowrap; */
margin: 0 ;
padding: 0;
}
p {
margin:0 ;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #475467;
margin-bottom: 16px;
}
a{
background-color: transparent;
}
blockquote {
border-left:none;
display: flex;
margin-top:0;
margin-bottom: 10px;
padding: 16px;
color: #98a2b3;
opacity: 0.8;
box-shadow: inset 2px 0px 0px #a8b0bd;
> p{
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
}
img{
cursor: pointer;
max-width:300px;
}
ul {
white-space: nowrap;
margin-top:0;
margin-bottom:10px;
/* display: flex;
flex-direction:column;
margin-left: 20px; */
li:before{
margin-top: 8px;
margin-left: -14px;
background-color: #475467;
}
/* list-style-type: disc; */
}
ul,
ol {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #475467;
/* list-style-position: inside; */
}
h1,
h2,
h3,[class*='heading']{
padding: 0;
margin: 0;
border-bottom: none;
font-weight: 700;
color: #475467;
}
h1,[class*='heading1'] {
font-size: 24px;
line-height: 32px;
}
h2,[class*='heading2'] {
font-size: 20px;
line-height: 30px;
}
h3,[class*='heading3'] {
font-size: 16px;
line-height: 24px;
}
}
`;
export default MarkdownOverrides;
@@ -0,0 +1,40 @@
// import React from 'react'
import { useEditorRef } from "@udecode/plate";
import { Transforms } from "slate";
import { ReactEditor } from "slate-react";
import styled from "styled-components";
import iconClose from "../../../assets/icons/close.circle.svg?url";
const StylePicture = styled.picture`
position: relative;
display: flex;
width: fit-content;
max-width: 240px;
max-height: 240px;
img {
width: 100%;
object-fit: cover;
}
.remove {
background: none;
position: absolute;
top: 0;
right: -20px;
}
`;
export default function ImageElement({ attributes, children, element }) {
const editor = useEditorRef();
const path = ReactEditor.findPath(editor, element);
const handleRemoveImage = () => {
Transforms.removeNodes(editor, { at: path });
};
return (
<StylePicture {...attributes}>
{children}
<img src={element.url}></img>
<button className="remove" onClick={handleRemoveImage}>
<img src={iconClose} alt="icon close" />
</button>
</StylePicture>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { ELEMENT_IMAGE, ELEMENT_PARAGRAPH } from "@udecode/plate";
export const CONFIG = {
editableProps: {
spellCheck: false,
autoFocus: true,
placeholder: "Type…",
},
trailingBlock: { type: ELEMENT_PARAGRAPH },
softBreak: {
options: {
rules: [
{
hotkey: "shift+enter",
query: {
allow: [ELEMENT_IMAGE, ELEMENT_PARAGRAPH],
},
},
],
},
},
exitBreak: {
options: {
rules: [
{
hotkey: "mod+enter",
query: {
allow: [ELEMENT_IMAGE, ELEMENT_PARAGRAPH],
},
},
// {
// hotkey: "mod+shift+enter",
// before: true,
// },
// {
// hotkey: "enter",
// query: {
// start: true,
// end: true,
// allow: KEYS_HEADING,
// },
// },
],
},
},
selectOnBackspace: {
options: {
query: {
allow: [ELEMENT_IMAGE],
},
},
},
};
+163
View File
@@ -0,0 +1,163 @@
import { useRef, useState, useCallback } from "react";
import { useKey } from "rooks";
import { Editor, Transforms } from "slate";
import {
createPlateUI,
Plate,
createExitBreakPlugin,
createTrailingBlockPlugin,
createNodeIdPlugin,
createParagraphPlugin,
createImagePlugin,
createSoftBreakPlugin,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateActions,
// usePlateEditorRef,
ELEMENT_IMAGE,
// usePlateStore
} from "@udecode/plate";
import Styled from "./styled";
import ImageElement from "./ImageElement";
import { CONFIG } from "./config";
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
let components = createPlateUI({
[ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key
});
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
const Plugins = ({
id = "",
placeholder = "Write some markdown...",
sendMessages,
}) => {
// const plateEditor = usePlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null);
// const editor = useEditorRef();
const initialProps = {
...CONFIG.editableProps,
className: "box",
placeholder,
};
useKey(
"Enter",
(evt) => {
console.log("enter keypress", evt);
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
return true;
}
evt.preventDefault();
sendMessages(msgs);
// 清空
const plateEditor = getPlateActions(`${TEXT_EDITOR_PREFIX}_${id}`).editor;
Transforms.delete(plateEditor, {
at: {
anchor: Editor.start(plateEditor, []),
focus: Editor.end(plateEditor, []),
},
});
},
{
// eventTypes: ["keydown"],
target: editableRef,
when: !cmdKey,
}
);
useKey(
[91, 93],
(evt) => {
setCmdKey(evt.type == "keydown");
console.log("cmd", evt.type);
},
{
eventTypes: ["keydown", "keyup"],
target: editableRef,
}
);
const plugins = createPlugins(
[
createParagraphPlugin(),
createImagePlugin(),
createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock),
createExitBreakPlugin(CONFIG.exitBreak),
],
{
components,
}
);
const handleChange = useCallback(
async (val) => {
console.log("tmps changed");
const tmps = [];
for await (const v of val) {
if (v.type == "img") {
// img
const resp = await fetch(v.url);
const value = await resp.blob();
tmps.push({ type: "image", value });
} else {
// text
const prev = tmps[tmps.length - 1];
if (!prev) {
tmps.push([{ type: "text", value: v.children[0].text }]);
} else {
if (Array.isArray(prev)) {
tmps[tmps.length - 1].push({
type: "text",
value: v.children[0].text,
});
} else {
tmps.push([{ type: "text", value: v.children[0].text }]);
}
}
}
}
const arr = tmps.map((tmp) => {
return Array.isArray(tmp)
? { type: "text", value: tmp.map((t) => t.value).join("\n") }
: tmp;
});
const msgs = arr.filter(({ value }) => !!value);
setMsgs(msgs);
console.log("tmps", val, msgs);
},
[msgs]
);
// useEffect(() => {
// return () => {
// if (plateEditor) {
// // 清空
// Transforms.delete(plateEditor, {
// at: {
// anchor: Editor.start(plateEditor, []),
// focus: Editor.end(plateEditor, []),
// },
// });
// }
// };
// }, [plateEditor]);
return (
<Styled className="input" ref={editableRef}>
<Plate
onChange={handleChange}
id={`${TEXT_EDITOR_PREFIX}_${id}`}
editableProps={{ ...initialProps, style: { userSelect: "text" } }}
initialValue={initialValue}
plugins={plugins}
></Plate>
</Styled>
);
};
export default Plugins;
// export default memo(Plugins, (prevs, nexts) => {
// console.log("placeholder", prevs.placeholder, nexts.placeholder);
// return prevs.placeholder == nexts.placeholder;
// });
@@ -0,0 +1,21 @@
import {
createBasicElementsPlugin,
createImagePlugin,
createParagraphPlugin,
createSelectOnBackspacePlugin,
} from "@udecode/plate";
import { CONFIG } from "./config";
const basicElements = [
createParagraphPlugin(), // paragraph element
];
export const PLUGINS = {
basicElements,
basicNodes: [...basicElements],
image: [
createBasicElementsPlugin(),
createImagePlugin(),
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
],
};
+19
View File
@@ -0,0 +1,19 @@
import styled from "styled-components";
const Styled = styled.div`
max-height: 50vh;
overflow: auto;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #475467;
> .box {
display: flex;
flex-direction: column;
gap: 16px;
p {
padding: 0;
}
}
`;
export default Styled;
@@ -0,0 +1,6 @@
const nodeTypes = {
paragraph: "p",
image: "img",
};
export default nodeTypes;
@@ -0,0 +1,30 @@
import {
ELEMENT_PARAGRAPH,
// TElement,
} from "@udecode/plate";
// import { Text } from 'slate'
export const createElement = (
text = "",
{ type = ELEMENT_PARAGRAPH, mark } = {}
) => {
const leaf = { text };
if (mark) {
leaf[mark] = true;
}
return {
type,
children: [leaf],
};
};
export const getNodesWithRandomId = (nodes = []) => {
let _id = 10000;
nodes.forEach((node) => {
node.id = _id;
_id++;
});
return nodes;
};
+67 -15
View File
@@ -1,8 +1,16 @@
import { useEffect, useState } from "react";
import showdown from "showdown";
import { useEffect, useState, useRef } from "react";
import { Viewer } from "@toast-ui/react-editor";
// import showdown from "showdown";
// import showdownHighlight from "showdown-highlight";
import styled from "styled-components";
import ImagePreviewModal from "../../common/component/ImagePreviewModal";
// const { codeSyntaxHighlight } = Editor.plugin;
const Styled = styled.div`
p {
* {
user-select: text;
}
/* p {
font-weight: 400;
font-size: 14px;
line-height: 22px;
@@ -11,11 +19,6 @@ const Styled = styled.div`
ul {
list-style-type: disc;
}
i,
em {
/* padding: 0 2px; */
font-style: italic;
}
ul,
ol {
font-weight: 400;
@@ -24,6 +27,10 @@ const Styled = styled.div`
color: #475467;
list-style-position: inside;
}
i,
em {
font-style: italic;
}
strong {
font-weight: 700;
}
@@ -56,15 +63,60 @@ const Styled = styled.div`
line-height: 20px;
padding: 16px;
}
pre .hljs {
background-color: #eee;
} */
/* pre {
user-select: text;
padding: 16px;
background-color: #0d1117;
code {
user-select: text;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #f5feff;
}
} */
`;
export default function MrakdownRender({ content }) {
const [html, setHtml] = useState("");
const mdContainer = useRef(null);
const [previewImage, setPreviewImage] = useState(null);
useEffect(() => {
if (content) {
const converter = new showdown.Converter();
setHtml(converter.makeHtml(content));
if (mdContainer) {
const container = mdContainer.current;
// 点击查看大图
container.addEventListener(
"click",
(evt) => {
console.log(evt);
const { target } = evt;
// 图片
if (target.nodeType == 1) {
setPreviewImage(target.dataset.origin || target.src);
}
},
true
);
}
}, [content]);
return <Styled dangerouslySetInnerHTML={{ __html: html }}></Styled>;
}, []);
const closePreviewModal = () => {
setPreviewImage(null);
};
return (
<>
{previewImage && (
<ImagePreviewModal
image={previewImage}
closeModal={closePreviewModal}
/>
)}
<Styled ref={mdContainer}>
<Viewer
initialValue={content}
// plugins={[[codeSyntaxHighlight]]}
></Viewer>
</Styled>
</>
);
}
+2 -1
View File
@@ -54,7 +54,7 @@ import Styled from "./styled";
import { CONFIG } from "./config";
// import { VALUES } from "./values/values";
const id = "rustchat_richEditor";
const id = "rustchat_rich_editor";
let components = createPlateUI({
// customize your components by plugin key
@@ -183,6 +183,7 @@ const Plugins = ({
updatePureText("");
}
};
return (
<Styled className="input" ref={editableRef}>
<Plate
+25 -2
View File
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux";
import { ContentTypes } from "../../../app/config";
import closeIcon from "../../../assets/icons/close.circle.svg?url";
import pictureIcon from "../../../assets/icons/picture.svg?url";
import { getFileIcon } from "../../utils";
import { removeReplyingMessage } from "../../../app/slices/message";
import styled from "styled-components";
const Styled = styled.div`
@@ -41,6 +42,17 @@ const Styled = styled.div`
overflow: hidden;
text-overflow: ellipsis;
padding-right: 30px;
display: flex;
align-items: center;
.icon {
width: 15px;
height: 20px;
}
.name {
margin-left: 5px;
font-size: 10px;
color: #555;
}
}
.close {
background: none;
@@ -51,7 +63,7 @@ const Styled = styled.div`
}
`;
const renderContent = (data) => {
const { content_type, content } = data;
const { content_type, content, properties } = data;
let res = null;
switch (content_type) {
case ContentTypes.text:
@@ -61,7 +73,18 @@ const renderContent = (data) => {
case ContentTypes.imageJPG:
res = <img className="pic" src={pictureIcon} />;
break;
case ContentTypes.file:
{
const { file_type, name } = properties;
const icon = getFileIcon(file_type, name);
res = (
<>
{icon}
<span className="name">{name}</span>
</>
);
}
break;
default:
break;
}
+45 -24
View File
@@ -1,6 +1,6 @@
// import React from 'react'
import { useState, useRef } from "react";
import styled from "styled-components";
import UploadModal from "../../component/UploadModal";
import AddIcon from "../../../assets/icons/add.solid.svg";
import MarkdownIcon from "../../../assets/icons/markdown.svg";
const Styled = styled.div`
@@ -8,9 +8,15 @@ const Styled = styled.div`
align-items: center;
justify-content: flex-end;
gap: 10px;
/* &.markdown {
flex-direction: column;
} */
.md {
cursor: pointer;
display: flex;
.markdown path {
fill: #22ccee;
}
}
.add {
cursor: pointer;
@@ -28,30 +34,45 @@ const Styled = styled.div`
}
}
`;
export default function Toolbar({
contentType = "text",
updateContentType,
handleUpload,
handleSend,
}) {
const toggleMarkdown = () => {
updateContentType(contentType == "markdown" ? "text" : "markdown");
export default function Toolbar({ toggleMode, mode, to, type }) {
const [files, setFiles] = useState([]);
const fileInputRef = useRef(null);
const resetFiles = () => {
setFiles([]);
fileInputRef.current.value = "";
};
const handleUpload = (evt) => {
const files = [...evt.target.files];
console.log("files", files);
setFiles([...evt.target.files]);
};
return (
<Styled>
<div className="md" onClick={toggleMarkdown}>
{contentType == "markdown" ? <MarkdownIcon /> : <MarkdownIcon />}
</div>
<div className="add">
<AddIcon />
<input
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
<>
{files.length !== 0 && (
<UploadModal
type={type}
files={files}
sendTo={to}
closeModal={resetFiles}
/>
</div>
</Styled>
)}
<Styled className={mode}>
<div className="md" onClick={toggleMode}>
<MarkdownIcon className={mode} />
</div>
<div className="add">
<AddIcon />
<input
ref={fileInputRef}
multiple={false}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</div>
</Styled>
</>
);
}
+84 -83
View File
@@ -1,122 +1,123 @@
import { useState, useEffect, useRef } from "react";
import { memo } from "react";
// import TextareaAutosize from "react-textarea-autosize";
import { useDispatch, useSelector } from "react-redux";
// import { useKey } from "rooks";
import { getPlateEditorRef } from "@udecode/plate";
import { updateInputMode } from "../../../app/slices/ui";
import { removeReplyingMessage } from "../../../app/slices/message";
import { useSendChannelMsgMutation } from "../../../app/services/channel";
import { useSendMsgMutation } from "../../../app/services/contact";
import { useReplyMessageMutation } from "../../../app/services/message";
import StyledSend from "./styled";
import useFiles from "./useFiles";
import UploadModal from "./UploadModal";
import Replying from "./Replying";
import Toolbar from "./Toolbar";
import EmojiPicker from "./EmojiPicker";
// import RichInput from "../RichInput";
import RichInput from "../RichEditor";
import MarkdownEditor from "../MarkdownEditor";
// import MarkdownInput from "../MarkdownInput";
import MixedInput, { TEXT_EDITOR_PREFIX } from "../MixedInput";
const Types = {
channel: "#",
user: "@",
};
export default function Send({
const Modes = {
text: "text",
markdown: "markdown",
};
function Send({
name,
type = "channel",
// 发给谁,或者是channel,或者是user
id = "",
dragFiles = [],
}) {
const [contentType, setContentType] = useState("text");
// const editorRef = usePlateEditorRef(`${TEXT_EDITOR_PREFIX}_${type}_${id}`);
const [replyMessage] = useReplyMessageMutation();
const { files, setFiles, resetFiles } = useFiles([]);
// const [shift, setShift] = useState(false);
// const [enter, setEnter] = useState(false);
const [editor, setEditor] = useState(null);
const [markdown, setMarkdown] = useState("");
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
// 谁发的
const { from_uid, replying_mid = null } = useSelector((store) => {
const { from_uid, replying_mid = null, mode } = useSelector((store) => {
return {
mode: store.ui.inputMode,
from_uid: store.authData.uid,
replying_mid: store.message.replying[id],
};
});
useEffect(() => {
if (dragFiles.length) {
setFiles((prev) => [...prev, ...dragFiles]);
}
}, [dragFiles]);
const [sendMsg, { isLoading: userSending }] = useSendMsgMutation();
const [
sendChannelMsg,
{ isLoading: channelSending },
] = useSendChannelMsgMutation();
const [sendMsg] = useSendMsgMutation();
const [sendChannelMsg] = useSendChannelMsgMutation();
const sendMessage = type == "channel" ? sendChannelMsg : sendMsg;
const sendingMessage = userSending || channelSending;
// const sendingMessage = userSending || channelSending;
const insertEmoji = (emoji) => {
console.log("insert emoji", emoji, editor);
editor.insertText(emoji);
};
const handleUpload = (evt) => {
setFiles([...evt.target.files]);
};
const handleSendMessage = () => {
if (!markdown || !id || sendingMessage) return;
console.log("send message", msg);
const content_type = msg ? "text" : "markdown";
const content = msg ? msg : markdown;
console.log("current message", markdown, msg);
if (replying_mid) {
console.log("replying", replying_mid);
replyMessage({
id,
reply_mid: replying_mid,
type: content_type,
content,
context: type,
from_uid,
});
dispatch(removeReplyingMessage(id));
} else {
sendMessage({
id,
type: content_type,
content,
from_uid,
properties: { local_id: new Date().getTime() },
});
console.log("insert emoji", emoji);
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${type}_${id}`);
if (editorRef) {
console.log("wtf", editorRef);
editorRef.insertText(emoji);
}
setMsg("");
};
const handleSendMessage = async (msgs = []) => {
if (!msgs || msgs.length == 0 || !id) return;
for await (const msg of msgs) {
const { type, value: content } = msg;
if (replying_mid) {
console.log("replying", replying_mid);
await replyMessage({
id,
reply_mid: replying_mid,
type,
content,
context: type,
from_uid,
});
dispatch(removeReplyingMessage(id));
} else {
await sendMessage({
id,
type,
content,
from_uid,
properties: { local_id: new Date().getTime() },
});
}
}
};
const sendMarkdown = (content) => {
sendMessage({
id,
type: "markdown",
content,
from_uid,
properties: { local_id: new Date().getTime() },
});
};
const toggleMode = () => {
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
};
return (
<>
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${type}`}>
{replying_mid && <Replying mid={replying_mid} id={id} />}
<EmojiPicker selectEmoji={insertEmoji} />
<RichInput
setEditor={setEditor}
<StyledSend
className={`send ${mode} ${replying_mid ? "reply" : ""} ${type}`}
>
{replying_mid && <Replying mid={replying_mid} id={id} />}
<EmojiPicker selectEmoji={insertEmoji} />
{mode == Modes.text && (
<MixedInput
id={`${type}_${id}`}
placeholder={`Send to ${Types[type]}${name} `}
sendMessage={handleSendMessage}
updateMarkdown={setMarkdown}
updatePureText={setMsg}
/>
<Toolbar
contentType={contentType}
updateContentType={setContentType}
handleUpload={handleUpload}
/>
</StyledSend>
{files.length !== 0 && (
<UploadModal
type={type}
files={files}
sendTo={id}
closeModal={resetFiles}
sendMessages={handleSendMessage}
/>
)}
</>
{/* <MarkdownInput placeholder={`Send to ${Types[type]}${name} `} /> */}
<Toolbar type={type} to={id} mode={mode} toggleMode={toggleMode} />
{mode == Modes.markdown && (
<MarkdownEditor
placeholder={`Send to ${Types[type]}${name} `}
// updateMarkdown={setMarkdown}
sendMarkdown={sendMarkdown}
/>
)}
</StyledSend>
);
}
export default memo(Send, (prevs, nexts) => {
console.log("send name", prevs.name, nexts.name);
return prevs.name == nexts.name;
});
+12 -2
View File
@@ -5,20 +5,30 @@ const StyledSend = styled.div`
background: #e5e7eb;
border-radius: var(--br);
width: 100%;
/* width: fit-content; */
/* min-height: 54px; */
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
padding: 14px 18px;
/* margin: 0 16px; */
&.markdown {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 0;
.input {
grid-column: span 2;
}
}
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.input {
width: 100%;
/* padding: 4px 0; */
}
`;
+2
View File
@@ -8,6 +8,7 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "./assets/vars.css";
import "animate.css";
import MarkdownStyleOverride from "./common/component/MarkdownStyleOverride";
import ReduxRoutes from "./routes";
import BASE_URL from "./app/config";
@@ -21,6 +22,7 @@ ReactDOM.render(
<DndProvider backend={HTML5Backend}>
<ReduxRoutes />
</DndProvider>
<MarkdownStyleOverride />
</>,
document.getElementById("root")
);