chore: update favorite icon in different sences

This commit is contained in:
zerosoul
2022-05-06 10:19:42 +08:00
parent 49fbd92e0c
commit bfa83409d5
10 changed files with 1127 additions and 1200 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 7.5C21 9.98528 18.9853 12 16.5 12C14.0147 12 12 9.98528 12 7.5C12 5.01472 14.0147 3 16.5 3C18.9853 3 21 5.01472 21 7.5ZM17 5.5C17 5.22386 16.7761 5 16.5 5C16.2239 5 16 5.22386 16 5.5V7H14.5C14.2239 7 14 7.22386 14 7.5C14 7.77614 14.2239 8 14.5 8H16V9.5C16 9.77614 16.2239 10 16.5 10C16.7761 10 17 9.77614 17 9.5V8H18.5C18.7761 8 19 7.77614 19 7.5C19 7.22386 18.7761 7 18.5 7H17V5.5ZM16.5 13C17.02 13 17.5232 12.9278 18 12.793V19.5C18 19.6881 17.8945 19.8602 17.7269 19.9456C17.5593 20.0309 17.358 20.015 17.2059 19.9044L12 16.1183L6.79409 19.9044C6.64199 20.015 6.4407 20.0309 6.27311 19.9456C6.10553 19.8602 6 19.6881 6 19.5V6.5C6 5.11929 7.11929 4 8.5 4H12.2572C11.4718 4.95094 11 6.17037 11 7.5C11 10.5376 13.4624 13 16.5 13Z" fill="#667085"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 7.5C21 9.98528 18.9853 12 16.5 12C14.0147 12 12 9.98528 12 7.5C12 5.01472 14.0147 3 16.5 3C18.9853 3 21 5.01472 21 7.5ZM17 5.5C17 5.22386 16.7761 5 16.5 5C16.2239 5 16 5.22386 16 5.5V7H14.5C14.2239 7 14 7.22386 14 7.5C14 7.77614 14.2239 8 14.5 8H16V9.5C16 9.77614 16.2239 10 16.5 10C16.7761 10 17 9.77614 17 9.5V8H18.5C18.7761 8 19 7.77614 19 7.5C19 7.22386 18.7761 7 18.5 7H17V5.5ZM16.5 13C17.02 13 17.5232 12.9278 18 12.793V19.5C18 19.6881 17.8945 19.8602 17.7269 19.9456C17.5593 20.0309 17.358 20.015 17.2059 19.9044L12 16.1183L6.79409 19.9044C6.64199 20.015 6.4407 20.0309 6.27311 19.9456C6.10553 19.8602 6 19.6881 6 19.5V6.5C6 5.11929 7.11929 4 8.5 4H12.2572C11.4718 4.95094 11 6.17037 11 7.5C11 10.5376 13.4624 13 16.5 13Z" fill="#667085"/>
<path d="M5.99988 6.5C5.99988 5.11929 7.11917 4 8.49988 4H15.4999C16.8806 4 17.9999 5.11929 17.9999 6.5V19.5C17.9999 19.6881 17.8944 19.8602 17.7268 19.9456C17.5592 20.0309 17.3579 20.015 17.2058 19.9044L11.9999 16.1183L6.79396 19.9044C6.64187 20.015 6.44057 20.0309 6.27299 19.9456C6.1054 19.8602 5.99988 19.6881 5.99988 19.5V6.5Z" fill="#344054"/>
</svg>

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 453 B

