feat: pin message

This commit is contained in:
zerosoul
2022-04-28 10:52:16 +08:00
parent 457b5e4f2f
commit 5204c58d13
10 changed files with 374 additions and 125 deletions
+23
View File
@@ -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,
+3
View File
@@ -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 -3
View File
@@ -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} />
+3 -1
View File
@@ -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,
+46
View File
@@ -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,
};
}
+134
View File
@@ -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 doesnt 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>
);
}
+36 -8
View File
@@ -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" />
+21
View File
@@ -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 {