refactor: lots updates

This commit is contained in:
zerosoul
2022-03-17 11:15:13 +08:00
parent 4bc9932d0f
commit f22c7a01cb
69 changed files with 4625 additions and 1640 deletions
+3 -2
View File
@@ -23,7 +23,7 @@ export default function ChannelModal({ personal = false, closeModal }) {
const [data, setData] = useState({
name: "",
dsecription: "",
members: [],
members: [loginUid],
is_public: !personal,
});
const { contacts, input, updateInput } = useFilteredUsers();
@@ -111,9 +111,10 @@ export default function ChannelModal({ personal = false, closeModal }) {
key={uid}
data-uid={uid}
className="user"
onClick={toggleCheckMember}
onClick={loginUid == uid ? null : toggleCheckMember}
>
<StyledCheckbox
disabled={loginUid == uid}
readOnly
checked={checked}
name="cb"
+1 -1
View File
@@ -12,7 +12,7 @@ const StyledWrapper = styled.div`
justify-content: flex-start;
gap: 8px;
padding: 8px;
border-radius: 4px;
border-radius: 8px;
user-select: none;
&.compact {
padding: 0;
+1 -1
View File
@@ -57,7 +57,7 @@ export default function ContactsModal({ closeModal }) {
useOutsideClick(wrapperRef, closeModal);
const handleSearch = (evt) => {
console.log("www");
// updateInput(evt.target.value);
updateInput(evt.target.value);
};
return (
+4 -10
View File
@@ -1,6 +1,8 @@
// import { useEffect } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import soundIcon from "../../assets/icons/sound.on.svg?url";
import micIcon from "../../assets/icons/mic.on.svg?url";
import Avatar from "./Avatar";
const StyledWrapper = styled.div`
background-color: #e5e5e5;
@@ -75,16 +77,8 @@ export default function CurrentUser() {
</div>
{/* {expand && ( */}
<div className="settings">
<img
src="https://static.nicegoodthings.com/project/rustchat/icon.speaker.svg"
className="icon"
alt="mic icon"
/>
<img
src="https://static.nicegoodthings.com/project/rustchat/icon.mic.svg"
className="icon"
alt="sound icon"
/>
<img src={soundIcon} className="icon" alt="mic icon" />
<img src={micIcon} className="icon" alt="sound icon" />
</div>
{/* )} */}
</StyledWrapper>
+27
View File
@@ -0,0 +1,27 @@
// import React from "react";
import styled from "styled-components";
const StyledDivider = styled.hr`
display: block;
position: relative;
border: 0;
border-top: 1px solid #e3e5e8;
margin: 25px 0;
&:before {
background: #fff;
padding: 2px 4px;
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
content: attr(data-content);
font-style: normal;
font-weight: 600;
font-size: 12px;
line-height: 18px;
color: #78787c;
}
`;
export default function Divider({ content }) {
if (!content) return null;
return <StyledDivider data-content={content}></StyledDivider>;
}
+11
View File
@@ -0,0 +1,11 @@
import { getEmojiDataFromNative, Emoji } from "emoji-mart";
import data from "emoji-mart/data/all.json";
export default function EmojiItem({ native = "", set = "twitter", size = 16 }) {
if (!native) return null;
const emojiData = getEmojiDataFromNative(native, "apple", data);
return (
<Emoji emoji={emojiData} set={set} skin={emojiData.skin || 1} size={size} />
);
}
+25
View File
@@ -0,0 +1,25 @@
// import React from 'react'
import { Picker } from "emoji-mart";
import "emoji-mart/css/emoji-mart.css";
import styled from "styled-components";
const StyledWrapper = styled.div`
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
.emoji-mart {
border: none;
border-radius: 12px;
}
`;
export default function EmojiPicker({ onSelect, ...rest }) {
return (
<StyledWrapper>
<Picker
// set="twitter"
showPreview={false}
showSkinTones={false}
onSelect={onSelect}
{...rest}
/>
</StyledWrapper>
);
}
+88
View File
@@ -0,0 +1,88 @@
import { useEffect } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { useLazyCreateInviteLinkQuery } from "../../app/services/server";
import Button from "./styled/Button";
import useCopy from "../hook/useCopy";
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
padding-bottom: 32px;
.tip {
font-weight: 500;
font-size: 14px;
color: #6b7280;
margin-bottom: 8px;
}
.link {
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
background: #ffffff;
border: 1px solid #e5e7eb;
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
border-radius: 4px;
padding: 3px 4px 3px 8px;
.content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 512px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #78787c;
}
}
.sub_tip {
margin-left: 4px;
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #616161;
margin-bottom: 20px;
}
`;
export default function InviteLink() {
const [copid, copy] = useCopy();
const {
inviteLink: { link },
loginUser,
} = useSelector((store) => {
return {
inviteLink: store.server.inviteLink,
loginUser: store.contacts.byId[store.authData.uid],
};
});
const [createLink, { isLoading }] = useLazyCreateInviteLinkQuery();
useEffect(() => {
if (!link && loginUser && loginUser.is_admin) {
createLink();
}
}, [link, loginUser]);
const handleNewLink = () => {
createLink();
};
if (!loginUser || !loginUser.is_admin) return null;
return (
<StyledWrapper>
<span className="tip">
Share this link to invite people to this server.
</span>
<div className="link">
<span title={link} className="content">
{link}
</span>
<Button onClick={copy.bind(null, link)} className="main">
{copid ? "Copied" : `Copy`}
</Button>
</div>
<span className="sub_tip">Invite link expires in 7 days.</span>
<Button className="ghost" disabled={isLoading} onClick={handleNewLink}>
{isLoading ? `Generating` : `Generate New Link`}
</Button>
</StyledWrapper>
);
}
@@ -0,0 +1,74 @@
import React from "react";
import { cx, css } from "@emotion/css";
export const Button = React.forwardRef(
({ className, active, reversed, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
className,
css`
cursor: pointer;
color: ${reversed
? active
? "white"
: "#aaa"
: active
? "black"
: "#ccc"};
`
)}
/>
)
);
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
"material-icons",
className,
css`
font-size: 18px;
vertical-align: text-bottom;
`
)}
/>
));
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
<div
{...props}
ref={ref}
className={cx(
className,
css`
& > * {
display: inline-block;
}
& > * + * {
margin-left: 15px;
}
`
)}
/>
));
export const Toolbar = React.forwardRef(({ className, ...props }, ref) => (
<Menu
{...props}
ref={ref}
className={cx(
className,
css`
position: relative;
padding: 5px 16px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
margin-bottom: 20px;
`
)}
/>
));
@@ -0,0 +1,31 @@
import Editor from "rich-markdown-editor";
import styled from "styled-components";
const Styled = styled.div`
padding-top: 8px;
.mde {
padding: 4px;
height: 150px;
> div {
height: 100%;
.ProseMirror {
overflow: scroll;
height: 100%;
max-height: 300px;
padding: 5px 28px;
}
}
}
`;
export default function MarkdownEditer({ value, updateValue }) {
return (
<Styled>
<Editor
defaultValue={value}
onChange={updateValue}
autoFocus={true}
placeholder="Type '/' to insert..."
className="mde"
></Editor>
</Styled>
);
}
@@ -0,0 +1,29 @@
import * as React from "react";
import ReactMde from "react-mde";
import * as Showdown from "showdown";
import "react-mde/lib/styles/css/react-mde-all.css";
const converter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
});
export default function MarkdownEditer() {
const [value, setValue] = React.useState("**Hello world!!!**");
const [selectedTab, setSelectedTab] = React.useState("write");
return (
<div className="container">
<ReactMde
value={value}
onChange={setValue}
selectedTab={selectedTab}
onTabChange={setSelectedTab}
generateMarkdownPreview={(markdown) =>
Promise.resolve(converter.makeHtml(markdown))
}
/>
</div>
);
}
@@ -0,0 +1,280 @@
import { useCallback, useMemo, useState } from "react";
import isHotkey from "is-hotkey";
import { Editable, withReact, useSlate, Slate } from "slate-react";
import {
Editor,
Transforms,
createEditor,
Element as SlateElement,
} from "slate";
import { withHistory } from "slate-history";
import Styled from "./styled";
import { Button, Icon, Toolbar } from "./components";
const HOTKEYS = {
"mod+b": "bold",
"mod+i": "italic",
"mod+u": "underline",
"mod+`": "code",
};
const LIST_TYPES = ["numbered-list", "bulleted-list"];
const TEXT_ALIGN_TYPES = ["left", "center", "right", "justify"];
const MarkdownEditor = () => {
const [value, setValue] = useState(initialValue);
const renderElement = useCallback((props) => <Element {...props} />, []);
const renderLeaf = useCallback((props) => <Leaf {...props} />, []);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
return (
<Styled>
<Slate
editor={editor}
value={value}
onChange={(value) => setValue(value)}
>
<Toolbar>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
{/* <BlockButton format="heading-one" icon="looks_one" />
<BlockButton format="heading-two" icon="looks_two" /> */}
<BlockButton format="block-quote" icon="format_quote" />
<BlockButton format="numbered-list" icon="format_list_numbered" />
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
<BlockButton format="left" icon="format_align_left" />
<BlockButton format="center" icon="format_align_center" />
<BlockButton format="right" icon="format_align_right" />
<BlockButton format="justify" icon="format_align_justify" />
</Toolbar>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={(event) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event)) {
event.preventDefault();
const mark = HOTKEYS[hotkey];
toggleMark(editor, mark);
}
}
}}
/>
</Slate>
</Styled>
);
};
const toggleBlock = (editor, format) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
);
const isList = LIST_TYPES.includes(format);
Transforms.unwrapNodes(editor, {
match: (n) =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
split: true,
});
let newProperties;
if (TEXT_ALIGN_TYPES.includes(format)) {
newProperties = {
align: isActive ? undefined : format,
};
} else {
newProperties = {
type: isActive ? "paragraph" : isList ? "list-item" : format,
};
}
Transforms.setNodes < SlateElement > (editor, newProperties);
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
};
const isBlockActive = (editor, format, blockType = "type") => {
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: (n) =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
})
);
return !!match;
};
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
const Element = ({ attributes, children, element }) => {
const style = { textAlign: element.align };
switch (element.type) {
case "block-quote":
return (
<blockquote style={style} {...attributes}>
{children}
</blockquote>
);
case "bulleted-list":
return (
<ul style={style} {...attributes}>
{children}
</ul>
);
case "heading-one":
return (
<h1 style={style} {...attributes}>
{children}
</h1>
);
case "heading-two":
return (
<h2 style={style} {...attributes}>
{children}
</h2>
);
case "list-item":
return (
<li style={style} {...attributes}>
{children}
</li>
);
case "numbered-list":
return (
<ol style={style} {...attributes}>
{children}
</ol>
);
default:
return (
<p style={style} {...attributes}>
{children}
</p>
);
}
};
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
if (leaf.code) {
children = <code>{children}</code>;
}
if (leaf.italic) {
children = <em>{children}</em>;
}
if (leaf.underline) {
children = <u>{children}</u>;
}
return <span {...attributes}>{children}</span>;
};
const BlockButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
)}
onMouseDown={(event) => {
event.preventDefault();
toggleBlock(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
const MarkButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleMark(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
const initialValue = [
{
type: "paragraph",
children: [
{ text: "This is editable " },
{ text: "rich", bold: true },
{ text: " text, " },
{ text: "much", italic: true },
{ text: " better than a " },
{ text: "<textarea>", code: true },
{ text: "!" },
],
},
{
type: "paragraph",
children: [
{
text:
"Since it's rich text, you can do things like turn a selection of text ",
},
{ text: "bold", bold: true },
{
text:
", or add a semantically rendered block quote in the middle of the page, like this:",
},
],
},
{
type: "block-quote",
children: [{ text: "A wise quote." }],
},
{
type: "paragraph",
align: "center",
children: [{ text: "Try it out for yourself!" }],
},
];
export default MarkdownEditor;
@@ -0,0 +1,62 @@
import styled from "styled-components";
const Styled = styled.div`
* {
line-height: 1.4;
}
/* ul,
ol {
list-style-position: inside;
display: list-item;
padding-left: 10px;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
} */
strong {
font-weight: bold;
}
p {
margin: 0;
margin-bottom: 12px;
}
pre {
padding: 10px;
background-color: #eee;
white-space: pre-wrap;
}
:not(pre) > code {
font-family: monospace;
background-color: #eee;
padding: 3px;
}
code {
background-color: #ccc;
}
img {
max-width: 100%;
max-height: 20em;
}
blockquote {
border-left: 2px solid #ddd;
margin-left: 0;
margin-right: 0;
padding-left: 10px;
color: #aaa;
font-style: italic;
}
blockquote[dir="rtl"] {
border-left: none;
padding-left: 0;
padding-right: 10px;
border-right: 2px solid #ddd;
}
`;
export default Styled;
+31 -31
View File
@@ -1,72 +1,73 @@
// import { Picker } from "emoji-mart";
// import "emoji-mart/css/emoji-mart.css";
// import Picker from "../EmojiPicker";
import { useRef } from "react";
import styled from "styled-components";
import { useOutsideClick } from "rooks";
import { useSelector } from "react-redux";
import { useReactMessageMutation } from "../../../app/services/message";
import { Emojis } from "../../../app/config";
import Emoji from "../Emoji";
const StyledPicker = styled.div`
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
position: absolute;
left: -10px;
top: 0;
transform: translateX(-100%);
background-color: #fff;
padding: 5px;
.emojis {
display: flex;
gap: 4px;
padding: 4px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
background: #ffffff;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
&.reacting {
opacity: 0.6;
}
.emoji {
cursor: pointer;
border-radius: 4px;
border-radius: 8px;
padding: 4px;
font-size: 30px;
&:hover,
&.reacted {
background-color: #f3f4f6;
background-color: #f5f6f7;
}
> * {
display: flex;
}
}
}
`;
const emojis = {
["U+1F44D"]: "👍",
["U+1F44C"]: "👌",
["U+2764"]: "❤️",
};
export default function EmojiPicker({ mid, hidePicker }) {
const wrapperRef = useRef(null);
const [reactMessage, { isLoading }] = useReactMessageMutation();
const { reactionData, currUid } = useSelector((store) => {
return {
reactionData: store.reactionMessage[mid],
reactionData: store.reactionMessage[mid] || {},
currUid: store.authData.uid,
};
});
useOutsideClick(wrapperRef, hidePicker);
const handleReact = (action) => {
console.log("react", action);
reactMessage({ mid, action });
const handleReact = (emoji) => {
console.log("react", emoji);
reactMessage({ mid, action: emoji });
hidePicker();
};
return (
<StyledPicker ref={wrapperRef}>
{/* <Picker
onSelect={handleReact}
className={`picker ${isLoading ? "reacting" : ""}`}
/> */}
<ul className={`emojis ${isLoading ? "reacting" : ""}`}>
{Object.entries(emojis).map(([key, emoji]) => {
{Emojis.map((emoji) => {
let reacted =
reactionData &&
reactionData[key] &&
reactionData[key].includes(currUid);
reactionData[emoji] &&
reactionData[emoji].findIndex((id) => id == currUid) > -1;
return (
<li
className={`emoji ${reacted ? "reacted" : ""}`}
key={key}
onClick={handleReact.bind(null, key)}
key={emoji}
onClick={handleReact.bind(null, emoji)}
>
{emoji}
<Emoji native={emoji} size={24} />
</li>
);
})}
@@ -74,4 +75,3 @@ export default function EmojiPicker({ mid, hidePicker }) {
</StyledPicker>
);
}
export { emojis };
+94 -29
View File
@@ -1,54 +1,119 @@
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { useState } from "react";
import { useSelector } from "react-redux";
import styled from "styled-components";
import { emojis } from "./EmojiPicker";
import Emoji from "../Emoji";
import EmojiPicker from "./EmojiPicker";
import { useReactMessageMutation } from "../../../app/services/message";
// import { Emojis } from "../../../app/config";
import addEmojiIcon from "../../../assets/icons/add.emoji.svg?url";
const StyledWrapper = styled.span`
z-index: 99;
position: relative;
margin-top: 8px;
margin-bottom: 4px;
display: flex;
gap: 8px;
font-size: 16px;
align-items: center;
gap: 4px;
width: fit-content;
/* align-items: center; */
.reaction {
cursor: pointer;
background-color: #ecfdff;
border-radius: 6px;
position: relative;
display: flex;
align-items: center;
gap: 6px;
em {
gap: 4px;
padding: 4px;
> .emoji {
> * {
display: flex;
}
}
&:hover {
background-color: #cff9fe;
}
&.reacted {
border: 1px solid #06aed4;
background-color: #a5f0fc;
}
> .count {
font-weight: 400;
font-size: 12px;
color: #999;
line-height: 16px;
color: #06aed4;
}
}
> .add {
visibility: hidden;
width: 24px;
height: 24px;
background-color: #ecfdff;
border-radius: 6px;
border: none;
background-image: url(${addEmojiIcon});
background-size: 16px;
background-repeat: no-repeat;
background-position: center;
&:hover {
background-color: #cff9fe;
}
}
.picker {
position: absolute;
right: 0;
top: 0;
transform: translateX(105%);
}
&:hover > .add {
visibility: visible;
}
`;
export default function Reaction({ reactions = null }) {
// const {
// messageData,
// reactionMessageData,
// contactsData,
// loginedUser,
// } = useSelector((store) => {
// return {
// reactionMessageData: store.reactionMessage,
// messageData: store.message,
// contactsData: store.contacts.byId,
// loginedUser: store.authData.user,
// };
// });
if (!reactions) return null;
export default function Reaction({ mid, reactions = null }) {
const [pickerVisible, setPickerVisible] = useState(false);
const [reactWithEmoji] = useReactMessageMutation();
const { currUid } = useSelector((store) => {
return {
currUid: store.authData.uid,
};
});
const handleReact = (emoji) => {
reactWithEmoji({ mid, action: emoji });
};
const togglePickerVisible = (wtf) => {
console.log("clicked", wtf);
setPickerVisible((prev) => !prev);
};
console.log("curr reactions", reactions);
if (!reactions || Object.entries(reactions).length == 0) return null;
return (
<StyledWrapper className="reactions">
{Object.entries(reactions).map(([reaction, uids]) => {
const reacted = uids.findIndex((id) => id == currUid) > -1;
return uids.length > 0 ? (
<i
className="reaction"
<span
onClick={handleReact.bind(null, reaction)}
className={`reaction ${reacted ? "reacted" : ""}`}
// data-count={count > 1 ? count : ""}
key={reaction}
>
{emojis[reaction]}
<i className="emoji">
<Emoji native={reaction} />
</i>
{uids.length > 1 ? <em>{`+${uids.length}`} </em> : null}
</i>
{uids.length > 1 ? (
<em className="count">{`${uids.length}`} </em>
) : null}
</span>
) : null;
})}
<button onClick={togglePickerVisible} className="add"></button>
{pickerVisible && (
<div className="picker">
<EmojiPicker mid={mid} hidePicker={togglePickerVisible} />
</div>
)}
</StyledWrapper>
);
}
+12 -4
View File
@@ -8,12 +8,14 @@ import Reply from "./Reply";
import Profile from "../Profile";
import Avatar from "../Avatar";
import { readMessage } from "../../../app/slices/message";
import { useReadMessageMutation } from "../../../app/services/message";
import StyledWrapper from "./styled";
import Commands from "./Commands";
import EditMessage from "./EditMessage";
import renderContent from "./renderContent";
function Message({ contextId = 0, mid = "" }) {
function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
const [updateReadIndex] = useReadMessageMutation();
const [myRef, inView] = useInViewRef();
const [edit, setEdit] = useState(false);
const [emojiPopVisible, setEmojiPopVisible] = useState(false);
@@ -47,10 +49,15 @@ function Message({ contextId = 0, mid = "" }) {
// console.log("message", mid, messageData[mid]);
useEffect(() => {
if (inView && !message.read) {
if (inView && !read) {
disptach(readMessage(mid));
const data =
context == "user"
? { users: [{ uid: +contextId, mid }] }
: { groups: [{ gid: +contextId, mid }] };
updateReadIndex(data);
}
}, [mid, message, inView]);
}, [mid, read, inView]);
if (!message) return null;
const {
reply_mid,
@@ -65,6 +72,7 @@ function Message({ contextId = 0, mid = "" }) {
const currUser = contactsData[fromUid] || {};
return (
<StyledWrapper
data-mid={mid}
ref={myRef}
className={`message ${menuVisible ? "menu" : ""} ${
inView ? "in_view" : ""
@@ -85,7 +93,6 @@ function Message({ contextId = 0, mid = "" }) {
<div className="up">
<span className="name">{currUser.name}</span>
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
{reactions && <Reaction reactions={reactions} />}
</div>
<div className={`down ${sending ? "sending" : ""}`}>
{edit ? (
@@ -97,6 +104,7 @@ function Message({ contextId = 0, mid = "" }) {
) : (
renderContent(content_type, content, edited)
)}
{reactions && <Reaction mid={mid} reactions={reactions} />}
</div>
</div>
{!edit && (
+12 -1
View File
@@ -1,6 +1,7 @@
import Linkify from "react-linkify";
import dayjs from "dayjs";
import BASE_URL from "../../../app/config";
import MrakdownRender from "../MrakdownRender";
const renderContent = (type, content, edited = false) => {
let ctn = null;
switch (type) {
@@ -9,7 +10,12 @@ const renderContent = (type, content, edited = false) => {
<>
<Linkify
componentDecorator={(decoratedHref, decoratedText, key) => (
<a target="blank" href={decoratedHref} key={key}>
<a
target="_blank"
href={decoratedHref}
key={key}
rel="noreferrer"
>
{decoratedText}
</a>
)}
@@ -27,6 +33,11 @@ const renderContent = (type, content, edited = false) => {
</>
);
break;
case "text/markdown":
{
ctn = <MrakdownRender content={content} />;
}
break;
case "image/png":
case "image/jpeg":
ctn = (
+1 -1
View File
@@ -4,7 +4,7 @@ const StyledMsg = styled.div`
display: flex;
align-items: flex-start;
gap: 16px;
padding: 4px;
padding: 4px 8px;
margin: 8px 0;
border-radius: 8px;
content-visibility: auto;
+154
View File
@@ -0,0 +1,154 @@
// import React from 'react'
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
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;
}
a {
color: #00213d;
}
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 {
font-style: italic;
}
dd {
margin-bottom: 1.5em;
}
li {
line-height: 1.5em;
}
code {
padding: 0.1em;
font-size: 14px;
font-family: "Menlo", monospace;
background-color: #f5f5f5;
border: 1px solid #efefef;
}
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;
}
pre code {
background-color: transparent;
border-width: 0;
}
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;
}
`;
export default function MrakdownRender({ content }) {
return (
<Styled>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</Styled>
);
}
+8 -7
View File
@@ -1,8 +1,9 @@
import { useState } from "react";
import styled from "styled-components";
import { MdSearch, MdAdd, MdMail } from "react-icons/md";
import { useSelector } from "react-redux";
import searchIcon from "../../assets/icons/search.svg?url";
import addIcon from "../../assets/icons/add.svg?url";
import mailIcon from "../../assets/icons/mail.svg?url";
import ChannelIcon from "./ChannelIcon";
import ChannelModal from "./ChannelModal";
import ContactsModal from "./ContactsModal";
@@ -98,14 +99,14 @@ export default function Search() {
<ContactsModal closeModal={toggleContactsModalVisible} />
)}
<div className="search">
<MdSearch size={20} color="#A1A1AA" />
<img src={searchIcon} />
<input placeholder="Search..." className="input" />
</div>
<MdAdd
<img
src={addIcon}
alt="add icon"
className="add"
onClick={togglePopupVisible}
size={24}
color="#A1A1AA"
/>
{popupVisible && (
<ul className="popup">
@@ -126,7 +127,7 @@ export default function Search() {
New Private Channel
</li>
<li className="item" onClick={toggleContactsModalVisible}>
<MdMail size={20} color="#616161" />
<img src={mailIcon} alt="icon mail" />
New Message
</li>
</ul>
+2 -7
View File
@@ -1,6 +1,5 @@
import { useState } from "react";
import { Picker } from "emoji-mart";
import "emoji-mart/css/emoji-mart.css";
import Picker from "../EmojiPicker";
export default function EmojiPicker({ selectEmoji }) {
const [emojiPickerVisible, setEmojiPickerVisible] = useState(false);
@@ -18,11 +17,7 @@ export default function EmojiPicker({ selectEmoji }) {
</button>
{emojiPickerVisible && (
<div className="picker">
<Picker
onSelect={handleSelect}
showPreview={false}
showSkinTones={false}
/>
<Picker onSelect={handleSelect} />
</div>
)}
</>
+111
View File
@@ -0,0 +1,111 @@
// import React from 'react'
import { RiMarkdownLine, RiMarkdownFill } from "react-icons/ri";
import styled from "styled-components";
import Button from "../styled/Button";
import EmojiPicker from "./EmojiPicker";
import addIcon from "../../../assets/icons/add.svg?url";
const Styled = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 5px;
.left {
display: flex;
align-items: center;
gap: 8px;
.md {
cursor: pointer;
}
.add {
cursor: pointer;
position: relative;
width: 16px;
height: 16px;
input {
opacity: 0;
cursor: pointer;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.emoji {
position: relative;
.toggle {
font-size: 22px;
border: none;
background: none;
}
.picker {
position: absolute;
top: -20px;
right: -15px;
transform: translateY(-100%);
}
}
}
.right {
display: flex;
align-items: center;
gap: 8px;
.send {
/* font-size: 16px;
font-weight: bold;
background: none; */
}
}
.divider {
width: 1px;
height: 16px;
background-color: #ccc;
margin: 0 4px;
}
`;
export default function Toolbar({
contentType = "text",
updateContentType,
handleUpload,
selectEmoji,
handleSend,
}) {
const toggleMarkdown = () => {
updateContentType(contentType == "markdown" ? "text" : "markdown");
};
return (
<Styled>
<div className="left">
<div className="add">
<img src={addIcon} />
<input
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</div>
<div className="divider"></div>
<div className="emoji">
<EmojiPicker selectEmoji={selectEmoji} />
</div>
<div className="md" onClick={toggleMarkdown}>
{contentType == "markdown" ? (
<RiMarkdownFill size={24} />
) : (
<RiMarkdownLine size={24} />
)}
</div>
</div>
<div className="right">
{contentType == "markdown" && (
<Button className="send main" onClick={handleSend}>
Send
</Button>
)}
</div>
</Styled>
);
}
+46 -34
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { MdAdd } from "react-icons/md";
import TextareaAutosize from "react-textarea-autosize";
import { useDispatch, useSelector } from "react-redux";
import { useKey } from "rooks";
@@ -11,9 +10,9 @@ import { useReplyMessageMutation } from "../../../app/services/message";
import StyledSend from "./styled";
import useFiles from "./useFiles";
import UploadModal from "./UploadModal";
import EmojiPicker from "./EmojiPicker";
import Replying from "./Replying";
import Toolbar from "./Toolbar";
import MarkdownEditor from "../MarkdownEditer";
const Types = {
channel: "#",
user: "@",
@@ -25,11 +24,13 @@ export default function Send({
id = "",
dragFiles = [],
}) {
const [contentType, setContentType] = useState("text");
const [replyMessage] = useReplyMessageMutation();
const { files, setFiles, resetFiles } = useFiles([]);
const inputRef = useRef();
const [shift, setShift] = useState(false);
const [enter, setEnter] = useState(false);
const [markdown, setMarkdown] = useState("");
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
// 谁发的
@@ -75,7 +76,9 @@ export default function Send({
setMsg((prev) => `${prev}${emoji}`);
};
useEffect(() => {
inputRef.current.focus();
if (inputRef) {
inputRef.current.focus();
}
}, [msg, replying_mid]);
const handleUpload = (evt) => {
setFiles([...evt.target.files]);
@@ -97,43 +100,52 @@ export default function Send({
}
setMsg("");
};
const sendMarkdown = () => {
console.log("markdown", markdown, markdown.endsWith("\\"));
sendMessage({
id,
content: markdown,
from_uid,
type: "markdown",
});
setMarkdown("");
};
return (
<>
<StyledSend className={`send ${replying_mid ? "reply" : ""}`}>
{replying_mid && <Replying mid={replying_mid} id={id} />}
<div className="addon">
<MdAdd size={20} color="#78787C" />
<input
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</div>
<div className="input">
<TextareaAutosize
autoFocus
onFocus={(e) =>
e.currentTarget.setSelectionRange(
e.currentTarget.value.length,
e.currentTarget.value.length
)
}
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`${Types[type]}${name} 发消息`}
/>
</div>
<div className="emoji">
<EmojiPicker selectEmoji={selectEmoji} />
{contentType == "markdown" ? (
<MarkdownEditor value={markdown} updateValue={setMarkdown} />
) : (
<TextareaAutosize
autoFocus
onFocus={(e) =>
e.currentTarget.setSelectionRange(
e.currentTarget.value.length,
e.currentTarget.value.length
)
}
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`Send to ${Types[type]}${name}`}
/>
)}
</div>
<Toolbar
handleSend={sendMarkdown}
contentType={contentType}
updateContentType={setContentType}
selectEmoji={selectEmoji}
handleUpload={handleUpload}
/>
</StyledSend>
{files.length !== 0 && (
<UploadModal
+4 -29
View File
@@ -10,27 +10,15 @@ const StyledSend = styled.div`
width: calc(100% - 32px);
min-height: 54px;
display: flex;
align-items: center;
gap: 18px;
flex-direction: column;
align-items: flex-start;
gap: 5px;
padding: 4px 18px;
/* margin: 0 16px; */
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.addon {
cursor: pointer;
position: relative;
input {
opacity: 0;
cursor: pointer;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.input {
width: 100%;
position: relative;
@@ -61,20 +49,7 @@ const StyledSend = styled.div`
transform: translateY(-50%);
}
}
.emoji {
position: relative;
.toggle {
font-size: 22px;
border: none;
background: none;
}
.picker {
position: absolute;
top: -20px;
right: -15px;
transform: translateY(-100%);
}
}
.reply {
display: flex;
justify-content: space-between;
@@ -1,7 +1,7 @@
// import React from 'react'
import styled from "styled-components";
import Modal from "./Modal";
import backIcon from "../../assets/icons/arrow.left.svg?url";
const StyledWrapper = styled.div`
width: 100%;
height: 100%;
@@ -18,7 +18,7 @@ const StyledWrapper = styled.div`
color: #1c1c1e;
margin-bottom: 32px;
padding-left: 24px;
background: url(https://static.nicegoodthings.com/project/rustchat/icon.arrow.left.svg);
background: url(${backIcon});
background-size: 16px;
background-repeat: no-repeat;
background-position: left;
+6 -1
View File
@@ -5,7 +5,7 @@ const StyledButton = styled.button`
background: none;
border: 1px solid #e5e7eb;
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
border-radius: 4px;
border-radius: var(--br, 4px);
font-weight: 500;
color: #374151;
&.main {
@@ -18,6 +18,11 @@ const StyledButton = styled.button`
background: #ef4444;
color: #fff;
}
&.ghost {
border-color: #1fe1f9;
background: none;
color: #1fe1f9;
}
`;
export default StyledButton;
+6 -1
View File
@@ -6,7 +6,6 @@ const Styled = styled.input`
appearance: none;
/* Not removed via appearance */
margin: 0;
/* color: #1fe1f9; */
width: 20px;
height: 20px;
border: 1px solid #d0d5dd;
@@ -30,6 +29,12 @@ const Styled = styled.input`
transform: scale(1);
}
}
&:disabled {
border-color: #ccc;
&::before {
box-shadow: inset 10px 10px #ccc;
}
}
`;
export default function StyledCheckbox(props) {
return <Styled {...props} type="checkbox" />;