feat: forward message and channel invite

This commit is contained in:
zerosoul
2022-04-22 17:06:52 +08:00
parent b34eee726b
commit 5661ade09a
13 changed files with 307 additions and 64 deletions
+1
View File
@@ -8,6 +8,7 @@ export const ContentTypes = {
file: "rustchat/file",
formData: "multipart/form-data",
json: "application/json",
archive: "rustchat/archive",
};
export const firebaseConfig = {
apiKey: "AIzaSyDyJ6B1Ouenoha_gdGkBwIkBNStlwhlbO0",
+11 -8
View File
@@ -6,6 +6,7 @@ import { addUserMsg, removeUserMsg } from "../slices/message.user";
import { addMessage, removeMessage } from "../slices/message";
export const onMessageSendStarted = async (
{
ignoreLocal = false,
id,
content,
type = "text",
@@ -16,9 +17,11 @@ export const onMessageSendStarted = async (
{ dispatch, queryFulfilled },
from = "channel"
) => {
// 忽略archive类型的消息
if (type == "archive") return;
// id: who send to ,from_uid: who sent
console.log("handlers data", content, type, properties);
const isImage = properties.file_type?.startsWith("image");
console.log("handlers data", content, type, properties, ignoreLocal, id);
const isImage = properties.content_type?.startsWith("image");
const ts = properties.local_id || new Date().getTime();
// let imageData = null;
// if (type == "image") {
@@ -39,17 +42,17 @@ export const onMessageSendStarted = async (
properties,
from_uid,
reply_mid,
// 已读
read: true,
sending: true,
};
const addContextMessage = from == "channel" ? addChannelMsg : addUserMsg;
const removeContextMessage =
from == "channel" ? removeChannelMsg : removeUserMsg;
batch(() => {
dispatch(addMessage({ mid: ts, ...tmpMsg }));
dispatch(addContextMessage({ id, mid: ts }));
});
if (!ignoreLocal) {
batch(() => {
dispatch(addMessage({ mid: ts, ...tmpMsg }));
dispatch(addContextMessage({ id, mid: ts }));
});
}
try {
const { data: server_mid } = await queryFulfilled;
+14
View File
@@ -45,6 +45,13 @@ export const messageApi = createApi({
body: meta,
}),
}),
createArchive: builder.mutation({
query: (mids = []) => ({
url: `/resource/archive`,
method: "POST",
body: { mid_list: mids },
}),
}),
uploadFile: builder.mutation({
query: (formData) => ({
// headers: {
@@ -64,6 +71,11 @@ export const messageApi = createApi({
url: `/resource/open_graphic_parse?url=${encodeURIComponent(url)}`,
}),
}),
getArchiveMessage: builder.query({
query: (file_path) => ({
url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`,
}),
}),
replyMessage: builder.mutation({
query: ({ reply_mid, content, type = "text" }) => ({
headers: {
@@ -102,6 +114,7 @@ export const messageApi = createApi({
});
export const {
useGetArchiveMessageQuery,
useLazyGetOGInfoQuery,
usePrepareUploadFileMutation,
useUploadFileMutation,
@@ -110,4 +123,5 @@ export const {
useReplyMessageMutation,
useLazyDeleteMessageQuery,
useReadMessageMutation,
useCreateArchiveMutation,
} = messageApi;
+4
View File
@@ -74,6 +74,9 @@ export const serverApi = createApi({
getSMTPConfig: builder.query({
query: () => ({ url: `admin/smtp/config` }),
}),
getSMTPStatus: builder.query({
query: () => ({ url: `/admin/smtp/enabled` }),
}),
updateSMTPConfig: builder.mutation({
query: (data) => ({
url: `admin/smtp/config`,
@@ -148,6 +151,7 @@ export const serverApi = createApi({
});
export const {
useGetSMTPStatusQuery,
useSendTestEmailMutation,
useUpdateFirebaseConfigMutation,
useGetFirebaseConfigQuery,
+75
View File
@@ -0,0 +1,75 @@
// import React from 'react';
import styled from "styled-components";
import { useSelector } from "react-redux";
// import { useNavigate } from "react-router-dom";
import Avatar from "./Avatar";
const StyledWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 8px;
border-radius: 8px;
user-select: none;
&.compact {
padding: 0;
}
&.interactive {
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
}
.avatar {
cursor: pointer;
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
position: relative;
img {
border-radius: 50%;
width: 100%;
height: 100%;
}
}
.name {
/* user-select: text; */
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
}
`;
export default function Channel({
interactive = true,
id = "",
compact = false,
avatarSize = 32,
}) {
const { channel, totalMemberCount } = useSelector((store) => {
return {
channel: store.channels.byId[id],
totalMemberCount: store.contacts.ids.length,
};
});
console.log("channel item", id, channel);
if (!channel) return null;
const { name, members = [], is_public, avatar } = channel;
return (
<StyledWrapper
size={avatarSize}
className={`${interactive ? "interactive" : ""} ${
compact ? "compact" : ""
}`}
>
<div className="avatar">
<Avatar type="channel" url={avatar} name={"#"} alt="avatar" />
</div>
{!compact && (
<span className="name">
{name} ({is_public ? totalMemberCount : members.length})
</span>
)}
</StyledWrapper>
);
}
@@ -45,7 +45,9 @@ export default function ChannelInviteModal({
Add friends to #{title || channel?.name}{" "}
<CloseIcon className="close" onClick={closeModal} />
</h2>
{!channel?.is_public && <AddMembers cid={cid} />}
{!channel?.is_public && (
<AddMembers cid={cid} closeModal={closeModal} />
)}
<InviteByEmail cid={cid} />
</Styled>
</Modal>
+4 -7
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import Modal from "../Modal";
import Button from "../styled/Button";
import ChannelIcon from "../ChannelIcon";
@@ -10,7 +10,6 @@ import StyledWrapper from "./styled";
import StyledToggle from "../../component/styled/Toggle";
import StyledCheckbox from "../../component/styled/Checkbox";
import useFilteredUsers from "../../hook/useFilteredUsers";
import { addChannel } from "../../../app/slices/channels";
import { useCreateChannelMutation } from "../../../app/services/channel";
@@ -19,7 +18,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
return { conactsData: store.contacts.byId, loginUid: store.authData.uid };
});
const navigateTo = useNavigate();
const dispatch = useDispatch();
const [data, setData] = useState({
name: "",
dsecription: "",
@@ -29,7 +27,7 @@ export default function ChannelModal({ personal = false, closeModal }) {
const { contacts, input, updateInput } = useFilteredUsers();
const [
createChannel,
{ isSuccess, isError, isLoading, data: newChannel },
{ isSuccess, isError, isLoading, data: newChannelId },
] = useCreateChannelMutation();
const handleToggle = () => {
@@ -59,10 +57,9 @@ export default function ChannelModal({ personal = false, closeModal }) {
if (isSuccess) {
toast.success("create new channel success");
closeModal();
dispatch(addChannel(newChannel));
navigateTo(`/chat/channel/${newChannel.gid}`);
navigateTo(`/chat/channel/${newChannelId}`);
}
}, [isSuccess, newChannel]);
}, [isSuccess, newChannelId]);
const handleNameInput = (evt) => {
setData((prev) => {
+121 -35
View File
@@ -1,41 +1,79 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
// import { useSelector, useDispatch } from "react-redux";
import { useState } from "react";
// import toast from "react-hot-toast";
import Modal from "../Modal";
import Button from "../styled/Button";
import Input from "../styled/Input";
import Channel from "../Channel";
import Contact from "../Contact";
// import Channel from "../Channel";
import Reply from "../Message/Reply";
import StyledWrapper from "./styled";
import useForwardMessage from "../../hook/useForwardMessage";
import useSendMessage from "../../hook/useSendMessage";
import useFilteredChannels from "../../hook/useFilteredChannels";
import useFilteredUsers from "../../hook/useFilteredUsers";
import CloseIcon from "../../../assets/icons/close.circle.svg";
import StyledCheckbox from "../../component/styled/Checkbox";
import useFilteredUsers from "../../hook/useFilteredUsers";
// import { addChannel } from "../../../app/slices/channels";
// import { useCreateChannelMutation } from "../../../app/services/channel";
import toast from "react-hot-toast";
export default function ForwardModal({ mid, closeModal }) {
const [members, setMembers] = useState([]);
const [appendText, setAppendText] = useState("");
const { sendMessages } = useSendMessage();
const { forwardMessage, forwarding } = useForwardMessage();
const [selectedMembers, setSelectedMembers] = useState([]);
const [selectedChannels, setSelectedChannels] = useState([]);
const {
channels,
// input: channelInput,
updateInput: updateChannelInput,
} = useFilteredChannels();
const { contacts, input, updateInput } = useFilteredUsers();
// const { conactsData, loginUid } = useSelector((store) => {
// return { conactsData: store.contacts.byId, loginUid: store.authData.uid };
// });
const handleInputChange = (evt) => {
updateInput(evt.target.value);
const toggleCheck = ({ currentTarget }) => {
const { id, type = "user" } = currentTarget.dataset;
const ids = type == "user" ? selectedMembers : selectedChannels;
const updateState =
type == "user" ? setSelectedMembers : setSelectedChannels;
let tmp = ids.includes(+id) ? ids.filter((m) => m != id) : [...ids, +id];
console.log(id, currentTarget);
updateState(tmp);
};
const toggleCheckMember = ({ currentTarget }) => {
const { uid } = currentTarget.dataset;
let tmp = members.includes(+uid)
? members.filter((m) => m != uid)
: [...members, +uid];
console.log(uid, currentTarget);
setMembers(tmp);
const updateAppendText = (evt) => {
setAppendText(evt.target.value);
};
const removeSelected = (uid) => {
setMembers(members.filter((m) => m != uid));
const handleForward = async () => {
await forwardMessage({
mids: [mid],
users: selectedMembers,
channels: selectedChannels,
});
if (appendText) {
await sendMessages({
content: appendText,
users: selectedMembers,
channels: selectedChannels,
});
}
toast.success("Forward Message Successfully");
closeModal();
};
const selectedCount = members.length ? `(${members.length})` : null;
const removeSelected = (id, from = "user") => {
if (from == "user") {
setSelectedMembers(selectedMembers.filter((m) => m != id));
} else {
setSelectedChannels(selectedChannels.filter((cid) => cid != id));
}
};
const handleSearchChange = (evt) => {
const newVal = evt.target.value;
updateChannelInput(newVal);
updateInput(newVal);
};
let selectedCount = selectedMembers.length + selectedChannels.length;
const sendButtonDisabled =
(selectedChannels.length == 0 && selectedMembers.length == 0) || forwarding;
return (
<Modal>
<StyledWrapper>
@@ -43,22 +81,46 @@ export default function ForwardModal({ mid, closeModal }) {
<div className="search">
<input
value={input}
onChange={handleInputChange}
onChange={handleSearchChange}
placeholder="Search user or channel"
/>
</div>
{contacts && (
<ul className="users">
{contacts.map((u) => {
<ul className="users">
{channels &&
channels.map((c) => {
const { gid } = c;
const checked = selectedChannels.includes(gid);
console.log({ checked });
return (
<li
key={gid}
data-type="channel"
data-id={gid}
className="user channel"
onClick={toggleCheck}
>
<StyledCheckbox
readOnly
checked={checked}
name="cb"
id="cb"
/>
<Channel id={gid} interactive={false} />
</li>
);
})}
{contacts &&
contacts.map((u) => {
const { uid } = u;
const checked = members.includes(uid);
const checked = selectedMembers.includes(uid);
console.log({ checked });
return (
<li
key={uid}
data-uid={uid}
data-id={uid}
data-type="user"
className="user"
onClick={toggleCheckMember}
onClick={toggleCheck}
>
<StyledCheckbox
readOnly
@@ -70,13 +132,28 @@ export default function ForwardModal({ mid, closeModal }) {
</li>
);
})}
</ul>
)}
</ul>
</div>
<div className={`right`}>
<h3 className="title">Send To {selectedCount}</h3>
<ul className="selected">
{members.map((uid) => {
{selectedChannels.map((cid) => {
return (
<li key={cid} className="item">
<Channel
key={cid}
id={cid}
interactive={false}
avatarSize={40}
/>
<CloseIcon
className="remove"
onClick={removeSelected.bind(null, cid, "channel")}
/>
</li>
);
})}
{selectedMembers.map((uid) => {
return (
<li key={uid} className="item">
<Contact
@@ -87,7 +164,7 @@ export default function ForwardModal({ mid, closeModal }) {
/>
<CloseIcon
className="remove"
onClick={removeSelected.bind(null, uid)}
onClick={removeSelected.bind(null, uid, "user")}
/>
</li>
);
@@ -96,13 +173,22 @@ export default function ForwardModal({ mid, closeModal }) {
<div className="reply">
<Reply mid={mid} interactive={false} />
</div>
<Input className="input" placeholder="Leave a message"></Input>
<Input
className="input"
placeholder="Leave a message"
value={appendText}
onChange={updateAppendText}
></Input>
<div className="btns">
<Button onClick={closeModal} className="normal cancel">
Cancel
</Button>
<Button className="normal" disabled={members.length == 0}>
Send To {selectedCount}
<Button
className="normal"
disabled={sendButtonDisabled}
onClick={handleForward}
>
Send To {selectedCount == 0 ? null : `(${selectedCount})`}
</Button>
</div>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@ const StyledWrapper = styled.div`
transition: all 0.5s ease;
overflow: hidden;
.left {
width: 260px;
width: 276px;
/* height: 100%; */
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
overflow-y: scroll;
+1 -1
View File
@@ -105,7 +105,7 @@ export default function URLPreview({ url = "" }) {
getMetaData(url);
}, [url]);
if (!url || !data) return null;
if (!url || !data || !data.title) return null;
const { favicon, title, description, ogImage } = data;
return ogImage ? (
<StyledDetails href={url} target="_blank">
+59
View File
@@ -0,0 +1,59 @@
import { useState } from "react";
import { useSendChannelMsgMutation } from "../../app/services/channel";
import { useSendMsgMutation } from "../../app/services/contact";
import { useCreateArchiveMutation } from "../../app/services/message";
export default function useForwardMessage() {
const [forwarding, setForwarding] = useState(false);
const [
createArchive,
{
isError: createArchiveError,
isLoading: creatingArchive,
isSuccess: createArchiveSuccess,
},
] = useCreateArchiveMutation();
const [
sendChannelMsg,
{
isLoading: channelSending,
isSuccess: channelSuccess,
isError: channelError,
},
] = useSendChannelMsgMutation();
const [
sendUserMsg,
{ isLoading: userSending, isSuccess: userSuccess, isError: userError },
] = useSendMsgMutation();
const forwardMessage = async ({ mids = [], users = [], channels = [] }) => {
setForwarding(true);
const { data: archive_id } = await createArchive(mids);
if (users.length) {
for await (const uid of users) {
await sendUserMsg({
type: "archive",
id: uid,
content: archive_id,
// from_uid: from,
});
}
}
if (channels.length) {
for await (const cid of channels) {
await sendChannelMsg({
type: "archive",
id: cid,
content: archive_id,
// from_uid: from,
});
}
}
setForwarding(false);
};
return {
forwardMessage,
forwarding,
isError: channelError || userError || createArchiveError,
isSending: userSending || channelSending || creatingArchive,
isSuccess: channelSuccess || userSuccess || createArchiveSuccess,
};
}
+9 -9
View File
@@ -2,15 +2,15 @@ import { useState, useEffect } from "react";
import useCopy from "./useCopy";
import {
useLazyCreateInviteLinkQuery as useCreateServerInviteLinkQuery,
useGetSMTPConfigQuery,
useGetSMTPStatusQuery,
} from "../../app/services/server";
import { useLazyCreateInviteLinkQuery as useCreateChannelInviteLinkQuery } from "../../app/services/channel";
export default function useInviteLink(cid = null) {
const [finalLink, setFinalLink] = useState("");
const {
data: config,
isSuccess: configQuerySuccess,
} = useGetSMTPConfigQuery();
data: SMTPEnabled,
isSuccess: smtpStatusFetchSuccess,
} = useGetSMTPStatusQuery();
const [
generateChannelInviteLink,
{ data: channelInviteLink, isLoading: generatingChannelLink },
@@ -39,15 +39,15 @@ export default function useInviteLink(cid = null) {
useEffect(() => {
const _link = serverInviteLink || channelInviteLink;
console.log("fetching", serverInviteLink, channelInviteLink);
if (_link && config) {
if (_link && smtpStatusFetchSuccess) {
const tmpURL = new URL(_link);
tmpURL.searchParams.set("code", config.enabled);
tmpURL.searchParams.set("code", SMTPEnabled);
setFinalLink(tmpURL.href);
}
}, [serverInviteLink, channelInviteLink, config]);
}, [serverInviteLink, channelInviteLink, smtpStatusFetchSuccess]);
return {
enableSMTP: config?.enabled,
generating: generatingChannelLink || generatingServerLink,
enableSMTP: SMTPEnabled,
generating: cid ? generatingChannelLink : generatingServerLink,
generateNewLink: regenLink,
link: finalLink,
linkCopied,
+4 -2
View File
@@ -82,8 +82,10 @@ const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
}
};
const toggleInviteModalVisible = (evt) => {
evt.preventDefault();
evt.stopPropagation();
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
setInviteModalVisible((prev) => !prev);
};
const handleMute = () => {