feat: markdown auto format
This commit is contained in:
@@ -13,6 +13,16 @@ export default async function handler({ operation, data = {}, payload }) {
|
||||
await table.setItem(id + "", data.byId[id]);
|
||||
}
|
||||
break;
|
||||
case "removeUserSession":
|
||||
{
|
||||
const ids = Array.isArray(payload) ? payload : [payload];
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
await table.removeItem(id + "");
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ const whiteList = [
|
||||
"getServer",
|
||||
"getOpenid",
|
||||
"getMetamaskNonce",
|
||||
"renew",
|
||||
];
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: BASE_URL,
|
||||
prepareHeaders: (headers, { getState, endpoint }) => {
|
||||
console.log("req", endpoint);
|
||||
const { token } = getState().authData;
|
||||
const noHeaderList = [...whiteList, "renew"];
|
||||
if (token && !noHeaderList.includes(endpoint)) {
|
||||
if (token && !whiteList.includes(endpoint)) {
|
||||
headers.set(tokenHeader, token);
|
||||
}
|
||||
return headers;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
// import { batch } from "react-redux";
|
||||
|
||||
import { ContentTypes } from "../config";
|
||||
import BASE_URL, { ContentTypes } from "../config";
|
||||
import { updateReadChannels, updateReadUsers } from "../slices/footprint";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
|
||||
@@ -38,6 +38,19 @@ export const messageApi = createApi({
|
||||
method: "DELETE",
|
||||
}),
|
||||
}),
|
||||
uploadImage: builder.mutation({
|
||||
query: (image) => ({
|
||||
headers: {
|
||||
"content-type": ContentTypes.image,
|
||||
},
|
||||
url: `/resource/image`,
|
||||
method: "POST",
|
||||
body: image,
|
||||
}),
|
||||
transformResponse: (image_id) => {
|
||||
return `${BASE_URL}/resource/image?id=${encodeURIComponent(image_id)}`;
|
||||
},
|
||||
}),
|
||||
replyMessage: builder.mutation({
|
||||
query: ({ reply_mid, content, type = "text" }) => ({
|
||||
headers: {
|
||||
@@ -89,6 +102,7 @@ export const messageApi = createApi({
|
||||
});
|
||||
|
||||
export const {
|
||||
useUploadImageMutation,
|
||||
useEditMessageMutation,
|
||||
useReactMessageMutation,
|
||||
useReplyMessageMutation,
|
||||
|
||||
@@ -38,9 +38,19 @@ const userMsgSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
removeUserSession(state, action) {
|
||||
const ids = Array.isArray(action.payload)
|
||||
? action.payload
|
||||
: [action.payload];
|
||||
state.ids = state.ids.filter((id) => ids.findIndex((i) => i == id) == -1);
|
||||
// ids.forEach((id) => {
|
||||
// delete state.byId[id];
|
||||
// });
|
||||
},
|
||||
},
|
||||
});
|
||||
export const {
|
||||
removeUserSession,
|
||||
resetUserMsg,
|
||||
fullfillUserMsg,
|
||||
addUserMsg,
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function DeleteConfirmModal({ id, closeModal }) {
|
||||
buttons={
|
||||
<>
|
||||
{" "}
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={closeModal.bind(null, undefined)}>Cancel</Button>
|
||||
<Button onClick={handleDelete} className="danger">
|
||||
{isLoading ? "Deleting" : `Delete`}
|
||||
</Button>
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import { useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useOutsideClick } from "rooks";
|
||||
import StyledMenu from "./StyledMenu";
|
||||
const StyledWrapper = styled(StyledMenu)`
|
||||
z-index: 999;
|
||||
position: fixed;
|
||||
left: ${({ x }) => `${x}px`};
|
||||
top: ${({ y }) => `${y}px`};
|
||||
`;
|
||||
export default function ContextMenu({
|
||||
posX = 0,
|
||||
posY = 0,
|
||||
items = [],
|
||||
hideMenu,
|
||||
}) {
|
||||
const wrapperRef = useRef(null);
|
||||
useOutsideClick(wrapperRef, hideMenu);
|
||||
// import { useRef } from "react";
|
||||
// import styled from "styled-components";
|
||||
import StyledMenu from "./styled/Menu";
|
||||
export default function ContextMenu({ items = [], hideMenu }) {
|
||||
return (
|
||||
<StyledWrapper ref={wrapperRef} x={posX} y={posY}>
|
||||
<StyledMenu>
|
||||
{items.map((item) => {
|
||||
if (!item) return null;
|
||||
const {
|
||||
@@ -44,6 +30,6 @@ export default function ContextMenu({
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StyledWrapper>
|
||||
</StyledMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import useCopy from "../hook/useCopy";
|
||||
import { useLazyDeleteContactQuery } from "../../app/services/contact";
|
||||
import { useRemoveMembersMutation } from "../../app/services/channel";
|
||||
import Contact from "./Contact";
|
||||
import StyledMenu from "./StyledMenu";
|
||||
import StyledMenu from "./styled/Menu";
|
||||
import InviteLink from "./InviteLink";
|
||||
import moreIcon from "../../assets/icons/more.svg?url";
|
||||
const StyledWrapper = styled.section`
|
||||
|
||||
@@ -5,7 +5,7 @@ import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
// import toast from "react-hot-toast";
|
||||
import { addReplyingMessage } from "../../../app/slices/message";
|
||||
import StyledMenu from "../StyledMenu";
|
||||
import StyledMenu from "../styled/Menu";
|
||||
import DeleteMessageConfirm from "./DeleteMessageConfirm";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import replyIcon from "../../../assets/icons/reply.svg?url";
|
||||
@@ -108,7 +108,7 @@ export default function Commands({
|
||||
onShow={handleTippyVisible.bind(null, true)}
|
||||
onHide={handleTippyVisible.bind(null, false)}
|
||||
interactive
|
||||
placement="right-start"
|
||||
placement="left-start"
|
||||
trigger="click"
|
||||
content={
|
||||
<StyledMenu className="menu">
|
||||
|
||||
@@ -1,153 +1,70 @@
|
||||
// import React from 'react'
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useEffect, useState } from "react";
|
||||
import showdown from "showdown";
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
font-family: "Helvetica Neue", sans;
|
||||
background: transparent;
|
||||
color: #222;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
img {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00213d;
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #00213d;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #318100;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p img {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #003a6b;
|
||||
background-color: transparent;
|
||||
margin: 1em 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 180%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
dt {
|
||||
i,
|
||||
em {
|
||||
/* padding: 0 2px; */
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.1em;
|
||||
ul,
|
||||
ol {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
font-family: "Menlo", monospace;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #efefef;
|
||||
line-height: 20px;
|
||||
color: #475467;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: "Menlo", monospace;
|
||||
background-color: #fff;
|
||||
padding: 0.5em;
|
||||
line-height: 1.25em;
|
||||
border: 1px solid #efefef;
|
||||
border-bottom: 1px solid #ddd;
|
||||
-webkit-box-shadow: 0 1px 3px 0 #eee;
|
||||
-moz-box-shadow: 0 1px 3px 0 #eee;
|
||||
-ms-box-shadow: 0 1px 3px 0 #eee;
|
||||
box-shadow: 0 1px 3px 0 #eee;
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: 700;
|
||||
color: #475467;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-top: 1px solid #efefef;
|
||||
border-bottom: 1px solid #ddd;
|
||||
-webkit-box-shadow: 0 1px 3px 0 #eee;
|
||||
-moz-box-shadow: 0 1px 3px 0 #eee;
|
||||
-ms-box-shadow: 0 1px 3px 0 #eee;
|
||||
box-shadow: 0 1px 3px 0 #eee;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #efefef;
|
||||
border-bottom: 1px solid #ddd;
|
||||
-webkit-box-shadow: 0 1px 3px 0 #eee;
|
||||
-moz-box-shadow: 0 1px 3px 0 #eee;
|
||||
-ms-box-shadow: 0 1px 3px 0 #eee;
|
||||
box-shadow: 0 1px 3px 0 #eee;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
color: #98a2b3;
|
||||
opacity: 0.8;
|
||||
box-shadow: inset 2px 0px 0px #a8b0bd;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
export default function MrakdownRender({ content }) {
|
||||
return (
|
||||
<Styled>
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
</Styled>
|
||||
);
|
||||
const [html, setHtml] = useState("");
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
const converter = new showdown.Converter();
|
||||
setHtml(converter.makeHtml(content));
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return <Styled dangerouslySetInnerHTML={{ __html: html }}></Styled>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
// AutoformatRule,
|
||||
ELEMENT_BLOCKQUOTE,
|
||||
ELEMENT_CODE_BLOCK,
|
||||
ELEMENT_DEFAULT,
|
||||
ELEMENT_H1,
|
||||
ELEMENT_H2,
|
||||
ELEMENT_H3,
|
||||
ELEMENT_H4,
|
||||
ELEMENT_H5,
|
||||
ELEMENT_H6,
|
||||
ELEMENT_HR,
|
||||
getPluginType,
|
||||
insertEmptyCodeBlock,
|
||||
insertNodes,
|
||||
// PlateEditor,
|
||||
setNodes,
|
||||
} from "@udecode/plate";
|
||||
import { preFormat } from "./autoformatUtils";
|
||||
|
||||
export const autoformatBlocks = [
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H1,
|
||||
match: "# ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H2,
|
||||
match: "## ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H3,
|
||||
match: "### ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H4,
|
||||
match: "#### ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H5,
|
||||
match: "##### ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_H6,
|
||||
match: "###### ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_BLOCKQUOTE,
|
||||
match: "> ",
|
||||
preFormat,
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_CODE_BLOCK,
|
||||
match: "```",
|
||||
triggerAtBlockStart: false,
|
||||
preFormat,
|
||||
format: (editor) => {
|
||||
insertEmptyCodeBlock(editor, {
|
||||
defaultType: getPluginType(editor, ELEMENT_DEFAULT),
|
||||
insertNodesOptions: { select: true },
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_HR,
|
||||
match: ["---", "—-", "___ "],
|
||||
format: (editor) => {
|
||||
setNodes(editor, { type: ELEMENT_HR });
|
||||
insertNodes(editor, {
|
||||
type: ELEMENT_DEFAULT,
|
||||
children: [{ text: "" }],
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
// AutoformatRule,
|
||||
ELEMENT_LI,
|
||||
ELEMENT_OL,
|
||||
ELEMENT_TODO_LI,
|
||||
ELEMENT_UL,
|
||||
setNodes,
|
||||
// TElement,
|
||||
// TodoListItemNodeData,
|
||||
} from "@udecode/plate";
|
||||
import { Editor } from "slate";
|
||||
import { formatList, preFormat } from "./autoformatUtils";
|
||||
|
||||
export const autoformatLists = [
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_LI,
|
||||
match: ["* ", "- "],
|
||||
preFormat,
|
||||
format: (editor) => formatList(editor, ELEMENT_UL),
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_LI,
|
||||
match: ["1. ", "1) "],
|
||||
preFormat,
|
||||
format: (editor) => formatList(editor, ELEMENT_OL),
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_TODO_LI,
|
||||
match: "[] ",
|
||||
},
|
||||
{
|
||||
mode: "block",
|
||||
type: ELEMENT_TODO_LI,
|
||||
match: "[x] ",
|
||||
format: (editor) =>
|
||||
setNodes(
|
||||
editor,
|
||||
{ type: ELEMENT_TODO_LI, checked: true },
|
||||
{
|
||||
match: (n) => Editor.isBlock(editor, n),
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
// AutoformatRule,
|
||||
MARK_BOLD,
|
||||
MARK_CODE,
|
||||
MARK_HIGHLIGHT,
|
||||
MARK_ITALIC,
|
||||
MARK_STRIKETHROUGH,
|
||||
MARK_SUBSCRIPT,
|
||||
MARK_SUPERSCRIPT,
|
||||
MARK_UNDERLINE,
|
||||
} from "@udecode/plate";
|
||||
|
||||
export const autoformatMarks = [
|
||||
{
|
||||
mode: "mark",
|
||||
type: [MARK_BOLD, MARK_ITALIC],
|
||||
match: "***",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: [MARK_UNDERLINE, MARK_ITALIC],
|
||||
match: "__*",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: [MARK_UNDERLINE, MARK_BOLD],
|
||||
match: "__**",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: [MARK_UNDERLINE, MARK_BOLD, MARK_ITALIC],
|
||||
match: "___***",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_BOLD,
|
||||
match: "**",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_UNDERLINE,
|
||||
match: "__",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_ITALIC,
|
||||
match: "*",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_ITALIC,
|
||||
match: "_",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_STRIKETHROUGH,
|
||||
match: "~~",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_SUPERSCRIPT,
|
||||
match: "^",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_SUBSCRIPT,
|
||||
match: "~",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_HIGHLIGHT,
|
||||
match: "==",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_HIGHLIGHT,
|
||||
match: "≡",
|
||||
},
|
||||
{
|
||||
mode: "mark",
|
||||
type: MARK_CODE,
|
||||
match: "`",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
autoformatArrow,
|
||||
autoformatLegal,
|
||||
autoformatLegalHtml,
|
||||
autoformatMath,
|
||||
autoformatPunctuation,
|
||||
autoformatSmartQuotes,
|
||||
} from "@udecode/plate";
|
||||
import { autoformatBlocks } from "./autoformatBlocks";
|
||||
import { autoformatLists } from "./autoformatLists";
|
||||
import { autoformatMarks } from "./autoformatMarks";
|
||||
|
||||
export const autoformatRules = [
|
||||
...autoformatBlocks,
|
||||
...autoformatLists,
|
||||
...autoformatMarks,
|
||||
...autoformatSmartQuotes,
|
||||
...autoformatPunctuation,
|
||||
...autoformatLegal,
|
||||
...autoformatLegalHtml,
|
||||
...autoformatArrow,
|
||||
...autoformatMath,
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
// AutoformatBlockRule,
|
||||
ELEMENT_CODE_BLOCK,
|
||||
ELEMENT_CODE_LINE,
|
||||
getParent,
|
||||
isElement,
|
||||
isType,
|
||||
// PlateEditor,
|
||||
// TEditor,
|
||||
toggleList,
|
||||
unwrapList,
|
||||
} from "@udecode/plate";
|
||||
|
||||
export const preFormat = (editor) => unwrapList(editor);
|
||||
|
||||
export const format = (editor, customFormatting) => {
|
||||
if (editor.selection) {
|
||||
const parentEntry = getParent(editor, editor.selection);
|
||||
if (!parentEntry) return;
|
||||
const [node] = parentEntry;
|
||||
if (
|
||||
isElement(node) &&
|
||||
!isType(editor, node, ELEMENT_CODE_BLOCK) &&
|
||||
!isType(editor, node, ELEMENT_CODE_LINE)
|
||||
) {
|
||||
customFormatting();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const formatList = (editor, elementType = "") => {
|
||||
format(editor, () =>
|
||||
toggleList(editor, {
|
||||
type: elementType,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const formatText = (editor, text = "") => {
|
||||
format(editor, () => editor.insertText(text));
|
||||
};
|
||||
@@ -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,180 @@
|
||||
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_TD,
|
||||
ELEMENT_TODO_LI,
|
||||
// ExitBreakPlugin,
|
||||
// IndentPlugin,
|
||||
isBlockAboveEmpty,
|
||||
isSelectionAtBlockStart,
|
||||
KEYS_HEADING,
|
||||
// NormalizeTypesPlugin,
|
||||
// PlatePlugin,
|
||||
// ResetNodePlugin,
|
||||
// SelectOnBackspacePlugin,
|
||||
// SoftBreakPlugin,
|
||||
// TrailingBlockPlugin,
|
||||
withProps,
|
||||
} from "@udecode/plate";
|
||||
// import { EditableProps } from 'slate-react/dist/components/editable'
|
||||
import { css } from "styled-components";
|
||||
import { autoformatRules } from "./autoformat/autoformatRules";
|
||||
// import { MENTIONABLES } from './mentionables'
|
||||
|
||||
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;
|
||||
}
|
||||
`,
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
align: {
|
||||
inject: {
|
||||
props: {
|
||||
validTypes: [
|
||||
ELEMENT_PARAGRAPH,
|
||||
ELEMENT_H1,
|
||||
ELEMENT_H2,
|
||||
ELEMENT_H3,
|
||||
ELEMENT_H4,
|
||||
ELEMENT_H5,
|
||||
ELEMENT_H6,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
indent: {
|
||||
inject: {
|
||||
props: {
|
||||
validTypes: [
|
||||
ELEMENT_PARAGRAPH,
|
||||
ELEMENT_H1,
|
||||
ELEMENT_H2,
|
||||
ELEMENT_H3,
|
||||
ELEMENT_H4,
|
||||
ELEMENT_H5,
|
||||
ELEMENT_H6,
|
||||
ELEMENT_BLOCKQUOTE,
|
||||
ELEMENT_CODE_BLOCK,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
lineHeight: {
|
||||
inject: {
|
||||
props: {
|
||||
defaultNodeValue: 1.5,
|
||||
validNodeValues: [1, 1.2, 1.5, 2, 3],
|
||||
validTypes: [
|
||||
ELEMENT_PARAGRAPH,
|
||||
ELEMENT_H1,
|
||||
ELEMENT_H2,
|
||||
ELEMENT_H3,
|
||||
ELEMENT_H4,
|
||||
ELEMENT_H5,
|
||||
ELEMENT_H6,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
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],
|
||||
},
|
||||
},
|
||||
},
|
||||
autoformat: {
|
||||
options: {
|
||||
rules: autoformatRules,
|
||||
},
|
||||
},
|
||||
// mentionItems: MENTIONABLES,
|
||||
forceLayout: {
|
||||
options: {
|
||||
rules: [{ path: [0], strictType: ELEMENT_H1 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from "react";
|
||||
import { serialize } from "remark-slate";
|
||||
import { useKey } from "rooks";
|
||||
|
||||
import { useUploadImageMutation } from "../../../app/services/message";
|
||||
import nodeTypes from "./values/nodeTypes";
|
||||
import {
|
||||
createPlateUI,
|
||||
// HeadingToolbar,
|
||||
// MentionCombobox,
|
||||
Plate,
|
||||
createAutoformatPlugin,
|
||||
createBlockquotePlugin,
|
||||
createBoldPlugin,
|
||||
createCodeBlockPlugin,
|
||||
createCodePlugin,
|
||||
createExitBreakPlugin,
|
||||
createHeadingPlugin,
|
||||
createHighlightPlugin,
|
||||
createKbdPlugin,
|
||||
createImagePlugin,
|
||||
createItalicPlugin,
|
||||
createLinkPlugin,
|
||||
createListPlugin,
|
||||
// createMediaEmbedPlugin,
|
||||
createNodeIdPlugin,
|
||||
createParagraphPlugin,
|
||||
createResetNodePlugin,
|
||||
createSelectOnBackspacePlugin,
|
||||
createSoftBreakPlugin,
|
||||
// createDndPlugin,
|
||||
createStrikethroughPlugin,
|
||||
createTablePlugin,
|
||||
createTodoListPlugin,
|
||||
createTrailingBlockPlugin,
|
||||
createUnderlinePlugin,
|
||||
// createComboboxPlugin,
|
||||
// createMentionPlugin,
|
||||
createIndentPlugin,
|
||||
createDeserializeMdPlugin,
|
||||
createHorizontalRulePlugin,
|
||||
createPlugins,
|
||||
ELEMENT_PARAGRAPH,
|
||||
getPlateActions,
|
||||
platesActions,
|
||||
eventEditorStore,
|
||||
// usePlateStore
|
||||
// usePlateStore
|
||||
} from "@udecode/plate";
|
||||
// import MarkBallonToolbar from "./components/MarkBallonToolbar";
|
||||
import Styled from "./styled";
|
||||
// import { withStyledPlaceHolders } from "./components/withStyledPlaceHolders";
|
||||
// import { MENTIONABLES } from "./mentionables";
|
||||
import { CONFIG } from "./config";
|
||||
// import { VALUES } from "./values/values";
|
||||
|
||||
const id = "rustchat_richEditor";
|
||||
|
||||
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...",
|
||||
updateMarkdown,
|
||||
updatePureText,
|
||||
sendMessage,
|
||||
}) => {
|
||||
const editableRef = useRef(null);
|
||||
// const editor = useEditorRef();
|
||||
// const { } =usePlateStore()
|
||||
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(),
|
||||
createBlockquotePlugin(),
|
||||
createTodoListPlugin(),
|
||||
createHeadingPlugin(),
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
createHorizontalRulePlugin(),
|
||||
createLinkPlugin(),
|
||||
createListPlugin(),
|
||||
createTablePlugin(),
|
||||
// createMediaEmbedPlugin(),
|
||||
createCodeBlockPlugin(),
|
||||
createBoldPlugin(),
|
||||
createCodePlugin(),
|
||||
createItalicPlugin(),
|
||||
createHighlightPlugin(),
|
||||
createUnderlinePlugin(),
|
||||
createStrikethroughPlugin(),
|
||||
createKbdPlugin(),
|
||||
createNodeIdPlugin(),
|
||||
// createDndPlugin(),
|
||||
createIndentPlugin(CONFIG.indent),
|
||||
createAutoformatPlugin(CONFIG.autoformat),
|
||||
createResetNodePlugin(CONFIG.resetBlockType),
|
||||
// createSoftBreakPlugin(CONFIG.softBreak),
|
||||
createExitBreakPlugin(CONFIG.exitBreak),
|
||||
createTrailingBlockPlugin(CONFIG.trailingBlock),
|
||||
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
|
||||
// createComboboxPlugin(),
|
||||
// createMentionPlugin(),
|
||||
createDeserializeMdPlugin(),
|
||||
],
|
||||
{
|
||||
components,
|
||||
}
|
||||
);
|
||||
const handleChange = (val) => {
|
||||
const nonEmptyElements = val;
|
||||
// .filter((v) => {
|
||||
// const obj = v.children[0];
|
||||
// return obj.type == "p" && !!v.children[0].text;
|
||||
// });
|
||||
const md = nonEmptyElements
|
||||
.map((v) =>
|
||||
serialize(v, {
|
||||
nodeTypes,
|
||||
listDepth: 3,
|
||||
})
|
||||
)
|
||||
.join("\n");
|
||||
console.log("md changed", val, nonEmptyElements, md);
|
||||
updateMarkdown(md);
|
||||
const nonParagraphs = nonEmptyElements.filter((v) => {
|
||||
return v.type != "p" || v.children.length > 1;
|
||||
});
|
||||
if (nonParagraphs.length == 0) {
|
||||
const nonEmptyPs = nonEmptyElements.filter((v) => !!v.children[0].text);
|
||||
// 全部是P
|
||||
const pureText = nonEmptyPs
|
||||
.map((v) => {
|
||||
return v.children.map(({ text }) => text).join("");
|
||||
})
|
||||
.join("\n");
|
||||
console.log("pure text", nonEmptyPs, pureText);
|
||||
updatePureText(pureText);
|
||||
} else {
|
||||
updatePureText("");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Styled className="input" ref={editableRef}>
|
||||
<Plate
|
||||
onChange={handleChange}
|
||||
id={id}
|
||||
editableProps={{
|
||||
...CONFIG.editableProps,
|
||||
placeholder,
|
||||
}}
|
||||
initialValue={initialValue}
|
||||
plugins={plugins}
|
||||
>
|
||||
{/* <MarkBallonToolbar /> */}
|
||||
|
||||
{/* <MentionCombobox items={MENTIONABLES} /> */}
|
||||
</Plate>
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
export default Plugins;
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
createBasicElementsPlugin,
|
||||
createBlockquotePlugin,
|
||||
createBoldPlugin,
|
||||
createCodeBlockPlugin,
|
||||
createCodePlugin,
|
||||
createHeadingPlugin,
|
||||
createImagePlugin,
|
||||
createItalicPlugin,
|
||||
createParagraphPlugin,
|
||||
createSelectOnBackspacePlugin,
|
||||
createStrikethroughPlugin,
|
||||
createUnderlinePlugin,
|
||||
} 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],
|
||||
image: [
|
||||
createBasicElementsPlugin(),
|
||||
...basicMarks,
|
||||
createImagePlugin(),
|
||||
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
|
||||
],
|
||||
};
|
||||
+5
-13
@@ -5,17 +5,15 @@ const Styled = styled.div`
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
/* line-height: 20px; */
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
/* margin-bottom: 10px; */
|
||||
}
|
||||
i {
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
/* i {
|
||||
font-style: italic;
|
||||
}
|
||||
/* del{
|
||||
font-style: italic;
|
||||
} */
|
||||
ul,
|
||||
ol {
|
||||
font-weight: 400;
|
||||
@@ -24,12 +22,6 @@ const Styled = styled.div`
|
||||
color: #475467;
|
||||
list-style-position: inside;
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ol {
|
||||
/* list-style-type:; */
|
||||
}
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -60,7 +52,7 @@ const Styled = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
} */
|
||||
`;
|
||||
|
||||
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>></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,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
console.log("element type", element);
|
||||
switch (element.type) {
|
||||
case "del":
|
||||
return <del {...attributes}>{children}</del>;
|
||||
case "italic":
|
||||
return <i {...attributes}>{children}</i>;
|
||||
case "bold":
|
||||
return <strong {...attributes}>{children}</strong>;
|
||||
case "blockquote":
|
||||
return <blockquote {...attributes}>{children}</blockquote>;
|
||||
case "bulleted-list":
|
||||
return <ul {...attributes}>{children}</ul>;
|
||||
case "heading-one":
|
||||
return <h1 {...attributes}>{children}</h1>;
|
||||
case "heading-two":
|
||||
return <h2 {...attributes}>{children}</h2>;
|
||||
case "heading-three":
|
||||
return <h3 {...attributes}>{children}</h3>;
|
||||
case "heading-four":
|
||||
return <h4 {...attributes}>{children}</h4>;
|
||||
case "heading-five":
|
||||
return <h5 {...attributes}>{children}</h5>;
|
||||
case "heading-six":
|
||||
return <h6 {...attributes}>{children}</h6>;
|
||||
case "list-item":
|
||||
return <li {...attributes}>{children}</li>;
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>;
|
||||
}
|
||||
};
|
||||
|
||||
export default Element;
|
||||
@@ -1,118 +0,0 @@
|
||||
// import React from "react";
|
||||
// import styled from 'styled-components';
|
||||
import AutoReplace from "slate-auto-replace";
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from "react";
|
||||
import { serialize } from "remark-slate";
|
||||
import { Slate, Editable, withReact } from "slate-react";
|
||||
import { createEditor, Transforms, Editor } from "slate";
|
||||
import { withHistory } from "slate-history";
|
||||
import { useKey } from "rooks";
|
||||
import Styled from "./styled";
|
||||
|
||||
import initialValue, { emptyValue } from "./initialValue";
|
||||
import Element from "./Element";
|
||||
// import withShortcuts from "./withShortcuts";
|
||||
|
||||
// Add the plugin to your set of plugins...
|
||||
const plugins = [
|
||||
AutoReplace({
|
||||
trigger: "space",
|
||||
before: /^(>)$/,
|
||||
change: (change, e, matches) => {
|
||||
return change.setBlocks({ type: "blockquote" });
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export default function RichInput({
|
||||
setEditor,
|
||||
placeholder = "Write some markdown...",
|
||||
updateMarkdown,
|
||||
updatePureText,
|
||||
sendMessage,
|
||||
}) {
|
||||
const editableRef = useRef(null);
|
||||
const [shift, setShift] = useState(false);
|
||||
const [value, setValue] = useState(emptyValue);
|
||||
// const renderElement = useCallback((props) => <Element {...props} />, []);
|
||||
const editor = useMemo(() => withReact(withHistory(createEditor())), []);
|
||||
// const editor = useMemo(
|
||||
// () => withShortcuts(withReact(withHistory(createEditor()))),
|
||||
// []
|
||||
// );
|
||||
useEffect(() => {
|
||||
setEditor(editor);
|
||||
}, []);
|
||||
|
||||
useKey(
|
||||
"Enter",
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
sendMessage();
|
||||
Transforms.delete(editor, {
|
||||
at: {
|
||||
anchor: Editor.start(editor, []),
|
||||
focus: Editor.end(editor, []),
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
target: editableRef,
|
||||
when: !shift,
|
||||
}
|
||||
);
|
||||
useKey(
|
||||
"Shift",
|
||||
(e) => {
|
||||
console.log("shift", e.type);
|
||||
setShift(e.type == "keydown");
|
||||
},
|
||||
{ eventTypes: ["keydown", "keyup"], target: editableRef }
|
||||
);
|
||||
const handleChange = useCallback(
|
||||
(nextValue) => {
|
||||
setValue(nextValue);
|
||||
// serialize slate state to a markdown string
|
||||
const md = nextValue.map((v) => serialize(v)).join("\n");
|
||||
updateMarkdown(md);
|
||||
console.log("serialized", nextValue, md);
|
||||
|
||||
const nonParagraphs = nextValue.filter((v) => v.type != "paragraph");
|
||||
if (nonParagraphs.length == 0) {
|
||||
const nonEmptyPs = nextValue.filter((v) => !!v.children[0].text);
|
||||
// 全部是P
|
||||
const pureText = nonEmptyPs
|
||||
.map((v) => {
|
||||
return v.children.map(({ text }) => text).join("");
|
||||
})
|
||||
.join("\n");
|
||||
console.log("pure text", nonEmptyPs, pureText);
|
||||
updatePureText(pureText);
|
||||
} else {
|
||||
updatePureText("");
|
||||
}
|
||||
},
|
||||
[updateMarkdown, updatePureText]
|
||||
);
|
||||
|
||||
console.log("slate value", value);
|
||||
return (
|
||||
<Styled className="input" ref={editableRef}>
|
||||
<Slate
|
||||
editor={editor}
|
||||
plugins={plugins}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<Editable
|
||||
// plugins={plugins}
|
||||
renderElement={Element}
|
||||
placeholder={placeholder}
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</Slate>
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
const initialValue = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
text:
|
||||
'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. So that, when you start a line with "> " you get a blockquote that looks like this:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "block-quote",
|
||||
children: [{ text: "A wise quote." }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
text:
|
||||
'Order when you start a line with "## " you get a level-two heading, like this:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading-two",
|
||||
children: [{ text: "Try it out!" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
text:
|
||||
'Try it out for yourself! Try starting a new line with ">", "-", or "#"s.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export const emptyValue = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
text: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default initialValue;
|
||||
@@ -1,121 +0,0 @@
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
Range,
|
||||
Point,
|
||||
Element as SlateElement,
|
||||
} from "slate";
|
||||
const SHORTCUTS = {
|
||||
"-": "list-item",
|
||||
"+": "list-item",
|
||||
"~~": "del",
|
||||
_: "italic",
|
||||
__: "bold",
|
||||
">": "blockquote",
|
||||
"#": "heading-one",
|
||||
"##": "heading-two",
|
||||
"###": "heading-three",
|
||||
"####": "heading-four",
|
||||
"#####": "heading-five",
|
||||
"######": "heading-six",
|
||||
};
|
||||
const withShortcuts = (editor) => {
|
||||
const { deleteBackward, insertText } = editor;
|
||||
|
||||
editor.insertText = (text) => {
|
||||
const { selection } = editor;
|
||||
console.log("insert text", text, selection, Range.isCollapsed(selection));
|
||||
|
||||
if (text === " " && selection && Range.isCollapsed(selection)) {
|
||||
const { anchor } = selection;
|
||||
const block = Editor.above(editor, {
|
||||
match: (n) => Editor.isBlock(editor, n),
|
||||
});
|
||||
const path = block ? block[1] : [];
|
||||
const start = Editor.start(editor, path);
|
||||
const range = { anchor, focus: start };
|
||||
const beforeText = Editor.string(editor, range);
|
||||
const type = SHORTCUTS[beforeText];
|
||||
console.log(
|
||||
"insert text parsed",
|
||||
block,
|
||||
path,
|
||||
start,
|
||||
range,
|
||||
beforeText,
|
||||
type
|
||||
);
|
||||
if (type) {
|
||||
Transforms.select(editor, range);
|
||||
Transforms.delete(editor);
|
||||
const newProperties = {
|
||||
type,
|
||||
};
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
match: (n) => Editor.isBlock(editor, n),
|
||||
});
|
||||
|
||||
if (type === "list-item") {
|
||||
const list = {
|
||||
type: "bulleted-list",
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, list, {
|
||||
match: (n) =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === "list-item",
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
insertText(text);
|
||||
};
|
||||
|
||||
editor.deleteBackward = (...args) => {
|
||||
const { selection } = editor;
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const match = Editor.above(editor, {
|
||||
match: (n) => Editor.isBlock(editor, n),
|
||||
});
|
||||
|
||||
if (match) {
|
||||
const [block, path] = match;
|
||||
const start = Editor.start(editor, path);
|
||||
|
||||
if (
|
||||
!Editor.isEditor(block) &&
|
||||
SlateElement.isElement(block) &&
|
||||
block.type !== "paragraph" &&
|
||||
Point.equals(selection.anchor, start)
|
||||
) {
|
||||
const newProperties = {
|
||||
type: "paragraph",
|
||||
};
|
||||
Transforms.setNodes(editor, newProperties);
|
||||
|
||||
if (block.type === "list-item") {
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: (n) =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === "bulleted-list",
|
||||
split: true,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
deleteBackward(...args);
|
||||
}
|
||||
};
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export default withShortcuts;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
// import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useKey } from "rooks";
|
||||
// import { useKey } from "rooks";
|
||||
|
||||
import { removeReplyingMessage } from "../../../app/slices/message";
|
||||
import { useSendChannelMsgMutation } from "../../../app/services/channel";
|
||||
@@ -13,7 +13,8 @@ import UploadModal from "./UploadModal";
|
||||
import Replying from "./Replying";
|
||||
import Toolbar from "./Toolbar";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import RichInput from "../RichInput";
|
||||
// import RichInput from "../RichInput";
|
||||
import RichInput from "../RichEditor";
|
||||
const Types = {
|
||||
channel: "#",
|
||||
user: "@",
|
||||
@@ -62,7 +63,7 @@ export default function Send({
|
||||
setFiles([...evt.target.files]);
|
||||
};
|
||||
const handleSendMessage = () => {
|
||||
if (!msg || !id || sendingMessage) return;
|
||||
if (!markdown || !id || sendingMessage) return;
|
||||
console.log("send message", msg);
|
||||
const content_type = msg ? "text" : "markdown";
|
||||
const content = msg ? msg : markdown;
|
||||
|
||||
@@ -131,7 +131,12 @@ export default function ConfigSMTP() {
|
||||
</div>
|
||||
<div className="tip">
|
||||
<img src={iconQuestion} alt="question icon" />
|
||||
<a href="#" className="link">
|
||||
<a
|
||||
href="https://rustchat.com/doc/smtp-setting"
|
||||
target="_blank"
|
||||
className="link"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to set up SMTP?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -3,20 +3,26 @@ const StyledMenu = styled.ul`
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 12px;
|
||||
padding: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
||||
0px 10px 10px rgba(31, 41, 55, 0.04);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--br);
|
||||
.item {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
/* margin: 0 12px; */
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
/* transition: color 0.2s ease; */
|
||||
&:hover {
|
||||
background-color: #22ccee;
|
||||
color: #fff;
|
||||
}
|
||||
&.underline {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useContextMenu() {
|
||||
export default function useContextMenu(placement = "right-start") {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
// for tippy.js
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const handleContextMenuEvent = (evt) => {
|
||||
console.log("context menu event", evt);
|
||||
console.log("context menu event", evt, evt.currentTarget);
|
||||
evt.preventDefault();
|
||||
const { currentTarget, clientX, clientY } = evt;
|
||||
const { left, top, width, height } = currentTarget.getBoundingClientRect();
|
||||
let x, y;
|
||||
if (placement == "right-start") {
|
||||
x = clientX - (left + width);
|
||||
y = top + height - clientY;
|
||||
} else {
|
||||
x = clientX - left;
|
||||
y = top - clientY;
|
||||
}
|
||||
setOffset({ x, y });
|
||||
|
||||
setVisible(true);
|
||||
setPos({ x: evt.clientX, y: evt.clientY });
|
||||
console.log("offset", x, y);
|
||||
};
|
||||
const hideContextMenu = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
return {
|
||||
posX: pos.x,
|
||||
posY: pos.y,
|
||||
offset,
|
||||
visible,
|
||||
hideContextMenu,
|
||||
handleContextMenuEvent,
|
||||
|
||||
@@ -2,16 +2,28 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
// import { useDebounce} from "rooks";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
|
||||
import StyledLink from "./styled";
|
||||
import { toggleChannelSetting } from "../../../app/slices/ui";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import { getUnreadCount } from "../utils";
|
||||
|
||||
const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
|
||||
const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
// const getUnreadCountDebounced=useDebounce(getUnreadCount,300)
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
offset,
|
||||
handleContextMenuEvent,
|
||||
hideContextMenu,
|
||||
} = useContextMenu();
|
||||
const { channel, mids, messageData, readIndex, loginUid } = useSelector(
|
||||
(store) => {
|
||||
return {
|
||||
@@ -45,32 +57,76 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
|
||||
isActive: monitor.canDrop() && monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
const handleReadAll = () => {
|
||||
const lastMid = mids[mids.length - 1];
|
||||
console.log("last mid", mids, lastMid);
|
||||
if (lastMid) {
|
||||
const param = { groups: [{ gid: id, mid: lastMid }] };
|
||||
updateReadIndex(param);
|
||||
}
|
||||
};
|
||||
const { is_public, name } = channel;
|
||||
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
|
||||
return (
|
||||
<StyledLink
|
||||
data-cid={id}
|
||||
onContextMenu={(evt) => {
|
||||
contextMenuEventHandler(evt, id);
|
||||
}}
|
||||
ref={drop}
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={contextMenuVisible}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={id}
|
||||
className={`link ${isActive ? "drop_over" : ""}`}
|
||||
to={`/chat/channel/${id}`}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
underline: true,
|
||||
handler: handleReadAll,
|
||||
},
|
||||
{
|
||||
title: "Mute",
|
||||
},
|
||||
{
|
||||
title: "Notification Settings",
|
||||
underline: true,
|
||||
},
|
||||
is_public
|
||||
? null
|
||||
: {
|
||||
title: "Invite People",
|
||||
},
|
||||
{
|
||||
title: "Delete Channel",
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm.bind(null, id),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="name" title={name}>
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||
</div>
|
||||
<div className="icons">
|
||||
<i className="setting" onClick={handleChannelSetting}></i>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</StyledLink>
|
||||
<StyledLink
|
||||
data-cid={id}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
ref={drop}
|
||||
key={id}
|
||||
className={`link ${isActive ? "drop_over" : ""}`}
|
||||
to={`/chat/channel/${id}`}
|
||||
>
|
||||
<div className="name" title={name}>
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||
</div>
|
||||
<div className="icons">
|
||||
<i className="setting" onClick={handleChannelSetting}></i>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</StyledLink>
|
||||
</Tippy>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,76 +1,33 @@
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
|
||||
import DeleteConfirmModal from "../../../common/component/ChannelSetting/DeleteConfirmModal";
|
||||
import NavItem from "./NavItem";
|
||||
|
||||
export default function ChannelList({ setDropFiles }) {
|
||||
const [removeConfirmVisible, setRemoveConfirmVisible] = useState(false);
|
||||
const [currId, setCurrId] = useState(null);
|
||||
const { channelIds, channelData } = useSelector((store) => {
|
||||
const { channelIds } = useSelector((store) => {
|
||||
return { channelIds: store.channels.ids, channelData: store.channels.byId };
|
||||
});
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
posX,
|
||||
posY,
|
||||
hideContextMenu,
|
||||
handleContextMenuEvent,
|
||||
} = useContextMenu();
|
||||
const handleContextMenuClick = (evt, id) => {
|
||||
console.log("wtf", evt, id);
|
||||
setCurrId(id);
|
||||
handleContextMenuEvent(evt);
|
||||
|
||||
const setRemoveChannel = (cid = undefined) => {
|
||||
setCurrId(cid);
|
||||
};
|
||||
const toggleRemoveConfirm = () => {
|
||||
setRemoveConfirmVisible((prev) => !prev);
|
||||
};
|
||||
const { is_public } = channelData[currId] || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
{channelIds.map((cid) => {
|
||||
return (
|
||||
<NavItem
|
||||
contextMenuEventHandler={handleContextMenuClick}
|
||||
key={cid}
|
||||
toggleRemoveConfirm={setRemoveChannel}
|
||||
id={cid}
|
||||
setFiles={setDropFiles}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{contextMenuVisible ? (
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
posX={posX}
|
||||
posY={posY}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
underline: true,
|
||||
},
|
||||
{
|
||||
title: "Mute",
|
||||
},
|
||||
{
|
||||
title: "Notification Settings",
|
||||
underline: true,
|
||||
},
|
||||
is_public
|
||||
? null
|
||||
: {
|
||||
title: "Invite People",
|
||||
},
|
||||
{
|
||||
title: "Delete Channel",
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
{removeConfirmVisible && (
|
||||
<DeleteConfirmModal id={currId} closeModal={toggleRemoveConfirm} />
|
||||
{typeof currId !== "undefined" && (
|
||||
<DeleteConfirmModal id={currId} closeModal={setRemoveChannel} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
// import React from "react";
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
import { useSelector } from "react-redux";
|
||||
import { renderPreviewMessage, getUnreadCount } from "./utils";
|
||||
import Contact from "../../common/component/Contact";
|
||||
dayjs.extend(relativeTime);
|
||||
const NavItem = ({ uid, mid, unreads, setFiles }) => {
|
||||
const { currMsg, currUser } = useSelector((store) => {
|
||||
return {
|
||||
currUser: store.contacts.byId[uid],
|
||||
currMsg: store.message[mid],
|
||||
};
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const [{ isActive }, drop] = useDrop(() => ({
|
||||
accept: [NativeTypes.FILE],
|
||||
drop({ dataTransfer }) {
|
||||
if (dataTransfer.files.length) {
|
||||
// console.log(files, rest);
|
||||
setFiles([...dataTransfer.files]);
|
||||
navigate(`/chat/dm/${uid}`);
|
||||
// 重置
|
||||
setTimeout(() => {
|
||||
setFiles([]);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isActive: monitor.canDrop() && monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
if (!currUser || !currMsg) return null;
|
||||
return (
|
||||
<NavLink
|
||||
ref={drop}
|
||||
key={uid}
|
||||
className={`session ${isActive ? "drop_over" : ""}`}
|
||||
to={`/chat/dm/${uid}`}
|
||||
>
|
||||
<Contact compact interactive={false} className="avatar" uid={uid} />
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{currUser.name}</span>
|
||||
{currMsg && <time>{dayjs(currMsg.created_at).fromNow()}</time>}
|
||||
</div>
|
||||
|
||||
<div className="down">
|
||||
{renderPreviewMessage(currMsg)}
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
// mids: ChannelMsgData[channel_id],
|
||||
// messageData,
|
||||
// readIndex: readChannels[channel_id],
|
||||
// loginUid,
|
||||
export default function DMList({ uids, setDropFiles }) {
|
||||
const { userMessage, messageData, readUsers, loginUid } = useSelector(
|
||||
(store) => {
|
||||
return {
|
||||
loginUid: store.authData.uid,
|
||||
readUsers: store.footprint.readUsers,
|
||||
contactData: store.contacts.byId,
|
||||
userMessage: store.userMessage.byId,
|
||||
messageData: store.message,
|
||||
};
|
||||
}
|
||||
);
|
||||
const sessions = uids.map((uid) => {
|
||||
const mids = userMessage[uid];
|
||||
const lastMid = [...mids].pop();
|
||||
const readIndex = readUsers[uid];
|
||||
const unreads = getUnreadCount({ mids, readIndex, messageData, loginUid });
|
||||
|
||||
return { lastMid, unreads, uid };
|
||||
});
|
||||
|
||||
return sessions.map(({ lastMid, uid, unreads }) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={uid}
|
||||
uid={uid}
|
||||
mid={lastMid}
|
||||
unreads={unreads}
|
||||
setFiles={setDropFiles}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// import React from 'react'
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { removeUserSession } from "../../../app/slices/message.user";
|
||||
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
dayjs.extend(relativeTime);
|
||||
import { renderPreviewMessage } from "../utils";
|
||||
import Contact from "../../../common/component/Contact";
|
||||
const NavItem = ({ uid, mid, unreads, setFiles }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const { currMsg, currUser } = useSelector((store) => {
|
||||
return {
|
||||
currUser: store.contacts.byId[uid],
|
||||
currMsg: store.message[mid],
|
||||
};
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
offset,
|
||||
handleContextMenuEvent,
|
||||
hideContextMenu,
|
||||
} = useContextMenu();
|
||||
const [{ isActive }, drop] = useDrop(() => ({
|
||||
accept: [NativeTypes.FILE],
|
||||
drop({ dataTransfer }) {
|
||||
if (dataTransfer.files.length) {
|
||||
// console.log(files, rest);
|
||||
setFiles([...dataTransfer.files]);
|
||||
navigate(`/chat/dm/${uid}`);
|
||||
// 重置
|
||||
setTimeout(() => {
|
||||
setFiles([]);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isActive: monitor.canDrop() && monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
const handleReadAll = () => {
|
||||
const param = { users: [{ uid, mid }] };
|
||||
updateReadIndex(param);
|
||||
};
|
||||
const handleRemoveSession = () => {
|
||||
dispatch(removeUserSession(uid));
|
||||
};
|
||||
if (!currUser || !currMsg) return null;
|
||||
return (
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={contextMenuVisible}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={uid}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
handler: handleReadAll,
|
||||
},
|
||||
{
|
||||
title: "Hide Session",
|
||||
danger: true,
|
||||
handler: handleRemoveSession,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
ref={drop}
|
||||
key={uid}
|
||||
className={`session ${isActive ? "drop_over" : ""}`}
|
||||
to={`/chat/dm/${uid}`}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
>
|
||||
<Contact compact interactive={false} className="avatar" uid={uid} />
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{currUser.name}</span>
|
||||
{currMsg && <time>{dayjs(currMsg.created_at).fromNow()}</time>}
|
||||
</div>
|
||||
|
||||
<div className="down">
|
||||
{renderPreviewMessage(currMsg)}
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
||||
{unreads > 99 ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
</Tippy>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavItem;
|
||||
@@ -0,0 +1,41 @@
|
||||
// import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { getUnreadCount } from "../utils";
|
||||
|
||||
import NavItem from "./NavItem";
|
||||
export default function DMList({ uids, setDropFiles }) {
|
||||
const { userMessage, messageData, readUsers, loginUid } = useSelector(
|
||||
(store) => {
|
||||
return {
|
||||
loginUid: store.authData.uid,
|
||||
readUsers: store.footprint.readUsers,
|
||||
contactData: store.contacts.byId,
|
||||
userMessage: store.userMessage.byId,
|
||||
messageData: store.message,
|
||||
};
|
||||
}
|
||||
);
|
||||
const sessions = uids.map((uid) => {
|
||||
const mids = userMessage[uid];
|
||||
const lastMid = [...mids].pop();
|
||||
const readIndex = readUsers[uid];
|
||||
const unreads = getUnreadCount({ mids, readIndex, messageData, loginUid });
|
||||
|
||||
return { lastMid, unreads, uid };
|
||||
});
|
||||
|
||||
return sessions
|
||||
.sort((s1, s2) => s2.lastMid - s1.lastMid)
|
||||
.map(({ lastMid, uid, unreads }) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={uid}
|
||||
uid={uid}
|
||||
mid={lastMid}
|
||||
unreads={unreads}
|
||||
setFiles={setDropFiles}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user