feat: pin message
This commit is contained in:
@@ -79,6 +79,28 @@ const channelsSlice = createSlice({
|
||||
break;
|
||||
}
|
||||
},
|
||||
updatePinMessage(state, action) {
|
||||
const { gid, mid, msg } = action.payload;
|
||||
let msgs = state.byId[gid]?.pinned_messages;
|
||||
if (!msgs) return;
|
||||
if (msg) {
|
||||
if (!msgs) {
|
||||
msgs = [msg];
|
||||
} else {
|
||||
const idx = msgs.findIndex((msg) => msg.mid == mid);
|
||||
if (idx > -1) {
|
||||
msgs.splice(idx, 1);
|
||||
}
|
||||
msgs.push(msg);
|
||||
}
|
||||
} else {
|
||||
// remove
|
||||
const idx = msgs.findIndex((msg) => msg.mid == mid);
|
||||
if (idx > -1) {
|
||||
msgs.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeChannel(state, action) {
|
||||
const gid = action.payload;
|
||||
const idx = state.ids.findIndex((i) => i == gid);
|
||||
@@ -90,6 +112,7 @@ const channelsSlice = createSlice({
|
||||
},
|
||||
});
|
||||
export const {
|
||||
updatePinMessage,
|
||||
resetChannels,
|
||||
fullfillChannels,
|
||||
addChannel,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M79.9997 13.3642C116.819 13.3642 146.666 43.2118 146.666 80.0308C146.666 116.85 116.819 146.698 79.9997 146.698C43.1807 146.698 13.333 116.85 13.333 80.0308C13.333 43.2118 43.1807 13.3642 79.9997 13.3642ZM79.9997 23.3642C48.7035 23.3642 23.333 48.7347 23.333 80.0308C23.333 111.327 48.7035 136.698 79.9997 136.698C111.296 136.698 136.666 111.327 136.666 80.0308C136.666 48.7347 111.296 23.3642 79.9997 23.3642ZM79.9997 86.6667C88.284 86.6667 94.9997 93.3824 94.9997 101.667C94.9997 109.951 88.284 116.667 79.9997 116.667C71.7154 116.667 64.9997 109.951 64.9997 101.667C64.9997 93.3824 71.7154 86.6667 79.9997 86.6667ZM60.0027 58.3411C64.6024 58.3411 68.3313 62.0699 68.3313 66.6697C68.3313 71.2694 64.6024 74.9983 60.0027 74.9983C55.4029 74.9983 51.6741 71.2694 51.6741 66.6697C51.6741 62.0699 55.4029 58.3411 60.0027 58.3411ZM100.003 58.3411C104.602 58.3411 108.331 62.0699 108.331 66.6697C108.331 71.2694 104.602 74.9983 100.003 74.9983C95.4029 74.9983 91.6741 71.2694 91.6741 66.6697C91.6741 62.0699 95.4029 58.3411 100.003 58.3411Z" fill="#D0D5DD"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -8,7 +8,7 @@ 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 DeleteMessageConfirm from "../DeleteMessageConfirm";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import replyIcon from "../../../assets/icons/reply.svg?url";
|
||||
import reactIcon from "../../../assets/icons/reaction.svg?url";
|
||||
@@ -17,6 +17,7 @@ import editIcon from "../../../assets/icons/edit.svg?url";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import ForwardModal from "../ForwardModal";
|
||||
import PinMessageModal from "./PinMessageModal";
|
||||
import usePinMessage from "../../hook/usePinMessage";
|
||||
const StyledCmds = styled.ul`
|
||||
z-index: 9999;
|
||||
position: absolute;
|
||||
@@ -63,6 +64,9 @@ export default function Commands({
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
||||
const [tippyVisible, setTippyVisible] = useState(false);
|
||||
const { canPin } = usePinMessage(
|
||||
context == "channel" ? contextId : undefined
|
||||
);
|
||||
const currUid = useSelector((store) => store.authData.uid);
|
||||
const cmdsRef = useRef(null);
|
||||
const handleReply = (fromMenu) => {
|
||||
@@ -92,6 +96,7 @@ export default function Commands({
|
||||
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
|
||||
hideAll();
|
||||
};
|
||||
const enablePin = context == "channel" && canPin;
|
||||
return (
|
||||
<StyledCmds
|
||||
ref={cmdsRef}
|
||||
@@ -139,7 +144,7 @@ export default function Commands({
|
||||
content={
|
||||
<StyledMenu className="menu">
|
||||
{/* <li className="item">Edit Message</li> */}
|
||||
{context == "channel" && (
|
||||
{enablePin && (
|
||||
<li className="item underline" onClick={togglePinModal}>
|
||||
Pin Message
|
||||
</li>
|
||||
@@ -169,7 +174,7 @@ export default function Commands({
|
||||
</Tippy>
|
||||
|
||||
{deleteModalVisible && (
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mids={mid} />
|
||||
)}
|
||||
{forwardModalVisible && (
|
||||
<ForwardModal mids={[mid]} closeModal={toggleForwardModal} />
|
||||
|
||||
@@ -33,9 +33,11 @@ export default function useNormalizeMessage() {
|
||||
: "";
|
||||
let user = { ...(data.users[from_user] || {}) };
|
||||
user.avatar =
|
||||
typeof user.avatar !== "undefined"
|
||||
user.avatar !== null
|
||||
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${user.avatar}`
|
||||
: "";
|
||||
|
||||
console.log("user data", transformedContent, user);
|
||||
return {
|
||||
user,
|
||||
content: transformedContent,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
usePinMessageMutation,
|
||||
useUnpinMessageMutation,
|
||||
} from "../../app/services/message";
|
||||
export default function usePinMessage(cid = null) {
|
||||
const [pins, setPins] = useState([]);
|
||||
const { channel, loginUser } = useSelector((store) => {
|
||||
return {
|
||||
channel: store.channels.byId[cid],
|
||||
loginUser: store.contacts.byId[store.authData.uid],
|
||||
};
|
||||
});
|
||||
const [pin, { isError, isLoading, isSuccess }] = usePinMessageMutation();
|
||||
const [
|
||||
unpin,
|
||||
{ isError: isUnpinError, isLoading: isUnpining, isSuccess: isUnpinSuccess },
|
||||
] = useUnpinMessageMutation();
|
||||
const pinMessage = (mid) => {
|
||||
if (!mid || !cid) return;
|
||||
pin({ mid, gid: +cid });
|
||||
};
|
||||
const unpinMessage = (mid) => {
|
||||
if (!mid || !cid) return;
|
||||
unpin({ mid, gid: +cid });
|
||||
};
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
setPins(channel.pinned_messages);
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
return {
|
||||
pins,
|
||||
canPin: loginUser.is_admin || channel?.owner == loginUser.uid,
|
||||
pinMessage,
|
||||
unpinMessage,
|
||||
isError,
|
||||
isPining: isLoading,
|
||||
isSuccess,
|
||||
isUnpinError,
|
||||
isUnpining,
|
||||
isUnpinSuccess,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// import React from "react";
|
||||
// import { useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import usePinMessage from "../../../common/hook/usePinMessage";
|
||||
import PreviewMessage from "../../../common/component/Message/PreviewMessage";
|
||||
import IconSurprise from "../../../assets/icons/emoji.suprise.svg";
|
||||
// import IconForward from "../../../assets/icons/forward.svg";
|
||||
import IconClose from "../../../assets/icons/close.svg";
|
||||
const Styled = styled.div`
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 12px;
|
||||
width: 406px;
|
||||
> .head {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #344054;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
> .none {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
.tip {
|
||||
width: 240px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
color: #475467;
|
||||
}
|
||||
}
|
||||
> .list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.pin {
|
||||
position: relative;
|
||||
border: 1px solid #f2f4f7;
|
||||
border-radius: var(--br);
|
||||
> .preview {
|
||||
background: none;
|
||||
.down img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
> .opts {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
.btn {
|
||||
display: flex;
|
||||
background: none;
|
||||
border: none;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
path {
|
||||
fill-opacity: 1;
|
||||
fill: #667085;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .opts {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default function PinList({ id }) {
|
||||
const { pins, unpinMessage, canPin } = usePinMessage(id);
|
||||
const handleUnpin = (evt) => {
|
||||
const { mid } = evt.currentTarget.dataset;
|
||||
console.log("unpin msg", mid);
|
||||
unpinMessage(+mid);
|
||||
};
|
||||
const noPins = pins.length == 0;
|
||||
return (
|
||||
<Styled>
|
||||
<h4 className="head">Pinned Message({pins.length})</h4>
|
||||
|
||||
{noPins ? (
|
||||
<div className="none">
|
||||
<IconSurprise />
|
||||
<div className="tip">
|
||||
This channel doesn’t have any pinned message yet.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list">
|
||||
{pins.map(({ mid }) => {
|
||||
return (
|
||||
<li key={mid} className="pin">
|
||||
<PreviewMessage mid={mid} />
|
||||
<div className="opts">
|
||||
{/* <button
|
||||
className="btn"
|
||||
data-mid={mid}
|
||||
// onClick={handleUnpin}
|
||||
>
|
||||
<IconForward />
|
||||
</button> */}
|
||||
{canPin && (
|
||||
<button
|
||||
className="btn"
|
||||
data-mid={mid}
|
||||
onClick={handleUnpin}
|
||||
>
|
||||
<IconClose />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useDebounce } from "rooks";
|
||||
import { useSelector } from "react-redux";
|
||||
import PinList from "./PinList";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import useChatScroll from "../../../common/hook/useChatScroll";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
@@ -9,7 +10,7 @@ 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 alertIcon from "../../../assets/icons/alert.svg?url";
|
||||
import peopleIcon from "../../../assets/icons/people.svg?url";
|
||||
import pinIcon from "../../../assets/icons/pin.svg?url";
|
||||
// import searchIcon from "../../../assets/icons/search.svg?url";
|
||||
@@ -25,8 +26,10 @@ import {
|
||||
} 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);
|
||||
@@ -70,6 +73,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
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 && (
|
||||
@@ -93,16 +97,40 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
<img src={headphoneIcon} alt="opt icon" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
<li className="tool">
|
||||
{/* <li className="tool">
|
||||
<Tooltip tip="Notifications" placement="left">
|
||||
<img src={alertIcon} alt="opt icon" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
<li className="tool">
|
||||
<Tooltip tip="Pin" placement="left">
|
||||
<img src={pinIcon} alt="opt icon" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
</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" : ""}`}
|
||||
data-count={pinCount}
|
||||
>
|
||||
<img src={pinIcon} alt="opt icon" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<li className="tool" onClick={toggleMembersVisible}>
|
||||
<Tooltip tip="Channel Members" placement="left">
|
||||
<img src={peopleIcon} alt="opt icon" />
|
||||
|
||||
@@ -65,7 +65,28 @@ const Styled = styled.article`
|
||||
.tools {
|
||||
gap: 24px;
|
||||
.tool {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&.badge:after {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
content: attr(data-count);
|
||||
display: flex;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user