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]);
|
await table.setItem(id + "", data.byId[id]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "removeUserSession":
|
||||||
|
{
|
||||||
|
const ids = Array.isArray(payload) ? payload : [payload];
|
||||||
|
await Promise.all(
|
||||||
|
ids.map(async (id) => {
|
||||||
|
await table.removeItem(id + "");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ const whiteList = [
|
|||||||
"getServer",
|
"getServer",
|
||||||
"getOpenid",
|
"getOpenid",
|
||||||
"getMetamaskNonce",
|
"getMetamaskNonce",
|
||||||
|
"renew",
|
||||||
];
|
];
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: BASE_URL,
|
baseUrl: BASE_URL,
|
||||||
prepareHeaders: (headers, { getState, endpoint }) => {
|
prepareHeaders: (headers, { getState, endpoint }) => {
|
||||||
console.log("req", endpoint);
|
console.log("req", endpoint);
|
||||||
const { token } = getState().authData;
|
const { token } = getState().authData;
|
||||||
const noHeaderList = [...whiteList, "renew"];
|
if (token && !whiteList.includes(endpoint)) {
|
||||||
if (token && !noHeaderList.includes(endpoint)) {
|
|
||||||
headers.set(tokenHeader, token);
|
headers.set(tokenHeader, token);
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
// import { batch } from "react-redux";
|
// import { batch } from "react-redux";
|
||||||
|
|
||||||
import { ContentTypes } from "../config";
|
import BASE_URL, { ContentTypes } from "../config";
|
||||||
import { updateReadChannels, updateReadUsers } from "../slices/footprint";
|
import { updateReadChannels, updateReadUsers } from "../slices/footprint";
|
||||||
import { onMessageSendStarted } from "./handlers";
|
import { onMessageSendStarted } from "./handlers";
|
||||||
|
|
||||||
@@ -38,6 +38,19 @@ export const messageApi = createApi({
|
|||||||
method: "DELETE",
|
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({
|
replyMessage: builder.mutation({
|
||||||
query: ({ reply_mid, content, type = "text" }) => ({
|
query: ({ reply_mid, content, type = "text" }) => ({
|
||||||
headers: {
|
headers: {
|
||||||
@@ -89,6 +102,7 @@ export const messageApi = createApi({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
useUploadImageMutation,
|
||||||
useEditMessageMutation,
|
useEditMessageMutation,
|
||||||
useReactMessageMutation,
|
useReactMessageMutation,
|
||||||
useReplyMessageMutation,
|
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 {
|
export const {
|
||||||
|
removeUserSession,
|
||||||
resetUserMsg,
|
resetUserMsg,
|
||||||
fullfillUserMsg,
|
fullfillUserMsg,
|
||||||
addUserMsg,
|
addUserMsg,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function DeleteConfirmModal({ id, closeModal }) {
|
|||||||
buttons={
|
buttons={
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
<Button onClick={closeModal}>Cancel</Button>
|
<Button onClick={closeModal.bind(null, undefined)}>Cancel</Button>
|
||||||
<Button onClick={handleDelete} className="danger">
|
<Button onClick={handleDelete} className="danger">
|
||||||
{isLoading ? "Deleting" : `Delete`}
|
{isLoading ? "Deleting" : `Delete`}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { useRef } from "react";
|
// import { useRef } from "react";
|
||||||
import styled from "styled-components";
|
// import styled from "styled-components";
|
||||||
import { useOutsideClick } from "rooks";
|
import StyledMenu from "./styled/Menu";
|
||||||
import StyledMenu from "./StyledMenu";
|
export default function ContextMenu({ items = [], hideMenu }) {
|
||||||
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);
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper ref={wrapperRef} x={posX} y={posY}>
|
<StyledMenu>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
const {
|
const {
|
||||||
@@ -44,6 +30,6 @@ export default function ContextMenu({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</StyledWrapper>
|
</StyledMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import useCopy from "../hook/useCopy";
|
|||||||
import { useLazyDeleteContactQuery } from "../../app/services/contact";
|
import { useLazyDeleteContactQuery } from "../../app/services/contact";
|
||||||
import { useRemoveMembersMutation } from "../../app/services/channel";
|
import { useRemoveMembersMutation } from "../../app/services/channel";
|
||||||
import Contact from "./Contact";
|
import Contact from "./Contact";
|
||||||
import StyledMenu from "./StyledMenu";
|
import StyledMenu from "./styled/Menu";
|
||||||
import InviteLink from "./InviteLink";
|
import InviteLink from "./InviteLink";
|
||||||
import moreIcon from "../../assets/icons/more.svg?url";
|
import moreIcon from "../../assets/icons/more.svg?url";
|
||||||
const StyledWrapper = styled.section`
|
const StyledWrapper = styled.section`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Tippy from "@tippyjs/react";
|
|||||||
import { hideAll } from "tippy.js";
|
import { hideAll } from "tippy.js";
|
||||||
// import toast from "react-hot-toast";
|
// import toast from "react-hot-toast";
|
||||||
import { addReplyingMessage } from "../../../app/slices/message";
|
import { addReplyingMessage } from "../../../app/slices/message";
|
||||||
import StyledMenu from "../StyledMenu";
|
import StyledMenu from "../styled/Menu";
|
||||||
import DeleteMessageConfirm from "./DeleteMessageConfirm";
|
import DeleteMessageConfirm from "./DeleteMessageConfirm";
|
||||||
import EmojiPicker from "./EmojiPicker";
|
import EmojiPicker from "./EmojiPicker";
|
||||||
import replyIcon from "../../../assets/icons/reply.svg?url";
|
import replyIcon from "../../../assets/icons/reply.svg?url";
|
||||||
@@ -108,7 +108,7 @@ export default function Commands({
|
|||||||
onShow={handleTippyVisible.bind(null, true)}
|
onShow={handleTippyVisible.bind(null, true)}
|
||||||
onHide={handleTippyVisible.bind(null, false)}
|
onHide={handleTippyVisible.bind(null, false)}
|
||||||
interactive
|
interactive
|
||||||
placement="right-start"
|
placement="left-start"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
content={
|
content={
|
||||||
<StyledMenu className="menu">
|
<StyledMenu className="menu">
|
||||||
|
|||||||
@@ -1,153 +1,70 @@
|
|||||||
// import React from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import showdown from "showdown";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
const Styled = styled.div`
|
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 {
|
p {
|
||||||
margin: 1em 0;
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #475467;
|
||||||
}
|
}
|
||||||
|
ul {
|
||||||
a {
|
list-style-type: disc;
|
||||||
color: #00213d;
|
|
||||||
}
|
}
|
||||||
|
i,
|
||||||
a:visited {
|
em {
|
||||||
color: #00213d;
|
/* padding: 0 2px; */
|
||||||
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 {
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
ul,
|
||||||
dd {
|
ol {
|
||||||
margin-bottom: 1.5em;
|
font-weight: 400;
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding: 0.1em;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: "Menlo", monospace;
|
line-height: 20px;
|
||||||
background-color: #f5f5f5;
|
color: #475467;
|
||||||
border: 1px solid #efefef;
|
list-style-position: inside;
|
||||||
}
|
}
|
||||||
|
strong {
|
||||||
pre {
|
font-weight: 700;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
h1,
|
||||||
pre code {
|
h2,
|
||||||
background-color: transparent;
|
h3 {
|
||||||
border-width: 0;
|
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 {
|
blockquote {
|
||||||
border-top: 1px solid #efefef;
|
display: flex;
|
||||||
border-bottom: 1px solid #ddd;
|
margin-bottom: 10px;
|
||||||
-webkit-box-shadow: 0 1px 3px 0 #eee;
|
color: #98a2b3;
|
||||||
-moz-box-shadow: 0 1px 3px 0 #eee;
|
opacity: 0.8;
|
||||||
-ms-box-shadow: 0 1px 3px 0 #eee;
|
box-shadow: inset 2px 0px 0px #a8b0bd;
|
||||||
box-shadow: 0 1px 3px 0 #eee;
|
font-weight: 400;
|
||||||
}
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
table {
|
padding: 16px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export default function MrakdownRender({ content }) {
|
export default function MrakdownRender({ content }) {
|
||||||
return (
|
const [html, setHtml] = useState("");
|
||||||
<Styled>
|
useEffect(() => {
|
||||||
<ReactMarkdown>{content}</ReactMarkdown>
|
if (content) {
|
||||||
</Styled>
|
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 {
|
p {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
/* line-height: 20px; */
|
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
color: #475467;
|
color: #475467;
|
||||||
/* margin-bottom: 10px; */
|
|
||||||
}
|
}
|
||||||
i {
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
/* i {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
/* del{
|
|
||||||
font-style: italic;
|
|
||||||
} */
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -24,12 +22,6 @@ const Styled = styled.div`
|
|||||||
color: #475467;
|
color: #475467;
|
||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
}
|
}
|
||||||
ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
/* list-style-type:; */
|
|
||||||
}
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -60,7 +52,7 @@ const Styled = styled.div`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
} */
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Styled;
|
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 { useState, useEffect, useRef } from "react";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
// import TextareaAutosize from "react-textarea-autosize";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useKey } from "rooks";
|
// import { useKey } from "rooks";
|
||||||
|
|
||||||
import { removeReplyingMessage } from "../../../app/slices/message";
|
import { removeReplyingMessage } from "../../../app/slices/message";
|
||||||
import { useSendChannelMsgMutation } from "../../../app/services/channel";
|
import { useSendChannelMsgMutation } from "../../../app/services/channel";
|
||||||
@@ -13,7 +13,8 @@ import UploadModal from "./UploadModal";
|
|||||||
import Replying from "./Replying";
|
import Replying from "./Replying";
|
||||||
import Toolbar from "./Toolbar";
|
import Toolbar from "./Toolbar";
|
||||||
import EmojiPicker from "./EmojiPicker";
|
import EmojiPicker from "./EmojiPicker";
|
||||||
import RichInput from "../RichInput";
|
// import RichInput from "../RichInput";
|
||||||
|
import RichInput from "../RichEditor";
|
||||||
const Types = {
|
const Types = {
|
||||||
channel: "#",
|
channel: "#",
|
||||||
user: "@",
|
user: "@",
|
||||||
@@ -62,7 +63,7 @@ export default function Send({
|
|||||||
setFiles([...evt.target.files]);
|
setFiles([...evt.target.files]);
|
||||||
};
|
};
|
||||||
const handleSendMessage = () => {
|
const handleSendMessage = () => {
|
||||||
if (!msg || !id || sendingMessage) return;
|
if (!markdown || !id || sendingMessage) return;
|
||||||
console.log("send message", msg);
|
console.log("send message", msg);
|
||||||
const content_type = msg ? "text" : "markdown";
|
const content_type = msg ? "text" : "markdown";
|
||||||
const content = msg ? msg : markdown;
|
const content = msg ? msg : markdown;
|
||||||
|
|||||||
@@ -131,7 +131,12 @@ export default function ConfigSMTP() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="tip">
|
<div className="tip">
|
||||||
<img src={iconQuestion} alt="question icon" />
|
<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?
|
How to set up SMTP?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ const StyledMenu = styled.ul`
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 4px 12px;
|
padding: 4px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
||||||
0px 10px 10px rgba(31, 41, 55, 0.04);
|
0px 10px 10px rgba(31, 41, 55, 0.04);
|
||||||
border-radius: 6px;
|
border-radius: var(--br);
|
||||||
.item {
|
.item {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 8px 0;
|
border-radius: 3px;
|
||||||
/* margin: 0 12px; */
|
padding: 8px;
|
||||||
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: #616161;
|
color: #616161;
|
||||||
|
/* transition: color 0.2s ease; */
|
||||||
|
&:hover {
|
||||||
|
background-color: #22ccee;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
&.underline {
|
&.underline {
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,32 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function useContextMenu() {
|
export default function useContextMenu(placement = "right-start") {
|
||||||
const [visible, setVisible] = useState(false);
|
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) => {
|
const handleContextMenuEvent = (evt) => {
|
||||||
console.log("context menu event", evt);
|
console.log("context menu event", evt, evt.currentTarget);
|
||||||
evt.preventDefault();
|
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);
|
setVisible(true);
|
||||||
setPos({ x: evt.clientX, y: evt.clientY });
|
console.log("offset", x, y);
|
||||||
};
|
};
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
posX: pos.x,
|
offset,
|
||||||
posY: pos.y,
|
|
||||||
visible,
|
visible,
|
||||||
hideContextMenu,
|
hideContextMenu,
|
||||||
handleContextMenuEvent,
|
handleContextMenuEvent,
|
||||||
|
|||||||
@@ -2,16 +2,28 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useDrop } from "react-dnd";
|
import { useDrop } from "react-dnd";
|
||||||
import { NativeTypes } from "react-dnd-html5-backend";
|
import { NativeTypes } from "react-dnd-html5-backend";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
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 { useDebounce} from "rooks";
|
||||||
|
import { useReadMessageMutation } from "../../../app/services/message";
|
||||||
|
|
||||||
import StyledLink from "./styled";
|
import StyledLink from "./styled";
|
||||||
import { toggleChannelSetting } from "../../../app/slices/ui";
|
import { toggleChannelSetting } from "../../../app/slices/ui";
|
||||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||||
import { getUnreadCount } from "../utils";
|
import { getUnreadCount } from "../utils";
|
||||||
|
|
||||||
const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
|
const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
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(
|
const { channel, mids, messageData, readIndex, loginUid } = useSelector(
|
||||||
(store) => {
|
(store) => {
|
||||||
return {
|
return {
|
||||||
@@ -45,32 +57,76 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
|
|||||||
isActive: monitor.canDrop() && monitor.isOver(),
|
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 { is_public, name } = channel;
|
||||||
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
|
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
|
||||||
return (
|
return (
|
||||||
<StyledLink
|
<Tippy
|
||||||
data-cid={id}
|
interactive
|
||||||
onContextMenu={(evt) => {
|
placement="right-start"
|
||||||
contextMenuEventHandler(evt, id);
|
offset={[offset.y, offset.x]}
|
||||||
}}
|
visible={contextMenuVisible}
|
||||||
ref={drop}
|
onClickOutside={hideContextMenu}
|
||||||
key={id}
|
key={id}
|
||||||
className={`link ${isActive ? "drop_over" : ""}`}
|
content={
|
||||||
to={`/chat/channel/${id}`}
|
<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}>
|
<StyledLink
|
||||||
<ChannelIcon personal={!is_public} />
|
data-cid={id}
|
||||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
onContextMenu={handleContextMenuEvent}
|
||||||
</div>
|
ref={drop}
|
||||||
<div className="icons">
|
key={id}
|
||||||
<i className="setting" onClick={handleChannelSetting}></i>
|
className={`link ${isActive ? "drop_over" : ""}`}
|
||||||
{unreads > 0 && (
|
to={`/chat/channel/${id}`}
|
||||||
<i className={`badge ${unreads > 99 ? "dot" : ""}`}>
|
>
|
||||||
{unreads > 99 ? null : unreads}
|
<div className="name" title={name}>
|
||||||
</i>
|
<ChannelIcon personal={!is_public} />
|
||||||
)}
|
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||||
</div>
|
</div>
|
||||||
</StyledLink>
|
<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 { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
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 DeleteConfirmModal from "../../../common/component/ChannelSetting/DeleteConfirmModal";
|
||||||
import NavItem from "./NavItem";
|
import NavItem from "./NavItem";
|
||||||
|
|
||||||
export default function ChannelList({ setDropFiles }) {
|
export default function ChannelList({ setDropFiles }) {
|
||||||
const [removeConfirmVisible, setRemoveConfirmVisible] = useState(false);
|
|
||||||
const [currId, setCurrId] = useState(null);
|
const [currId, setCurrId] = useState(null);
|
||||||
const { channelIds, channelData } = useSelector((store) => {
|
const { channelIds } = useSelector((store) => {
|
||||||
return { channelIds: store.channels.ids, channelData: store.channels.byId };
|
return { channelIds: store.channels.ids, channelData: store.channels.byId };
|
||||||
});
|
});
|
||||||
const {
|
|
||||||
visible: contextMenuVisible,
|
const setRemoveChannel = (cid = undefined) => {
|
||||||
posX,
|
setCurrId(cid);
|
||||||
posY,
|
|
||||||
hideContextMenu,
|
|
||||||
handleContextMenuEvent,
|
|
||||||
} = useContextMenu();
|
|
||||||
const handleContextMenuClick = (evt, id) => {
|
|
||||||
console.log("wtf", evt, id);
|
|
||||||
setCurrId(id);
|
|
||||||
handleContextMenuEvent(evt);
|
|
||||||
};
|
};
|
||||||
const toggleRemoveConfirm = () => {
|
|
||||||
setRemoveConfirmVisible((prev) => !prev);
|
|
||||||
};
|
|
||||||
const { is_public } = channelData[currId] || {};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{channelIds.map((cid) => {
|
{channelIds.map((cid) => {
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
contextMenuEventHandler={handleContextMenuClick}
|
|
||||||
key={cid}
|
key={cid}
|
||||||
|
toggleRemoveConfirm={setRemoveChannel}
|
||||||
id={cid}
|
id={cid}
|
||||||
setFiles={setDropFiles}
|
setFiles={setDropFiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{contextMenuVisible ? (
|
{typeof currId !== "undefined" && (
|
||||||
<ContextMenu
|
<DeleteConfirmModal id={currId} closeModal={setRemoveChannel} />
|
||||||
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} />
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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