feat: markdown auto format

This commit is contained in:
zerosoul
2022-03-25 09:40:40 +08:00
parent a9e06dcc7d
commit bdaf27f224
37 changed files with 1750 additions and 1032 deletions
@@ -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;
}
+2 -2
View File
@@ -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;
+15 -1
View File
@@ -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,
+10
View File
@@ -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>
+6 -20
View File
@@ -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>
);
}
+1 -1
View File
@@ -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`
+2 -2
View File
@@ -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">
+54 -137
View File
@@ -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,
},
]);
+180
View File
@@ -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 }],
},
},
};
+205
View File
@@ -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,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>&gt;</htext> followed by <htext code>space</htext> to
create a block quote.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>```</htext> to create a code block.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>---</htext> to create a horizontal rule.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>#</htext> followed by <htext code>space</htext> to
create an H1 heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>##</htext> followed by <htext code>space</htext> to
create an H2 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>###</htext> followed by <htext code>space</htext> to
create an H3 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>####</htext> followed by <htext code>space</htext> to
create an H4 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>#####</htext> followed by <htext code>space</htext>{" "}
to create an H5 sub-heading.
</hlic>
</hli>
<hli>
<hlic>
Type <htext code>######</htext> followed by <htext code>space</htext>{" "}
to create an H6 sub-heading.
</hlic>
</hli>
</hul>
</>
);
const pasteHtml = (
<>
<hh1>🍪 Deserialize HTML</hh1>
<hp>
By default, pasting content into a Slate editor will use the clipboard's{" "}
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
sometimes you want users to be able to paste in content and have it
maintain its formatting. To do this, your editor needs to handle{" "}
<htext code>'text/html'</htext> data.
</hp>
<hp>This is an example of doing exactly that!</hp>
<hp>
Try it out for yourself! Copy and paste some rendered HTML rich text
content (not the source code) from another site into this editor and it's
formatting should be preserved.
</hp>
</>
);
const pasteMd = (
<>
<hh1>🍩 Deserialize Markdown</hh1>
<hp>
By default, pasting content into a Slate editor will use the clipboard's{" "}
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
sometimes you want users to be able to paste in content and have it
maintain its formatting. To do this, your editor needs to handle{" "}
<htext code>'text/html'</htext> data.
</hp>
<hp>This is an example of doing exactly that!</hp>
<hp>
Try it out for yourself! Copy and paste Markdown content from{" "}
<ha url="https://markdown-it.github.io/">
https://markdown-it.github.io/
</ha>
</hp>
</>
);
const basicMarks = [
<hp key="d">
The basic marks consist of text formatting such as bold, italic, underline,
strikethrough, subscript, superscript, and code.
</hp>,
];
const basicElements = [
<hblockquote key="3">Blockquote</hblockquote>,
<hcodeblock key="4" lang="javascript">
<hcodeline>const a = 'Hello';</hcodeline>
<hcodeline>const b = 'World';</hcodeline>
</hcodeblock>,
];
const playground = [...basicMarks, ...basicElements];
export const VALUES = {
playground,
};
-33
View File
@@ -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;
-118
View File
@@ -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;
+5 -4
View File
@@ -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;
+6 -1
View File
@@ -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;
}
+18 -6
View File
@@ -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,
+79 -23
View File
@@ -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>
);
};
+9 -52
View File
@@ -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} />
)}
</>
);
-99
View File
@@ -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}
/>
);
});
}
+111
View File
@@ -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;
+41
View File
@@ -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}
/>
);
});
}
+219 -345
View File
File diff suppressed because it is too large Load Diff