+216 -237
View File
@@ -1,252 +1,231 @@
import { useState, useRef, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import { useState, useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import Tippy from '@tippyjs/react';
import { hideAll } from 'tippy.js';
// import toast from "react-hot-toast";
import { updateSelectMessages } from "../../../app/slices/ui";
import { addReplyingMessage } from "../../../app/slices/message";
import StyledMenu from "../styled/Menu";
import Tooltip from "../../component/Tooltip";
import DeleteMessageConfirm from "../DeleteMessageConfirm";
import useFavMessage from "../../hook/useFavMessage";
import EmojiPicker from "./EmojiPicker";
import replyIcon from "../../../assets/icons/reply.svg?url";
import reactIcon from "../../../assets/icons/reaction.svg?url";
import editIcon from "../../../assets/icons/edit.svg?url";
import IconBookmark from "../../../assets/icons/bookmark.svg";
import moreIcon from "../../../assets/icons/more.svg?url";
import ForwardModal from "../ForwardModal";
import PinMessageModal from "./PinMessageModal";
import usePinMessage from "../../hook/usePinMessage";
import { ContentTypes } from "../../../app/config";
import toast from "react-hot-toast";
import { updateSelectMessages } from '../../../app/slices/ui';
import { addReplyingMessage } from '../../../app/slices/message';
import StyledMenu from '../styled/Menu';
import Tooltip from '../../component/Tooltip';
import DeleteMessageConfirm from '../DeleteMessageConfirm';
import useFavMessage from '../../hook/useFavMessage';
import EmojiPicker from './EmojiPicker';
import replyIcon from '../../../assets/icons/reply.svg?url';
import reactIcon from '../../../assets/icons/reaction.svg?url';
import editIcon from '../../../assets/icons/edit.svg?url';
import IconBookmark from '../../../assets/icons/bookmark.add.svg';
import moreIcon from '../../../assets/icons/more.svg?url';
import ForwardModal from '../ForwardModal';
import PinMessageModal from './PinMessageModal';
import usePinMessage from '../../hook/usePinMessage';
import { ContentTypes } from '../../../app/config';
import toast from 'react-hot-toast';
const StyledCmds = styled.ul`
z-index: 9999;
position: absolute;
right: 10px;
top: 0;
transform: translateY(-50%);
z-index: 9999;
position: absolute;
right: 10px;
top: 0;
transform: translateY(-50%);
display: flex;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
background-color: #fff;
visibility: hidden;
&.visible {
visibility: visible;
}
.cmd {
display: flex;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
background-color: #fff;
visibility: hidden;
&.visible {
visibility: visible;
cursor: pointer;
padding: 4px;
&:hover {
background-color: #f3f4f6;
}
.cmd {
display: flex;
cursor: pointer;
padding: 4px;
&:hover {
background-color: #f3f4f6;
}
img,
svg {
width: 24px;
height: 24px;
}
&.fav {
svg path {
fill: #667085;
}
}
img,
svg {
width: 24px;
height: 24px;
}
> .picker {
position: absolute;
left: -10px;
top: 0;
transform: translateX(-100%);
&.fav {
svg path {
fill: #667085;
}
}
}
> .picker {
position: absolute;
left: -10px;
top: 0;
transform: translateX(-100%);
}
`;
export default function Commands({
content_type = ContentTypes.text,
context = "user",
contextId = 0,
mid = 0,
from_uid = 0,
toggleEditMessage,
content_type = ContentTypes.text,
context = 'user',
contextId = 0,
mid = 0,
from_uid = 0,
toggleEditMessage
}) {
const { addFavorite, isFavorited } = useFavMessage(
context == "channel" ? contextId : null
);
const [mids, setMids] = useState([]);
const dispatch = useDispatch();
const [pinModalVisible, setPinModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [forwardModalVisible, setForwardModalVisible] = useState(false);
const [tippyVisible, setTippyVisible] = useState(false);
const { canPin, pins, unpinMessage, isUnpinSuccess } = usePinMessage(
context == "channel" ? contextId : undefined
);
const currUid = useSelector((store) => store.authData.uid);
const cmdsRef = useRef(null);
const handleReply = (fromMenu) => {
if (contextId) {
dispatch(addReplyingMessage({ id: contextId, mid }));
}
if (fromMenu) {
hideAll();
}
};
const toggleForwardModal = () => {
hideAll();
console.log("midss", mids);
setForwardModalVisible((prev) => !prev);
};
const toggleDeleteModal = () => {
hideAll();
setDeleteModalVisible((prev) => !prev);
};
const togglePinModal = () => {
hideAll();
setPinModalVisible((prev) => !prev);
};
const handleTippyVisible = (visible = true) => {
setTippyVisible(visible);
};
const handleSelect = (mid) => {
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
hideAll();
};
const handleUnpin = () => {
hideAll();
unpinMessage(mid);
};
const handleAddFav = async () => {
hideAll();
const faved = isFavorited(mid);
if (faved) {
toast.success("Favorited!");
return;
}
await addFavorite(mid);
toast.success("Added Favorites!");
};
useEffect(() => {
if (isUnpinSuccess) {
toast.success("Unpin Message Successfully!");
}
}, [isUnpinSuccess]);
const { addFavorite, isFavorited } = useFavMessage(context == 'channel' ? contextId : null);
const [mids, setMids] = useState([]);
const dispatch = useDispatch();
const [pinModalVisible, setPinModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [forwardModalVisible, setForwardModalVisible] = useState(false);
const [tippyVisible, setTippyVisible] = useState(false);
const { canPin, pins, unpinMessage, isUnpinSuccess } = usePinMessage(
context == 'channel' ? contextId : undefined
);
const currUid = useSelector((store) => store.authData.uid);
const cmdsRef = useRef(null);
const handleReply = (fromMenu) => {
if (contextId) {
dispatch(addReplyingMessage({ id: contextId, mid }));
}
if (fromMenu) {
hideAll();
}
};
const toggleForwardModal = () => {
hideAll();
console.log('midss', mids);
setForwardModalVisible((prev) => !prev);
};
const toggleDeleteModal = () => {
hideAll();
setDeleteModalVisible((prev) => !prev);
};
const togglePinModal = () => {
hideAll();
setPinModalVisible((prev) => !prev);
};
const handleTippyVisible = (visible = true) => {
setTippyVisible(visible);
};
const handleSelect = (mid) => {
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
hideAll();
};
const handleUnpin = () => {
hideAll();
unpinMessage(mid);
};
const handleAddFav = async () => {
hideAll();
const faved = isFavorited(mid);
if (faved) {
toast.success('Favorited!');
return;
}
await addFavorite(mid);
toast.success('Added Favorites!');
};
useEffect(() => {
if (isUnpinSuccess) {
toast.success('Unpin Message Successfully!');
}
}, [isUnpinSuccess]);
useEffect(() => {
if (content_type == ContentTypes.archive) {
// forward message
const forwardEle = document.querySelector(
`[data-msg-mid='${mid}'] .down [data-forwarded-mids]`
);
if (forwardEle) {
const mids = forwardEle.dataset.forwardedMids.split(",");
setMids(mids);
}
} else {
setMids([mid]);
}
}, [mid, content_type]);
useEffect(() => {
if (content_type == ContentTypes.archive) {
// forward message
const forwardEle = document.querySelector(`[data-msg-mid='${mid}'] .down [data-forwarded-mids]`);
if (forwardEle) {
const mids = forwardEle.dataset.forwardedMids.split(',');
setMids(mids);
}
} else {
setMids([mid]);
}
}, [mid, content_type]);
const enablePin = context == "channel" && canPin;
const enableEdit =
currUid == from_uid &&
[ContentTypes.text, ContentTypes.markdown].includes(content_type);
const enableReply = currUid != from_uid;
const pinned = enablePin ? pins.findIndex((p) => p.mid == mid) > -1 : false;
return (
<StyledCmds
ref={cmdsRef}
className={`cmds ${tippyVisible ? "visible" : ""}`}
>
<Tippy
duration={0}
delay={[0, 0]}
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
interactive
placement="left-start"
trigger="click"
content={<EmojiPicker mid={mid} hidePicker={hideAll} />}
>
<li className="cmd">
<Tooltip placement="top" tip="Add Reaction">
<img src={reactIcon} className="toggler" alt="icon emoji" />
</Tooltip>
</li>
</Tippy>
{enableEdit && (
<li className="cmd" onClick={toggleEditMessage}>
<Tooltip placement="top" tip="Edit">
<img src={editIcon} alt="icon edit" />
</Tooltip>
</li>
const enablePin = context == 'channel' && canPin;
const enableEdit =
currUid == from_uid && [ContentTypes.text, ContentTypes.markdown].includes(content_type);
const enableReply = currUid != from_uid;
const pinned = enablePin ? pins.findIndex((p) => p.mid == mid) > -1 : false;
return (
<StyledCmds ref={cmdsRef} className={`cmds ${tippyVisible ? 'visible' : ''}`}>
<Tippy
duration={0}
delay={[0, 0]}
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
interactive
placement="left-start"
trigger="click"
content={<EmojiPicker mid={mid} hidePicker={hideAll} />}
>
<li className="cmd">
<Tooltip placement="top" tip="Add Reaction">
<img src={reactIcon} className="toggler" alt="icon emoji" />
</Tooltip>
</li>
</Tippy>
{enableEdit && (
<li className="cmd" onClick={toggleEditMessage}>
<Tooltip placement="top" tip="Edit">
<img src={editIcon} alt="icon edit" />
</Tooltip>
</li>
)}
{enableReply && (
<li className="cmd" onClick={handleReply}>
<Tooltip placement="top" tip="Reply">
<img src={replyIcon} alt="icon reply" />
</Tooltip>
</li>
)}
<li className="cmd fav" onClick={handleAddFav}>
<Tooltip placement="top" tip="Add to Favorites">
<IconBookmark />
</Tooltip>
</li>
<Tippy
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
interactive
popperOptions={{ strategy: 'fixed' }}
placement="left-start"
trigger="click"
content={
<StyledMenu className="menu">
{/* <li className="item">Edit Message</li> */}
{enablePin && (
<li className="item" onClick={pinned ? handleUnpin : togglePinModal}>
{pinned ? `Unpin Message` : `Pin Message`}
</li>
)}
{enableReply && (
<li className="cmd" onClick={handleReply}>
<Tooltip placement="top" tip="Reply">
<img src={replyIcon} alt="icon reply" />
</Tooltip>
</li>
)}
<li className="cmd fav" onClick={handleAddFav}>
<Tooltip placement="top" tip="Add to Favorites">
<IconBookmark />
</Tooltip>
<li className="item" onClick={toggleForwardModal}>
Forward
</li>
<Tippy
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
interactive
popperOptions={{ strategy: "fixed" }}
placement="left-start"
trigger="click"
content={
<StyledMenu className="menu">
{/* <li className="item">Edit Message</li> */}
{enablePin && (
<li
className="item"
onClick={pinned ? handleUnpin : togglePinModal}
>
{pinned ? `Unpin Message` : `Pin Message`}
</li>
)}
<li className="item" onClick={toggleForwardModal}>
Forward
</li>
<li className="item" onClick={handleReply.bind(null, true)}>
Reply
</li>
<li className="item" onClick={handleSelect.bind(null, mid)}>
Select
</li>
{currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}>
Delete Message
</li>
)}
</StyledMenu>
}
>
<li className="cmd">
<Tooltip placement="top" tip="More">
<img src={moreIcon} alt="icon more" />
</Tooltip>
</li>
</Tippy>
<li className="item" onClick={handleReply.bind(null, true)}>
Reply
</li>
<li className="item" onClick={handleSelect.bind(null, mid)}>
Select
</li>
{currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}>
Delete Message
</li>
)}
</StyledMenu>
}
>
<li className="cmd">
<Tooltip placement="top" tip="More">
<img src={moreIcon} alt="icon more" />
</Tooltip>
</li>
</Tippy>
{deleteModalVisible && (
<DeleteMessageConfirm closeModal={toggleDeleteModal} mids={mid} />
)}
{forwardModalVisible && (
<ForwardModal mids={mids} closeModal={toggleForwardModal} />
)}
{pinModalVisible && (
<PinMessageModal
mid={mid}
gid={contextId}
closeModal={togglePinModal}
/>
)}
</StyledCmds>
);
{deleteModalVisible && <DeleteMessageConfirm closeModal={toggleDeleteModal} mids={mid} />}
{forwardModalVisible && <ForwardModal mids={mids} closeModal={toggleForwardModal} />}
{pinModalVisible && <PinMessageModal mid={mid} gid={contextId} closeModal={togglePinModal} />}
</StyledCmds>
);
}
+86 -87
View File
@@ -1,95 +1,94 @@
import styled from "styled-components";
import styled from 'styled-components';
const StyledMsg = styled.div`
position: relative;
position: relative;
display: flex;
align-items: flex-start;
gap: 16px;
padding: 4px 8px;
margin: 8px 0;
border-radius: 8px;
content-visibility: auto;
contain-intrinsic-size: auto 150px;
&.in_view {
content-visibility: visible;
}
&[data-highlight='true'] {
background: #f5f6f7;
}
&:hover,
&.preview {
content-visibility: inherit;
contain-intrinsic-size: inherit;
background: #f5f6f7;
.cmds {
visibility: visible;
}
}
&.readonly:hover {
background: none;
}
.avatar {
flex-shrink: 0;
cursor: pointer;
width: 40px;
height: 40px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
> .details {
width: 100%;
display: flex;
align-items: flex-start;
gap: 16px;
padding: 4px 8px;
margin: 8px 0;
border-radius: 8px;
content-visibility: auto;
contain-intrinsic-size: auto 150px;
&.in_view {
content-visibility: visible;
flex-direction: column;
gap: 8px;
.up {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
.name {
color: #06b6d4;
font-style: normal;
font-size: 14px;
line-height: 20px;
}
.time {
color: #bfbfbf;
font-size: 12px;
line-height: 18px;
}
}
&[data-highlight="true"] {
background: #f5f6f7;
}
&:hover,
&.preview {
content-visibility: inherit;
contain-intrinsic-size: inherit;
background: #f5f6f7;
.cmds {
visibility: visible;
}
}
&.readonly:hover {
background: none;
}
.avatar {
flex-shrink: 0;
.down {
user-select: text;
color: #374151;
font-weight: normal;
font-size: 14px;
line-height: 20px;
word-break: break-all;
white-space: pre-wrap;
.edited {
margin-left: 5px;
color: #999;
font-size: 10px;
}
&.sending {
opacity: 0.9;
}
.img {
max-width: 480px;
cursor: pointer;
width: 40px;
height: 40px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
> .details {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
.up {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
.name {
color: #06b6d4;
font-style: normal;
font-size: 14px;
line-height: 20px;
}
.time {
color: #bfbfbf;
font-size: 12px;
line-height: 18px;
}
}
.down {
user-select: text;
color: #374151;
font-weight: normal;
font-size: 14px;
line-height: 20px;
word-break: break-all;
white-space: break-spaces;
.edited {
margin-left: 5px;
color: #999;
font-size: 10px;
}
&.sending {
opacity: 0.9;
}
.img {
max-width: 480px;
cursor: pointer;
}
> .link {
text-decoration: none;
border-radius: 2px;
/* background-color: #f5feff; */
padding: 2px;
color: #1fe1f9;
}
}
}
> .link {
text-decoration: none;
border-radius: 2px;
/* background-color: #f5feff; */
padding: 2px;
color: #1fe1f9;
}
}
}
`;
export default StyledMsg;
+249 -255
View File
@@ -1,272 +1,266 @@
import { useRef, useEffect, useState, useCallback } from "react";
import { useKey } from "rooks";
import { useDispatch, useSelector } from "react-redux";
import { Editor, Transforms } from "slate";
import { useRef, useEffect, useState, useCallback } from 'react';
import { useKey } from 'rooks';
import { useDispatch, useSelector } from 'react-redux';
import { Editor, Transforms } from 'slate';
import {
createPlateUI,
Plate,
createExitBreakPlugin,
createTrailingBlockPlugin,
createNodeIdPlugin,
createParagraphPlugin,
// createImagePlugin,
createSoftBreakPlugin,
createComboboxPlugin,
createMentionPlugin,
// comboboxSelectors,
// getMentionOnSelectItem,
// findMentionInput,
// removeMentionInput,
// isSelectionInMentionInput,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateEditorRef,
// usePlateEditorRef,
// ELEMENT_IMAGE,
MentionCombobox,
} from "@udecode/plate";
import { ReactEditor } from "slate-react";
import Styled from "./styled";
import { CONFIG } from "./config";
import Contact from "../Contact";
import { updateUploadFiles } from "../../../app/slices/ui";
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
createPlateUI,
Plate,
createExitBreakPlugin,
createTrailingBlockPlugin,
createNodeIdPlugin,
createParagraphPlugin,
// createImagePlugin,
createSoftBreakPlugin,
// createComboboxPlugin,
createMentionPlugin,
// comboboxSelectors,
// getMentionOnSelectItem,
// findMentionInput,
// removeMentionInput,
// isSelectionInMentionInput,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateEditorRef,
// usePlateEditorRef,
// ELEMENT_IMAGE,
MentionCombobox
} from '@udecode/plate';
import { createComboboxPlugin } from '@udecode/plate-combobox';
import { ReactEditor } from 'slate-react';
import Styled from './styled';
import { CONFIG } from './config';
import Contact from '../Contact';
import { updateUploadFiles } from '../../../app/slices/ui';
export const TEXT_EDITOR_PREFIX = 'rustchat_text_editor';
let components = createPlateUI({
// [ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key
// [ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key
});
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }];
const Plugins = ({
id = "",
placeholder = "Write some markdown...",
sendMessages,
members = [],
id = '',
placeholder = 'Write some markdown...',
sendMessages,
members = []
}) => {
const dispatch = useDispatch();
const enableMentions = members.length > 0;
const filesRef = useRef([]);
const contactData = useSelector((store) => store.contacts.byId);
const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null);
const initialProps = {
...CONFIG.editableProps,
className: "box",
placeholder,
const dispatch = useDispatch();
const enableMentions = members.length > 0;
const filesRef = useRef([]);
const contactData = useSelector((store) => store.contacts.byId);
const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null);
const initialProps = {
...CONFIG.editableProps,
className: 'box',
placeholder
};
useEffect(() => {
const handlePasteEvent = (evt) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
console.log('paste event name', name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
const [context, to] = id.split('_');
console.log('paste event', context, to, files, evt);
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
};
useEffect(() => {
const handlePasteEvent = (evt) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
console.log("paste event name", name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
const [context, to] = id.split("_");
window.addEventListener('paste', handlePasteEvent);
return () => {
window.removeEventListener('paste', handlePasteEvent);
};
// window.addEventListener("paste")
}, []);
console.log("paste event", context, to, files, evt);
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
};
window.addEventListener("paste", handlePasteEvent);
return () => {
window.removeEventListener("paste", handlePasteEvent);
};
// window.addEventListener("paste")
}, []);
useKey(
"Enter",
(evt) => {
console.log("enter keypress", evt);
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
return true;
}
evt.preventDefault();
sendMessages(msgs);
// 清空
const plateEditor = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
Transforms.delete(plateEditor, {
at: {
anchor: Editor.start(plateEditor, []),
focus: Editor.end(plateEditor, []),
useKey(
'Enter',
(evt) => {
console.log('enter keypress', evt);
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
return true;
}
evt.preventDefault();
sendMessages(msgs);
// 清空
const plateEditor = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
Transforms.delete(plateEditor, {
at: {
anchor: Editor.start(plateEditor, []),
focus: Editor.end(plateEditor, [])
}
});
},
{
// eventTypes: ["keydown"],
target: editableRef,
when: !cmdKey
}
);
useKey(
[91, 93],
(evt) => {
setCmdKey(evt.type == 'keydown');
console.log('cmd', evt.type);
},
{
eventTypes: ['keydown', 'keyup'],
target: editableRef
}
);
const pluginArr = [
createParagraphPlugin(),
createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock),
createExitBreakPlugin(CONFIG.exitBreak)
];
const plugins = createPlugins(
enableMentions
? pluginArr.concat([
createComboboxPlugin(),
createMentionPlugin({
options: {
createMentionNode: (item) => {
console.log('mention', item);
const {
text,
data: { uid }
} = item;
return { value: `@${text}`, uid };
},
});
},
{
// eventTypes: ["keydown"],
target: editableRef,
when: !cmdKey,
}
);
useKey(
[91, 93],
(evt) => {
setCmdKey(evt.type == "keydown");
console.log("cmd", evt.type);
},
{
eventTypes: ["keydown", "keyup"],
target: editableRef,
}
);
const pluginArr = [
createParagraphPlugin(),
createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock),
createExitBreakPlugin(CONFIG.exitBreak),
];
const plugins = createPlugins(
enableMentions
? pluginArr.concat([
createComboboxPlugin(),
createMentionPlugin({
options: {
createMentionNode: (item) => {
console.log("mention", item);
const {
text,
data: { uid },
} = item;
return { value: `@${text}`, uid };
},
insertSpaceAfterMention: true,
},
}),
])
: pluginArr,
{
components,
}
);
insertSpaceAfterMention: true
}
})
])
: pluginArr,
{
components
}
);
const handleChange = useCallback(
async (val) => {
console.log("tmps changed", val);
const tmps = [];
const getMixedText = (children) => {
const mentions = [];
const arr = children.map(({ type, text, uid }) => {
if (type == "mention") {
mentions.push(uid);
return ` @${uid} `;
}
return text;
});
return { value: arr.join(""), mentions };
};
for (const v of val) {
if (v.type == "img") {
// img
const url = v.url;
const file_path = decodeURIComponent(
new URL(url).searchParams.get("file_path")
);
console.log("files", filesRef.current, file_path);
const json = filesRef.current.find((f) => f.path == file_path) || {};
const { name, size, hash, path, ...rest } = json;
tmps.push({
type: "file",
content: { name, size, hash, path },
properties: rest,
});
} else {
// p
const { value, mentions } = getMixedText(v.children);
const prev = tmps[tmps.length - 1];
if (!prev) {
tmps.push([
{ type: "text", content: value, properties: { mentions } },
]);
} else {
if (Array.isArray(prev)) {
tmps[tmps.length - 1].push({
type: "text",
content: value,
properties: { mentions },
});
} else {
tmps.push([
{ type: "text", content: value, properties: { mentions } },
]);
}
}
}
const handleChange = useCallback(
async (val) => {
console.log('tmps changed', val);
const tmps = [];
const getMixedText = (children) => {
const mentions = [];
const arr = children.map(({ type, text, uid }) => {
if (type == 'mention') {
mentions.push(uid);
return ` @${uid} `;
}
return text;
});
return { value: arr.join(''), mentions };
};
for (const v of val) {
if (v.type == 'img') {
// img
const url = v.url;
const file_path = decodeURIComponent(new URL(url).searchParams.get('file_path'));
console.log('files', filesRef.current, file_path);
const json = filesRef.current.find((f) => f.path == file_path) || {};
const { name, size, hash, path, ...rest } = json;
tmps.push({
type: 'file',
content: { name, size, hash, path },
properties: rest
});
} else {
// p
const { value, mentions } = getMixedText(v.children);
const prev = tmps[tmps.length - 1];
if (!prev) {
tmps.push([{ type: 'text', content: value, properties: { mentions } }]);
} else {
if (Array.isArray(prev)) {
tmps[tmps.length - 1].push({
type: 'text',
content: value,
properties: { mentions }
});
} else {
tmps.push([{ type: 'text', content: value, properties: { mentions } }]);
}
const arr = tmps.map((tmp) => {
return Array.isArray(tmp)
? {
type: "text",
content: tmp.map((t) => t.content).join("\n"),
properties: {
mentions: tmp.map((t) => t.properties?.mentions || []).flat(),
},
}
: tmp;
});
const msgs = arr.filter(({ content }) => !!content);
setMsgs(msgs);
console.log("tmps", tmps, arr, msgs);
},
[msgs]
);
}
}
}
const arr = tmps.map((tmp) => {
return Array.isArray(tmp)
? {
type: 'text',
content: tmp.map((t) => t.content).join('\n'),
properties: {
mentions: tmp.map((t) => t.properties?.mentions || []).flat()
}
}
: tmp;
});
const msgs = arr.filter(({ content }) => !!content);
setMsgs(msgs);
console.log('tmps', tmps, arr, msgs);
},
[msgs]
);
return (
<Styled className="input" ref={editableRef}>
<Plate
edi
onChange={handleChange}
id={`${TEXT_EDITOR_PREFIX}_${id}`}
editableProps={{ ...initialProps, style: { userSelect: "text" } }}
initialValue={initialValue}
plugins={plugins}
>
{enableMentions ? (
<MentionCombobox
// component={StyledCombobox}
onRenderItem={({ item }) => {
console.log("wtf", item);
return <Contact uid={item.data.uid} interactive={false} />;
}}
items={members.map((id) => {
const data = contactData[id];
if (!data) return null;
const { uid, name, ...rest } = data;
return {
key: uid,
text: name,
data: {
uid,
...rest,
},
};
})}
/>
) : null}
</Plate>
</Styled>
);
return (
<Styled className="input" ref={editableRef}>
<Plate
onChange={handleChange}
id={`${TEXT_EDITOR_PREFIX}_${id}`}
editableProps={{ ...initialProps, style: { userSelect: 'text' } }}
initialValue={initialValue}
plugins={plugins}
>
{enableMentions ? (
<MentionCombobox
// component={StyledCombobox}
onRenderItem={({ item }) => {
console.log('wtf', item);
return <Contact uid={item.data.uid} interactive={false} />;
}}
items={members.map((id) => {
const data = contactData[id];
if (!data) return null;
const { uid, name, ...rest } = data;
return {
key: uid,
text: name,
data: {
uid,
...rest
}
};
})}
/>
) : null}
</Plate>
</Styled>
);
};
export const useMixedEditor = (key) => {
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${key}`);
const focus = () => {
if (editorRef) {
ReactEditor.focus(editorRef);
}
};
const insertText = (txt) => {
if (editorRef) {
editorRef.insertText(txt);
}
};
return {
focus,
insertText,
};
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${key}`);
const focus = () => {
if (editorRef) {
ReactEditor.focus(editorRef);
}
};
const insertText = (txt) => {
if (editorRef) {
editorRef.insertText(txt);
}
};
return {
focus,
insertText
};
};
export default Plugins;
+217 -245
View File
@@ -1,259 +1,231 @@
import { useState } from "react";
import { useDebounce } from "rooks";
import { useSelector } from "react-redux";
import PinList from "./PinList";
import FavList from "./FavList";
import { useReadMessageMutation } from "../../../app/services/message";
import useChatScroll from "../../../common/hook/useChatScroll";
import ChannelIcon from "../../../common/component/ChannelIcon";
import Tooltip from "../../../common/component/Tooltip";
import Contact from "../../../common/component/Contact";
import Layout from "../Layout";
import { renderMessageFragment } from "../utils";
import EditIcon from "../../../assets/icons/edit.svg";
import { useState } from 'react';
import { useDebounce } from 'rooks';
import { useSelector } from 'react-redux';
import PinList from './PinList';
import FavList from './FavList';
import { useReadMessageMutation } from '../../../app/services/message';
import useChatScroll from '../../../common/hook/useChatScroll';
import ChannelIcon from '../../../common/component/ChannelIcon';
import Tooltip from '../../../common/component/Tooltip';
import Contact from '../../../common/component/Contact';
import Layout from '../Layout';
import { renderMessageFragment } from '../utils';
import EditIcon from '../../../assets/icons/edit.svg';
// import alertIcon from "../../../assets/icons/alert.svg?url";
import IconFav from "../../../assets/icons/bookmark.svg";
import IconPeople from "../../../assets/icons/people.svg";
import IconPin from "../../../assets/icons/pin.svg";
import IconFav from '../../../assets/icons/bookmark.svg';
import IconPeople from '../../../assets/icons/people.svg';
import IconPin from '../../../assets/icons/pin.svg';
// import searchIcon from "../../../assets/icons/search.svg?url";
import IconHeadphone from "../../../assets/icons/headphone.svg";
import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
import addIcon from "../../../assets/icons/add.svg?url";
import IconHeadphone from '../../../assets/icons/headphone.svg';
import boardosIcon from '../../../assets/icons/app.boardos.svg?url';
import webrowseIcon from '../../../assets/icons/app.webrowse.svg?url';
import addIcon from '../../../assets/icons/add.svg?url';
import {
// StyledNotification,
StyledContacts,
StyledChannelChat,
StyledHeader,
} from "./styled";
import ChannelInviteModal from "../../../common/component/ChannelInviteModal";
import { NavLink, useLocation } from "react-router-dom";
import Tippy from "@tippyjs/react";
// StyledNotification,
StyledContacts,
StyledChannelChat,
StyledHeader
} from './styled';
import ChannelInviteModal from '../../../common/component/ChannelInviteModal';
import { NavLink, useLocation } from 'react-router-dom';
import Tippy from '@tippyjs/react';
export default function ChannelChat({ cid = "", dropFiles = [] }) {
const [toolVisible, setToolVisible] = useState("");
const { pathname } = useLocation();
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [membersVisible, setMembersVisible] = useState(true);
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
// const dispatch = useDispatch();
const {
selects,
msgIds,
userIds,
data,
messageData,
loginUid,
loginUser,
footprint,
} = useSelector((store) => {
return {
selects: store.ui.selectMessages[`channel_${cid}`],
footprint: store.footprint,
loginUser: store.contacts.byId[store.authData.uid],
loginUid: store.authData.uid,
msgIds: store.channelMessage[cid] || [],
userIds: store.contacts.ids,
data: store.channels.byId[cid] || {},
messageData: store.message || {},
};
export default function ChannelChat({ cid = '', dropFiles = [] }) {
const [toolVisible, setToolVisible] = useState('');
const { pathname } = useLocation();
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [membersVisible, setMembersVisible] = useState(true);
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
// const dispatch = useDispatch();
const { selects, msgIds, userIds, data, messageData, loginUid, loginUser, footprint } =
useSelector((store) => {
return {
selects: store.ui.selectMessages[`channel_${cid}`],
footprint: store.footprint,
loginUser: store.contacts.byId[store.authData.uid],
loginUid: store.authData.uid,
msgIds: store.channelMessage[cid] || [],
userIds: store.contacts.ids,
data: store.channels.byId[cid] || {},
messageData: store.message || {}
};
});
const ref = useChatScroll(msgIds);
// const handleClearUnreads = () => {
// dispatch(readMessage(msgIds));
// };
const toggleMembersVisible = () => {
setMembersVisible((prev) => !prev);
};
const toggleAddVisible = () => {
setAddMemberModalVisible((prev) => !prev);
};
const ref = useChatScroll(msgIds);
// const handleClearUnreads = () => {
// dispatch(readMessage(msgIds));
// };
const toggleMembersVisible = () => {
setMembersVisible((prev) => !prev);
};
const toggleAddVisible = () => {
setAddMemberModalVisible((prev) => !prev);
};
const { name, description, is_public, members = [], owner } = data;
const memberIds = is_public ? userIds : members;
const addVisible = loginUser?.is_admin || owner == loginUid;
console.log("channel message list", msgIds);
const readIndex = footprint.readChannels[cid];
const pinCount = data?.pinned_messages?.length || 0;
return (
<>
{addMemberModalVisible && (
<ChannelInviteModal cid={cid} closeModal={toggleAddVisible} />
)}
<Layout
to={cid}
context="channel"
dropFiles={dropFiles}
// ref={containerRef}
aside={
<>
<ul className="tools">
{/* <li className="tool">
const { name, description, is_public, members = [], owner } = data;
const memberIds = is_public ? userIds : members;
const addVisible = loginUser?.is_admin || owner == loginUid;
console.log('channel message list', msgIds);
const readIndex = footprint.readChannels[cid];
const pinCount = data?.pinned_messages?.length || 0;
return (
<>
{addMemberModalVisible && <ChannelInviteModal cid={cid} closeModal={toggleAddVisible} />}
<Layout
to={cid}
context="channel"
dropFiles={dropFiles}
// ref={containerRef}
aside={
<>
<ul className="tools">
{/* <li className="tool">
<Tooltip tip="Search" placement="left">
<img src={searchIcon} alt="opt icon" />
</Tooltip>
</li> */}
<li className="tool">
<Tooltip tip="Voice/Video Chat" placement="left">
<IconHeadphone />
</Tooltip>
</li>
{/* <li className="tool">
<li className="tool">
<Tooltip tip="Voice/Video Chat" placement="left">
<IconHeadphone />
</Tooltip>
</li>
{/* <li className="tool">
<Tooltip tip="Notifications" placement="left">
<img src={alertIcon} alt="opt icon" />
</Tooltip>
</li> */}
<Tooltip
tip="Pin"
placement="left"
disabled={toolVisible == "pin"}
>
<Tippy
onShow={() => {
setToolVisible("pin");
}}
onHide={() => {
setToolVisible("");
}}
delay={[0, 0]}
duration={0}
placement="left-start"
popperOptions={{ strategy: "fixed" }}
offset={[0, 80]}
interactive
trigger="click"
content={<PinList id={cid} />}
>
<li
className={`tool ${pinCount > 0 ? "badge" : ""} ${
toolVisible == "pin" ? "active" : ""
} `}
data-count={pinCount}
>
<IconPin />
</li>
</Tippy>
</Tooltip>
<Tooltip
tip="Favorite"
placement="left"
disabled={toolVisible == "favorite"}
>
<Tippy
onShow={() => {
setToolVisible("favorite");
}}
onHide={() => {
setToolVisible("");
}}
delay={[0, 0]}
duration={0}
placement="left-start"
popperOptions={{ strategy: "fixed" }}
offset={[0, 180]}
interactive
trigger="click"
content={<FavList cid={cid} />}
>
<li
className={`tool ${
toolVisible == "favorite" ? "active" : ""
} `}
data-count={pinCount}
>
<IconFav />
</li>
</Tippy>
</Tooltip>
<li
className={`tool ${membersVisible ? "active" : ""}`}
onClick={toggleMembersVisible}
>
<Tooltip tip="Channel Members" placement="left">
<IconPeople />
</Tooltip>
</li>
</ul>
<hr className="divider" />
<ul className="apps">
<li className="app">
<Tooltip tip="Webrowse" placement="left">
<img src={webrowseIcon} alt="app icon" />
</Tooltip>
</li>
<li className="app">
<Tooltip tip="BoardOS" placement="left">
<img src={boardosIcon} alt="app icon" />
</Tooltip>
</li>
</ul>
</>
}
header={
<StyledHeader className="head">
<div className="txt">
<ChannelIcon personal={!is_public} />
<span className="title">{name}</span>
<span className="desc">{description}</span>
</div>
</StyledHeader>
}
contacts={
membersVisible ? (
<>
<StyledContacts>
{addVisible && (
<div className="add" onClick={toggleAddVisible}>
<img className="icon" src={addIcon} />
<div className="txt">Add members</div>
</div>
)}
{memberIds.map((uid) => {
return <Contact key={uid} uid={uid} dm popover />;
})}
</StyledContacts>
</>
) : null
}
>
<StyledChannelChat ref={ref}>
<div className="info">
<h2 className="title">Welcome to #{name} !</h2>
<p className="desc">This is the start of the #{name} channel. </p>
<NavLink
to={`/setting/channel/${cid}?f=${pathname}`}
className="edit"
>
<EditIcon className="icon" />
Edit Channel
</NavLink>
</div>
<div className="feed">
{[...msgIds]
.sort((a, b) => {
return Number(a) - Number(b);
})
.map((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
isFirst,
prev,
curr,
contextId: cid,
context: "channel",
});
})}
</div>
</StyledChannelChat>
{/* {unreads != 0 && (
<Tooltip tip="Pin" placement="left" disabled={toolVisible == 'pin'}>
<Tippy
onShow={() => {
setToolVisible('pin');
}}
onHide={() => {
setToolVisible('');
}}
delay={[0, 0]}
duration={0}
placement="left-start"
popperOptions={{ strategy: 'fixed' }}
offset={[0, 80]}
interactive
trigger="click"
content={<PinList id={cid} />}
>
<li
className={`tool ${pinCount > 0 ? 'badge' : ''} ${toolVisible == 'pin' ? 'active' : ''} `}
data-count={pinCount}
>
<IconPin />
</li>
</Tippy>
</Tooltip>
<Tooltip tip="Favorite" placement="left" disabled={toolVisible == 'favorite'}>
<Tippy
onShow={() => {
setToolVisible('favorite');
}}
onHide={() => {
setToolVisible('');
}}
delay={[0, 0]}
duration={0}
placement="left-start"
popperOptions={{ strategy: 'fixed' }}
offset={[0, 180]}
interactive
trigger="click"
content={<FavList cid={cid} />}
>
<li
className={`tool fav ${toolVisible == 'favorite' ? 'active' : ''} `}
data-count={pinCount}
>
<IconFav />
</li>
</Tippy>
</Tooltip>
<li className={`tool ${membersVisible ? 'active' : ''}`} onClick={toggleMembersVisible}>
<Tooltip tip="Channel Members" placement="left">
<IconPeople />
</Tooltip>
</li>
</ul>
<hr className="divider" />
<ul className="apps">
<li className="app">
<Tooltip tip="Webrowse" placement="left">
<img src={webrowseIcon} alt="app icon" />
</Tooltip>
</li>
<li className="app">
<Tooltip tip="BoardOS" placement="left">
<img src={boardosIcon} alt="app icon" />
</Tooltip>
</li>
</ul>
</>
}
header={
<StyledHeader className="head">
<div className="txt">
<ChannelIcon personal={!is_public} />
<span className="title">{name}</span>
<span className="desc">{description}</span>
</div>
</StyledHeader>
}
contacts={
membersVisible ? (
<>
<StyledContacts>
{addVisible && (
<div className="add" onClick={toggleAddVisible}>
<img className="icon" src={addIcon} />
<div className="txt">Add members</div>
</div>
)}
{memberIds.map((uid) => {
return <Contact key={uid} uid={uid} dm popover />;
})}
</StyledContacts>
</>
) : null
}
>
<StyledChannelChat ref={ref}>
<div className="info">
<h2 className="title">Welcome to #{name} !</h2>
<p className="desc">This is the start of the #{name} channel. </p>
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="edit">
<EditIcon className="icon" />
Edit Channel
</NavLink>
</div>
<div className="feed">
{[...msgIds]
.sort((a, b) => {
return Number(a) - Number(b);
})
.map((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
isFirst,
prev,
curr,
contextId: cid,
context: 'channel'
});
})}
</div>
</StyledChannelChat>
{/* {unreads != 0 && (
<StyledNotification>
<div className="content">
{unreads} new messages
@@ -266,7 +238,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
</button>
</StyledNotification>
)} */}
</Layout>
</>
);
</Layout>
</>
);
}
+175 -189
View File
@@ -1,199 +1,185 @@
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import { useSelector } from "react-redux";
import Tippy from "@tippyjs/react";
import useContextMenu from "../../../common/hook/useContextMenu";
import ContextMenu from "../../../common/component/ContextMenu";
import InviteModal from "../../../common/component/ChannelInviteModal";
import Tooltip from "../../../common/component/Tooltip";
import IconSetting from "../../../assets/icons/setting.svg";
import IconInvite from "../../../assets/icons/invite.from.channel.svg";
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDrop } from 'react-dnd';
import { NativeTypes } from 'react-dnd-html5-backend';
import { useSelector } from 'react-redux';
import Tippy from '@tippyjs/react';
import useContextMenu from '../../../common/hook/useContextMenu';
import ContextMenu from '../../../common/component/ContextMenu';
import InviteModal from '../../../common/component/ChannelInviteModal';
import Tooltip from '../../../common/component/Tooltip';
import IconSetting from '../../../assets/icons/setting.svg';
import IconInvite from '../../../assets/icons/invite.from.channel.svg';
// import { useDebounce} from "rooks";
import { useReadMessageMutation } from "../../../app/services/message";
import { useUpdateMuteSettingMutation } from "../../../app/services/contact";
import { useReadMessageMutation } from '../../../app/services/message';
import { useUpdateMuteSettingMutation } from '../../../app/services/contact';
import StyledLink from "./styled";
import ChannelIcon from "../../../common/component/ChannelIcon";
import { getUnreadCount } from "../utils";
import StyledLink from './styled';
import ChannelIcon from '../../../common/component/ChannelIcon';
import { getUnreadCount } from '../utils';
const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
const { pathname } = useLocation();
const [inviteModalVisible, setInviteModalVisible] = useState(false);
const navigate = useNavigate();
const [muteChannel] = useUpdateMuteSettingMutation();
const [updateReadIndex] = useReadMessageMutation();
const { pathname } = useLocation();
const [inviteModalVisible, setInviteModalVisible] = useState(false);
const navigate = useNavigate();
const [muteChannel] = useUpdateMuteSettingMutation();
const [updateReadIndex] = useReadMessageMutation();
const {
visible: contextMenuVisible,
offset,
handleContextMenuEvent,
hideContextMenu,
} = useContextMenu();
const {
channel,
mids,
messageData,
readIndex,
muted,
loginUid,
loginUser,
} = useSelector((store) => {
return {
loginUser: store.contacts.byId[store.authData.uid],
channel: store.channels.byId[id],
mids: store.channelMessage[id],
messageData: store.message,
loginUid: store.authData.uid,
readIndex: store.footprint.readChannels[id],
muted: store.footprint.muteChannels[id],
};
});
const handleChannelSetting = (evt) => {
evt.preventDefault();
evt.stopPropagation();
const { id } = evt.target.dataset;
if (id) {
navigate(`/setting/channel/${id}?f=${pathname}`);
const {
visible: contextMenuVisible,
offset,
handleContextMenuEvent,
hideContextMenu
} = useContextMenu();
const { channel, mids, messageData, readIndex, muted, loginUid, loginUser } = useSelector(
(store) => {
return {
loginUser: store.contacts.byId[store.authData.uid],
channel: store.channels.byId[id],
mids: store.channelMessage[id],
messageData: store.message,
loginUid: store.authData.uid,
readIndex: store.footprint.readChannels[id],
muted: store.footprint.muteChannels[id]
};
}
);
const handleChannelSetting = (evt) => {
evt.preventDefault();
evt.stopPropagation();
const { id } = evt.currentTarget.dataset;
if (id) {
navigate(`/setting/channel/${id}?f=${pathname}`);
}
};
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ dataTransfer }) {
if (dataTransfer.files.length) {
// console.log(files, rest);
setFiles([...dataTransfer.files]);
navigate(`/chat/channel/${id}`);
// 重置
setTimeout(() => {
setFiles([]);
}, 300);
}
},
collect: (monitor) => ({
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 toggleInviteModalVisible = (evt) => {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
setInviteModalVisible((prev) => !prev);
};
const handleMute = () => {
const data = muted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
muteChannel(data);
};
const { is_public, name, owner } = channel;
const { unreads = 0, mentions = [] } = getUnreadCount({
mids,
messageData,
readIndex,
loginUid
});
const isMentions = mentions.length !== 0;
const inviteIconVisible = loginUser?.is_admin || owner == loginUid;
return (
<>
<Tippy
interactive
placement="right-start"
popperOptions={{ strategy: 'fixed' }}
offset={[offset.y, offset.x]}
visible={contextMenuVisible}
onClickOutside={hideContextMenu}
key={id}
content={
<ContextMenu
hideMenu={hideContextMenu}
items={[
{
title: 'Mark As Read',
underline: true,
handler: handleReadAll
},
{
title: muted ? 'Unmute' : 'Mute',
handler: handleMute
},
{
title: 'Notification Settings',
underline: true
},
is_public
? null
: {
title: 'Invite People'
},
{
title: 'Delete Channel',
danger: true,
handler: toggleRemoveConfirm.bind(null, id)
}
]}
/>
}
};
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ dataTransfer }) {
if (dataTransfer.files.length) {
// console.log(files, rest);
setFiles([...dataTransfer.files]);
navigate(`/chat/channel/${id}`);
// 重置
setTimeout(() => {
setFiles([]);
}, 300);
}
},
collect: (monitor) => ({
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 toggleInviteModalVisible = (evt) => {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
setInviteModalVisible((prev) => !prev);
};
const handleMute = () => {
const data = muted
? { remove_groups: [id] }
: { add_groups: [{ gid: id }] };
muteChannel(data);
};
const { is_public, name, owner } = channel;
const { unreads = 0, mentions = [] } = getUnreadCount({
mids,
messageData,
readIndex,
loginUid,
});
const isMentions = mentions.length !== 0;
const inviteIconVisible = loginUser?.is_admin || owner == loginUid;
return (
<>
<Tippy
interactive
placement="right-start"
popperOptions={{ strategy: "fixed" }}
offset={[offset.y, offset.x]}
visible={contextMenuVisible}
onClickOutside={hideContextMenu}
key={id}
content={
<ContextMenu
hideMenu={hideContextMenu}
items={[
{
title: "Mark As Read",
underline: true,
handler: handleReadAll,
},
{
title: muted ? "Unmute" : "Mute",
handler: handleMute,
},
{
title: "Notification Settings",
underline: true,
},
is_public
? null
: {
title: "Invite People",
},
{
title: "Delete Channel",
danger: true,
handler: toggleRemoveConfirm.bind(null, id),
},
]}
/>
}
>
<StyledLink
data-cid={id}
onContextMenu={handleContextMenuEvent}
ref={drop}
key={id}
className={`link ${isActive ? "drop_over" : ""} ${
muted ? "muted" : ""
}`}
to={`/chat/channel/${id}`}
>
<div className={`name`} title={name}>
<ChannelIcon personal={!is_public} muted={muted} />
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
</div>
<div className="icons">
{inviteIconVisible && (
<Tooltip placement="bottom" tip="Add Member">
<IconInvite
className="icon invite"
data-id={id}
onClick={toggleInviteModalVisible}
></IconInvite>
</Tooltip>
)}
<Tooltip placement="bottom" tip="Channel Setting">
<IconSetting
className="icon setting"
data-id={id}
onClick={handleChannelSetting}
></IconSetting>
</Tooltip>
{unreads > 0 && (
<i className={`badge ${isMentions ? "mention" : ""}`}>
{isMentions ? mentions.length : unreads}
</i>
)}
</div>
</StyledLink>
</Tippy>
{inviteModalVisible && (
<InviteModal
cid={id}
title={channel.name}
closeModal={toggleInviteModalVisible}
/>
>
<StyledLink
data-cid={id}
onContextMenu={handleContextMenuEvent}
ref={drop}
key={id}
className={`link ${isActive ? 'drop_over' : ''} ${muted ? 'muted' : ''}`}
to={`/chat/channel/${id}`}
>
<div className={`name`} title={name}>
<ChannelIcon personal={!is_public} muted={muted} />
<span className={`txt ${unreads == 0 ? 'read' : ''}`}>{name}</span>
</div>
<div className="icons">
{inviteIconVisible && (
<Tooltip placement="bottom" tip="Add Member">
<IconInvite
className="icon invite"
data-id={id}
onClick={toggleInviteModalVisible}
></IconInvite>
</Tooltip>
)}
</>
);
<Tooltip placement="bottom" tip="Channel Setting">
<IconSetting
className="icon setting"
data-id={id}
onClick={handleChannelSetting}
></IconSetting>
</Tooltip>
{unreads > 0 && (
<i className={`badge ${isMentions ? 'mention' : ''}`}>
{isMentions ? mentions.length : unreads}
</i>
)}
</div>
</StyledLink>
</Tippy>
{inviteModalVisible && (
<InviteModal cid={id} title={channel.name} closeModal={toggleInviteModalVisible} />
)}
</>
);
};
export default NavItem;
+133 -130
View File
@@ -1,146 +1,149 @@
import styled from "styled-components";
import styled from 'styled-components';
const Styled = styled.article`
position: relative;
position: relative;
width: 100%;
background: #fff;
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
> .head {
box-sizing: border-box;
height: 56px;
padding: 0 20px;
/* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> .main {
height: calc(100vh - 56px - 22px);
width: 100%;
background: #fff;
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
> .head {
box-sizing: border-box;
height: 56px;
padding: 0 20px;
/* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
> .chat {
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
/* padding-bottom: 16px; */
height: calc(100vh - 56px - 22px);
> .send {
padding: 0 16px 16px 16px;
&.selecting {
padding: 0;
> .send {
display: none;
}
}
}
}
> .main {
height: calc(100vh - 56px - 22px);
width: 100%;
.members {
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
/* margin-top: 1px; */
/* border-top: 1px solid transparent; */
}
> .aside {
padding: 12px;
position: absolute;
right: 0;
top: -56px;
transform: translateX(100%);
display: flex;
flex-direction: column;
.divider {
border: none;
background-color: #d4d4d4;
width: 16px;
height: 1px;
margin: 18px auto;
}
.tools,
.apps {
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
> .chat {
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
/* padding-bottom: 16px; */
height: calc(100vh - 56px - 22px);
> .send {
padding: 0 16px 16px 16px;
&.selecting {
padding: 0;
> .send {
display: none;
}
}
}
}
.members {
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
/* margin-top: 1px; */
/* border-top: 1px solid transparent; */
}
> .aside {
padding: 12px;
flex-direction: column;
}
.tools {
gap: 24px;
.tool {
position: relative;
cursor: pointer;
&.fav svg path {
fill: #70707b;
}
&.active svg path {
fill: #3f3f46;
}
&:not(.active):hover svg path {
fill: #51525c;
}
&.badge:after {
position: absolute;
right: 0;
top: -56px;
transform: translateX(100%);
top: -8px;
right: -8px;
content: attr(data-count);
display: flex;
flex-direction: column;
.divider {
border: none;
background-color: #d4d4d4;
width: 16px;
height: 1px;
margin: 18px auto;
}
.tools,
.apps {
display: flex;
flex-direction: column;
}
.tools {
gap: 24px;
.tool {
position: relative;
cursor: pointer;
&.active svg path {
fill: #3f3f46;
}
&:not(.active):hover svg path {
fill: #51525c;
}
&.badge:after {
position: absolute;
top: -8px;
right: -8px;
content: attr(data-count);
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #22ccee;
color: #fff;
font-weight: 900;
font-size: 10px;
line-height: 10px;
text-align: center;
color: #ffffff;
}
}
}
.apps {
gap: 15px;
}
border-radius: 50%;
background-color: #22ccee;
color: #fff;
font-weight: 900;
font-size: 10px;
line-height: 10px;
text-align: center;
color: #ffffff;
}
}
}
.apps {
gap: 15px;
}
}
.drag_tip {
}
.drag_tip {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
/* pointer-events: none; */
&.visible {
visibility: visible;
}
.box {
padding: 16px;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
background: #52edff;
.inner {
padding: 16px;
padding-top: 64px;
border: 2px dashed #a5f3fc;
border-radius: 6px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
/* pointer-events: none; */
&.visible {
visibility: visible;
color: #fff;
.head {
font-weight: 600;
font-size: 20px;
line-height: 30px;
}
.box {
padding: 16px;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
background: #52edff;
.inner {
padding: 16px;
padding-top: 64px;
border: 2px dashed #a5f3fc;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
.head {
font-weight: 600;
font-size: 20px;
line-height: 30px;
}
.intro {
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
.intro {
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
}
}
`;
export default Styled;
+46 -55
View File
@@ -1,61 +1,52 @@
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import StyledWrapper from "./styled";
import { useNavigate, useParams } from "react-router-dom";
import { useLoginMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
import { setAuthData } from "../../app/slices/auth.data";
// import BASE_URL from "../../app/config";
// import { useCheckInviteTokenValidMutation } from "../../app/services/auth";
import StyledWrapper from './styled';
import { useNavigate, useParams } from 'react-router-dom';
import { useLoginMutation } from '../../app/services/auth';
import toast from 'react-hot-toast';
import { setAuthData } from '../../app/slices/auth.data';
export default function OAuthPage() {
const [
login,
{ data, isSuccess, isError, error: loginError },
] = useLoginMutation();
const { token } = useParams();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const dispatch = useDispatch();
// const [token, setToken] = useState("");
// const [valid, setValid] = useState(false);
// const [sp] = useSearchParams();
const navigateTo = useNavigate();
useEffect(() => {
const startOauth = () => {
if (!token) {
setError("Token Not Found");
return;
}
login({ key: token, type: "thirdparty" });
};
setTimeout(() => {
startOauth();
}, 1500);
}, [token]);
useEffect(() => {
if (isError) {
setError(loginError);
}
}, [isError, loginError]);
const [login, { data, isSuccess, isError, error: loginError }] = useLoginMutation();
const { token } = useParams();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const dispatch = useDispatch();
const navigateTo = useNavigate();
useEffect(() => {
const startOauth = () => {
if (!token) {
setError('Token Not Found');
return;
}
login({ key: token, type: 'thirdparty' });
};
setTimeout(() => {
startOauth();
}, 1500);
}, [token]);
useEffect(() => {
if (isError) {
setError(loginError);
}
}, [isError, loginError]);
useEffect(() => {
if (isSuccess && data) {
setLoading(false);
// 更新本地认证信息
console.log("login data", data);
toast.success("login success");
dispatch(setAuthData(data));
navigateTo("/");
}
}, [isSuccess, data]);
useEffect(() => {
if (isSuccess && data) {
setLoading(false);
// 更新本地认证信息
console.log('login data', data);
toast.success('login success');
dispatch(setAuthData(data));
navigateTo('/');
}
}, [isSuccess, data]);
return (
<StyledWrapper>
{loading ? "loading" : ""}
{error}
</StyledWrapper>
);
return (
<StyledWrapper>
{loading ? 'loading' : ''}
{error}
</StyledWrapper>
);
}