chore: update favorite icon in different sences
This commit is contained in:
@@ -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,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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user