refactor: lots updates
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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" />;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { batch } from "react-redux";
|
||||
import {
|
||||
addChannelMsg,
|
||||
removeChannelMsg,
|
||||
} from "../../../app/slices/message.channel";
|
||||
import {
|
||||
addMessage,
|
||||
removeMessage,
|
||||
updateMessage,
|
||||
} from "../../../app/slices/message";
|
||||
import { toggleReactionMessage } from "../../../app/slices/message.reaction";
|
||||
import { addUserMsg, removeUserMsg } from "../../../app/slices/message.user";
|
||||
import { updateAfterMid } from "../../../app/slices/footprint";
|
||||
const handler = (data, dispatch, currState) => {
|
||||
const {
|
||||
mid,
|
||||
from_uid,
|
||||
created_at,
|
||||
target,
|
||||
detail: {
|
||||
mid: detailMid,
|
||||
content,
|
||||
content_type,
|
||||
type,
|
||||
properties,
|
||||
expires_in,
|
||||
detail: innerDetail,
|
||||
},
|
||||
} = data;
|
||||
const common = {
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
content_type,
|
||||
properties,
|
||||
expires_in,
|
||||
};
|
||||
switch (type) {
|
||||
case "normal":
|
||||
case "reply":
|
||||
// 更新after_mid
|
||||
dispatch(updateAfterMid(mid));
|
||||
break;
|
||||
}
|
||||
const { ready, loginUid, readUsers = {}, readChannels = {} } = currState;
|
||||
const to = typeof target.gid !== "undefined" ? "channel" : "user";
|
||||
const appendMessage = to == "user" ? addUserMsg : addChannelMsg;
|
||||
const self = from_uid == loginUid;
|
||||
// 此处有点绕
|
||||
const id = to == "user" ? (self ? target.uid : from_uid) : target.gid;
|
||||
const readIndex = (to == "user" ? readUsers[id] : readChannels[id]) || 0;
|
||||
const read = self ? true : mid < readIndex ? true : false;
|
||||
switch (type) {
|
||||
case "normal":
|
||||
{
|
||||
batch(() => {
|
||||
dispatch(
|
||||
addMessage({
|
||||
mid,
|
||||
// 如果是自己发的消息,就是已读
|
||||
read,
|
||||
...common,
|
||||
})
|
||||
);
|
||||
// 未推送完 or 不是自己发的消息
|
||||
console.log("curr state", ready, loginUid, common.from_uid);
|
||||
if (!ready || loginUid != common.from_uid) {
|
||||
dispatch(appendMessage({ id, mid }));
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "reply":
|
||||
{
|
||||
batch(() => {
|
||||
dispatch(
|
||||
addMessage({
|
||||
mid,
|
||||
reply_mid: detailMid,
|
||||
// 如果是自己发的消息,就是已读
|
||||
read,
|
||||
...common,
|
||||
})
|
||||
);
|
||||
// 未推送完 or 不是自己发的消息
|
||||
if (!ready || loginUid != common.from_uid) {
|
||||
dispatch(appendMessage({ id, mid }));
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "reaction":
|
||||
{
|
||||
const removeContextMessage =
|
||||
to == "user" ? removeUserMsg : removeChannelMsg;
|
||||
const { type, action, content, content_type, properties } = innerDetail;
|
||||
switch (type) {
|
||||
case "like":
|
||||
{
|
||||
dispatch(
|
||||
toggleReactionMessage({ from_uid, mid: detailMid, action })
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case "delete":
|
||||
{
|
||||
batch(() => {
|
||||
dispatch(removeContextMessage({ id, mid: detailMid }));
|
||||
dispatch(removeMessage(detailMid));
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "edit":
|
||||
{
|
||||
dispatch(
|
||||
updateMessage({
|
||||
mid: detailMid,
|
||||
content,
|
||||
content_type,
|
||||
properties,
|
||||
edited: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
export default handler;
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
fetchEventSource,
|
||||
EventStreamContentType,
|
||||
} from "@microsoft/fetch-event-source";
|
||||
import toast from "react-hot-toast";
|
||||
import BASE_URL from "../../../app/config";
|
||||
import { setReady } from "../../../app/slices/ui";
|
||||
import {
|
||||
fullfillChannels,
|
||||
addChannel,
|
||||
removeChannel,
|
||||
updateChannel,
|
||||
} from "../../../app/slices/channels";
|
||||
import {
|
||||
updateUsersVersion,
|
||||
updateReadChannels,
|
||||
updateReadUsers,
|
||||
} from "../../../app/slices/footprint";
|
||||
import {
|
||||
updateUsersByLogs,
|
||||
updateUsersStatus,
|
||||
} from "../../../app/slices/contacts";
|
||||
import { resetAuthData } from "../../../app/slices/auth.data";
|
||||
import chatMessageHandler from "./chat.handler";
|
||||
|
||||
import { useDispatch } from "react-redux";
|
||||
class RetriableError extends Error {}
|
||||
class FatalError extends Error {}
|
||||
const getQueryString = (params = {}) => {
|
||||
const sp = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
sp.append(key, val);
|
||||
}
|
||||
});
|
||||
return sp.toString();
|
||||
};
|
||||
const StreamStatus = {
|
||||
waiting: "waiting",
|
||||
initializing: "initializing",
|
||||
streaming: "streaming",
|
||||
};
|
||||
export default function useStreaming() {
|
||||
// const store = useSelector((store) => store);
|
||||
const dispatch = useDispatch();
|
||||
const [status, setStatus] = useState(StreamStatus.waiting);
|
||||
const startStreaming = (store) => {
|
||||
if (status !== StreamStatus.waiting) return;
|
||||
const controller = new AbortController();
|
||||
setStatus(StreamStatus.initializing);
|
||||
const {
|
||||
authData: { token, afterMid, usersVersion },
|
||||
} = store;
|
||||
fetchEventSource(
|
||||
`${BASE_URL}/user/events?${getQueryString({
|
||||
"api-key": token,
|
||||
users_version: usersVersion,
|
||||
after_mid: afterMid,
|
||||
})}`,
|
||||
{
|
||||
openWhenHidden: true,
|
||||
signal: controller.signal,
|
||||
async onopen(response) {
|
||||
if (
|
||||
response.ok &&
|
||||
response.headers.get("content-type") === EventStreamContentType
|
||||
) {
|
||||
console.log("sse everything ok");
|
||||
setStatus(StreamStatus.streaming);
|
||||
return; // everything's good
|
||||
} else if (
|
||||
response.status >= 400 &&
|
||||
response.status < 500 &&
|
||||
response.status !== 429
|
||||
) {
|
||||
// 重新登录
|
||||
if (response.status == 401) {
|
||||
dispatch(resetAuthData());
|
||||
return;
|
||||
}
|
||||
// client-side errors are usually non-retriable:
|
||||
throw new FatalError();
|
||||
} else {
|
||||
throw new RetriableError();
|
||||
}
|
||||
},
|
||||
onmessage(evt) {
|
||||
console.log("sse message", evt.data);
|
||||
// if the server emits an error message, throw an exception
|
||||
// so it gets handled by the onerror callback below:
|
||||
if (evt.event === "FatalError") {
|
||||
throw new FatalError(evt.data);
|
||||
}
|
||||
const {
|
||||
ui: { ready },
|
||||
authData: { uid: loginUid },
|
||||
footprint: { readUsers, readChannels },
|
||||
channels: { byId: channelData },
|
||||
} = store;
|
||||
const data = JSON.parse(evt.data);
|
||||
const { type } = data;
|
||||
switch (type) {
|
||||
case "heartbeat":
|
||||
console.log("heartbeat");
|
||||
break;
|
||||
case "ready":
|
||||
console.log("streaming ready");
|
||||
dispatch(setReady());
|
||||
break;
|
||||
case "users_snapshot":
|
||||
{
|
||||
console.log("users snapshot");
|
||||
const { version } = data;
|
||||
dispatch(updateUsersVersion(version));
|
||||
}
|
||||
break;
|
||||
case "users_log":
|
||||
{
|
||||
console.log("users change logs");
|
||||
const { logs } = data;
|
||||
dispatch(updateUsersByLogs(logs));
|
||||
}
|
||||
break;
|
||||
case "user_settings":
|
||||
case "user_settings_changed":
|
||||
{
|
||||
console.log("users settings");
|
||||
const { read_index_users = [], read_index_groups = [] } = data;
|
||||
dispatch(updateReadChannels(read_index_groups));
|
||||
dispatch(updateReadUsers(read_index_users));
|
||||
}
|
||||
break;
|
||||
case "users_state":
|
||||
case "users_state_changed":
|
||||
{
|
||||
let { type, ...rest } = data;
|
||||
const onlines =
|
||||
type == "users_state_changed" ? [rest] : rest.users;
|
||||
dispatch(updateUsersStatus(onlines));
|
||||
}
|
||||
break;
|
||||
case "kick":
|
||||
{
|
||||
console.log("kicked");
|
||||
switch (data.reason) {
|
||||
case "login_from_other_device":
|
||||
dispatch(resetAuthData());
|
||||
toast("kicked from the other device");
|
||||
break;
|
||||
case "delete_user":
|
||||
dispatch(resetAuthData());
|
||||
toast("sorry, your account has been deleted");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "related_groups":
|
||||
console.log("fullfill channels from streaming", data);
|
||||
dispatch(fullfillChannels(data.groups));
|
||||
break;
|
||||
case "joined_group":
|
||||
console.log("joined group", data.group);
|
||||
dispatch(addChannel(data.group));
|
||||
break;
|
||||
case "user_joined_group":
|
||||
console.log("new user joined group", data.gid);
|
||||
dispatch(
|
||||
updateChannel({
|
||||
id: data.gid,
|
||||
members: [...channelData[data.gid].members, ...data.uid],
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "kick_from_group":
|
||||
console.log("kicked from group", data.gid);
|
||||
dispatch(removeChannel(data.gid));
|
||||
break;
|
||||
case "chat":
|
||||
{
|
||||
chatMessageHandler(data, dispatch, {
|
||||
ready,
|
||||
loginUid,
|
||||
readUsers,
|
||||
readChannels,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("sse event data", data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
// if the server closes the connection unexpectedly, retry:
|
||||
throw new RetriableError();
|
||||
},
|
||||
onerror(err) {
|
||||
if (err instanceof FatalError) {
|
||||
// 重连
|
||||
setTimeout(() => {
|
||||
startStreaming(store);
|
||||
}, 500);
|
||||
throw err; // rethrow to stop the operation
|
||||
} else {
|
||||
// do nothing to automatically retry. You can also
|
||||
// return a specific retry interval here.
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
// for controlling
|
||||
return controller;
|
||||
};
|
||||
return {
|
||||
initializing: status == StreamStatus.initializing,
|
||||
streaming: status == StreamStatus.streaming,
|
||||
startStreaming,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user