feat: lots updates

This commit is contained in:
zerosoul
2022-03-22 16:10:49 +08:00
parent eef55e5cb3
commit a23800b042
40 changed files with 504 additions and 415 deletions
+6 -2
View File
@@ -30,10 +30,14 @@ const useRehydrate = () => {
// console.log("iterated", key);
switch (_key) {
case "channels":
rehydrateData.channels.push(data);
if (data) {
rehydrateData.channels.push(data);
}
break;
case "contacts":
rehydrateData.contacts.push(data);
if (data) {
rehydrateData.contacts.push(data);
}
break;
case "footprint":
rehydrateData.footprint[key] = data;
+1 -1
View File
@@ -1,6 +1,6 @@
// const BASE_URL = `${location.origin}/api`;
const BASE_URL = `https://dev.rustchat.com/api`;
export const CACHE_VERSION = `0.1`;
export const CACHE_VERSION = `0.2.1`;
// const BASE_URL = `https://rustchat.net/api`;
export const ContentTypes = {
text: "text/plain",
+10
View File
@@ -1,6 +1,7 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { nanoid } from "@reduxjs/toolkit";
import baseQuery from "./base.query";
import { updateToken, resetAuthData } from "../slices/auth.data";
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
const getDeviceId = () => {
let d = localStorage.getItem(KEY_DEVICE_KEY);
@@ -43,6 +44,15 @@ export const authApi = createApi({
refresh_token: refreshToken,
},
}),
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
dispatch(updateToken(data));
} catch {
dispatch(resetAuthData());
console.log("remove channel error");
}
},
}),
// 获取openid
getOpenid: builder.mutation({
+9 -1
View File
@@ -78,7 +78,7 @@ export const channelApi = createApi({
query: ({ id, content, type = "text", properties = "" }) => ({
headers: {
"content-type": ContentTypes[type],
"X-Properties": properties,
"X-Properties": properties ? btoa(JSON.stringify(properties)) : "",
},
url: `group/${id}/send`,
method: "POST",
@@ -95,6 +95,13 @@ export const channelApi = createApi({
body: members,
}),
}),
removeMembers: builder.mutation({
query: ({ id, members }) => ({
url: `group/${id}/members/remove`,
method: "POST",
body: members,
}),
}),
}),
});
@@ -106,4 +113,5 @@ export const {
useCreateChannelMutation,
useSendChannelMsgMutation,
useAddMembersMutation,
useRemoveMembersMutation,
} = channelApi;
+1 -2
View File
@@ -32,7 +32,6 @@ export const contactApi = createApi({
// 用户已注销或被禁用
console.log("no matched user, redirect to login");
dispatch(resetAuthData());
// navigate("/login");
} else {
const markedContacts = contacts.map((u) => {
return u.uid == matchedUser.uid ? { ...u, online: true } : u;
@@ -76,7 +75,7 @@ export const contactApi = createApi({
query: ({ id, content, type = "text", properties = "" }) => ({
headers: {
"content-type": ContentTypes[type],
"X-Properties": properties,
"X-Properties": properties ? btoa(JSON.stringify(properties)) : "",
},
url: `user/${id}/send`,
method: "POST",
+10 -2
View File
@@ -5,16 +5,24 @@ import { addChannelMsg, removeChannelMsg } from "../slices/message.channel";
import { addUserMsg, removeUserMsg } from "../slices/message.user";
import { addMessage, removeMessage } from "../slices/message";
export const onMessageSendStarted = async (
{ id, content, type = "text", from_uid, reply_mid = null },
{
id,
content,
type = "text",
from_uid,
reply_mid = null,
properties = { local_id: new Date().getTime() },
},
{ dispatch, queryFulfilled },
from = "channel"
) => {
// id: who send to ,from_uid: who sent
const ts = new Date().getTime();
const ts = properties.local_id || new Date().getTime();
const tmpMsg = {
content: type == "image" ? URL.createObjectURL(content) : content,
content_type: ContentTypes[type],
created_at: ts,
properties,
from_uid,
reply_mid,
// 已读
+1
View File
@@ -48,6 +48,7 @@ const authDataSlice = createSlice({
setUid(state, action) {
const uid = action.payload;
state.uid = uid;
console.log("set uid orginal");
},
updateToken(state, action) {
const { token, refresh_token, expired_in } = action.payload;
+9 -3
View File
@@ -12,9 +12,12 @@ const channelMsgSlice = createSlice({
return action.payload;
},
addChannelMsg(state, action) {
const { id, mid } = action.payload;
const { id, mid, local_id = null } = action.payload;
if (state[id]) {
if (state[id].findIndex((id) => id == mid) > -1) return;
const midExsited = state[id].findIndex((id) => id == mid) > -1;
const localMsgExsited =
state[id].findIndex((id) => id == local_id) > -1;
if (midExsited || localMsgExsited) return;
state[id].push(+mid);
} else {
state[id] = [+mid];
@@ -24,7 +27,10 @@ const channelMsgSlice = createSlice({
const { id, mid } = action.payload;
if (state[id]) {
const idx = state[id].findIndex((i) => i == mid);
state[id].splice(idx, 1);
if (idx > -1) {
// 存在 则再删除
state[id].splice(idx, 1);
}
}
},
removeChannelSession(state, action) {
+1 -1
View File
@@ -3,7 +3,7 @@ import { createSlice } from "@reduxjs/toolkit";
const initialState = {
online: true,
ready: false,
menuExpand: true,
menuExpand: false,
setting: false,
channelSetting: null,
};
@@ -10,6 +10,7 @@ import Label from "../styled/Label";
import Textarea from "../styled/Textarea";
import SaveTip from "../SaveTip";
import channelIcon from "../../../assets/icons/channel.svg?url";
import { useSelector } from "react-redux";
const StyledWrapper = styled.div`
position: relative;
width: 512px;
@@ -30,7 +31,7 @@ const StyledWrapper = styled.div`
gap: 8px;
.name {
padding-left: 36px;
background: url(${channelIcon});
background-image: url(${channelIcon});
background-size: 20px;
background-position-x: 8px;
background-position-y: 8px;
@@ -40,6 +41,9 @@ const StyledWrapper = styled.div`
}
`;
export default function Overview({ id = 0 }) {
const loginUser = useSelector(
(store) => store.contacts.byId[store.authData.uid]
);
const { data, refetch } = useGetChannelQuery(id);
const [changed, setChanged] = useState(false);
const [values, setValues] = useState(null);
@@ -85,12 +89,14 @@ export default function Overview({ id = 0 }) {
if (!values || !id) return null;
const { name, description } = values;
const isAdmin = loginUser.is_admin;
return (
<StyledWrapper>
<div className="inputs">
<div className="input">
<Label htmlFor="name">Channel Name</Label>
<Input
disabled={!isAdmin}
className="name"
data-type="name"
onChange={handleChange}
@@ -103,6 +109,7 @@ export default function Overview({ id = 0 }) {
<div className="input">
<Label htmlFor="desc">Channel Topic</Label>
<Textarea
disabled={!isAdmin}
data-type="description"
onChange={handleChange}
value={description ?? ""}
+19 -27
View File
@@ -1,15 +1,6 @@
import Overview from "./Overview";
import ManageMembers from "../ManageMembers";
import { useSelector } from "react-redux";
const useNavs = (channelId) => {
const { channels, contactIds } = useSelector((store) => {
return {
channels: store.channels.byId,
contactIds: store.contacts.ids,
};
});
let ids = channels[channelId]?.members ?? [];
ids = ids.length == 0 ? contactIds : ids;
const navs = [
{
title: "General",
@@ -19,30 +10,31 @@ const useNavs = (channelId) => {
title: "Overview",
component: <Overview id={channelId} />,
},
{
name: "permissions",
title: "Permissions",
},
{
name: "invites",
title: "Invites",
},
{
name: "integrations",
title: "Integrations",
},
],
},
{
title: "User Management",
items: [
{
name: "members",
title: "Members",
component: <ManageMembers members={ids} />,
component: <ManageMembers cid={channelId} />,
},
// {
// name: "permissions",
// title: "Permissions",
// },
// {
// name: "invites",
// title: "Invites",
// },
// {
// name: "integrations",
// title: "Integrations",
// },
],
},
// {
// title: "User Management",
// items: [
// ],
// },
];
return navs;
};
-2
View File
@@ -2,7 +2,6 @@
import styled from "styled-components";
import { useSelector } from "react-redux";
import Tippy from "@tippyjs/react";
import "tippy.js/animations/scale-subtle.css";
import Avatar from "./Avatar";
import Profile from "./Profile";
@@ -75,7 +74,6 @@ export default function Contact({
>
<Tippy
inertia={true}
animation="scale"
interactive
disabled={!popover}
placement="left"
+1 -9
View File
@@ -1,13 +1,5 @@
// import React from "react";
import dayjs from "dayjs";
export default function FAQ() {
console.log("build time", process.env);
return (
<div>
Build Time:
{dayjs(new Date(Number(process.env.REACT_APP_BUILD_TIME + "000"))).format(
"YYYY-MM-DD HH:mm:ss"
)}
</div>
);
return <div>Build Timestamp: {process.env.REACT_APP_BUILD_TIME}</div>;
}
+1 -1
View File
@@ -27,7 +27,7 @@ const StyledWrapper = styled.div`
gap: 10px;
img {
max-width: 70vw;
max-height: 70vw;
max-height: 80vh;
}
.origin {
font-weight: bold;
+1
View File
@@ -16,6 +16,7 @@ const StyledWrapper = styled.div`
margin-bottom: 8px;
}
.link {
width: 512px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
+69 -44
View File
@@ -1,10 +1,13 @@
import { useState, useRef, useEffect } from "react";
import { useEffect } from "react";
import styled from "styled-components";
import { useOutsideClick } from "rooks";
// import { useOutsideClick } from "rooks";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import { useSelector } from "react-redux";
import toast from "react-hot-toast";
import useCopy from "../hook/useCopy";
import { useLazyDeleteContactQuery } from "../../app/services/contact";
import { useRemoveMembersMutation } from "../../app/services/channel";
import Contact from "./Contact";
import StyledMenu from "./StyledMenu";
import InviteLink from "./InviteLink";
@@ -12,7 +15,9 @@ import moreIcon from "../../assets/icons/more.svg?url";
const StyledWrapper = styled.section`
display: flex;
flex-direction: column;
width: 512px;
width: 100%;
height: calc(100vh - 94px);
overflow-y: scroll;
.intro {
display: flex;
flex-direction: column;
@@ -34,6 +39,8 @@ const StyledWrapper = styled.section`
display: flex;
flex-direction: column;
gap: 24px;
width: 512px;
margin-bottom: 176px;
.member {
width: 100%;
display: flex;
@@ -87,38 +94,55 @@ const StyledWrapper = styled.section`
}
}
`;
export default function ManageMembers({ members = [] }) {
const contacts = useSelector((store) => store.contacts);
const [copied, copy] = useCopy();
const [remove, { isSuccess: removeSuccess }] = useLazyDeleteContactQuery();
const wrapperRef = useRef(null);
const [menuVisible, setMenuVisible] = useState(null);
useOutsideClick(wrapperRef, () => {
setMenuVisible(null);
export default function ManageMembers({ cid = null }) {
const { contacts, channels, loginUser } = useSelector((store) => {
return {
contacts: store.contacts,
channels: store.channels,
loginUser: store.contacts.byId[store.authData.uid],
};
});
const toggleMenu = (evt) => {
const { uid } = evt.target.dataset;
if (menuVisible == uid) {
setMenuVisible(null);
} else {
setMenuVisible(uid);
}
};
const [copied, copy] = useCopy();
const [
removeUser,
{ isSuccess: removeSuccess },
] = useLazyDeleteContactQuery();
const [
removeMemberFromChannel,
{ isSuccess: removeMemberSuccess },
] = useRemoveMembersMutation();
const remove = cid ? removeMemberFromChannel : removeUser;
const handleRemoveUser = (uid) => {
remove(uid);
remove(cid ? { id: cid, members: [+uid] } : uid);
};
useEffect(() => {
if (removeSuccess) {
toast.success("delete successfully");
}
}, [removeSuccess]);
useEffect(() => {
if (removeMemberSuccess) {
toast.success("remove member successfully");
}
}, [removeMemberSuccess]);
const handleCopy = (str) => {
copy(str);
// const tippyInstance = tippyRef.current._tippy;
setTimeout(() => {
console.log("tippy close");
hideAll();
}, 800);
};
const uids = !members || members.length == 0 ? contacts.ids : members;
const channel = channels.byId[cid] ?? null;
const uids = channel
? channel.is_public
? contacts.ids
: channel.members
: contacts.ids;
return (
<StyledWrapper>
<InviteLink />
{loginUser?.is_admin && <InviteLink />}
<div className="intro">
<h4 className="title">Manage Members</h4>
<p className="desc">
@@ -140,35 +164,36 @@ export default function ManageMembers({ members = [] }) {
</div>
<div className="right">
<span className="role">{is_admin ? "Admin" : "User"}</span>
<div className="opts">
<img
data-uid={uid}
onClick={toggleMenu}
className="dots"
src={moreIcon}
alt="dots icon"
/>
{menuVisible == uid && (
<StyledMenu ref={wrapperRef} className="menu">
<Tippy
interactive
placement="right-start"
trigger="click"
content={
<StyledMenu className="menu">
<li
className="item"
onClick={handleCopy.bind(null, email)}
>
{copied ? "Copied" : `Copy Email`}
</li>
<li className="item">Mute</li>
<li className="item underline">Change Nickname</li>
<li className="item danger">Ban</li>
<li
className="item danger"
onClick={handleRemoveUser.bind(null, uid)}
data-uid={uid}
>
Remove
</li>
{/* <li className="item underline">Mute</li> */}
{/* <li className="item underline">Change Nickname</li> */}
{/* <li className="item danger">Ban</li> */}
{loginUser?.is_admin && (
<li
className="item danger"
onClick={handleRemoveUser.bind(null, uid)}
>
Remove
</li>
)}
</StyledMenu>
)}
</div>
}
>
<div className="opts">
<img className="dots" src={moreIcon} alt="dots icon" />
</div>
</Tippy>
</div>
</li>
);
+52 -39
View File
@@ -1,8 +1,9 @@
import { useState, useRef } 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 { useOutsideClick } from "rooks";
import { addReplyingMessage } from "../../../app/slices/message";
import StyledMenu from "../StyledMenu";
import DeleteMessageConfirm from "./DeleteMessageConfirm";
@@ -54,69 +55,81 @@ export default function Commands({
contextId = 0,
mid = 0,
from_uid = 0,
menuVisible,
toggleMenu,
emojiPopVisible,
toggleEmojiPopover,
toggleEditMessage,
}) {
const dispatch = useDispatch();
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [tippyVisible, setTippyVisible] = useState(false);
const currUid = useSelector((store) => store.authData.uid);
const menuRef = useRef(null);
const handleReply = (fromMenu = false) => {
const cmdsRef = useRef(null);
const handleReply = (fromMenu) => {
if (contextId) {
dispatch(addReplyingMessage({ id: contextId, mid }));
}
if (fromMenu) {
toggleMenu();
hideAll();
}
// toast.success("cooming soon");
};
useOutsideClick(menuRef, toggleMenu);
const toggleDeleteModal = () => {
hideAll();
setDeleteModalVisible((prev) => !prev);
};
const alwaysVisible = menuVisible || emojiPopVisible;
const handleTippyVisible = (visible = true) => {
setTippyVisible(visible);
};
return (
<StyledCmds className={`cmds ${alwaysVisible ? "visible" : ""}`}>
<li className="cmd" onClick={toggleEmojiPopover}>
<img src={reactIcon} alt="icon emoji" />
</li>
{emojiPopVisible && (
<div className="picker">
<EmojiPicker mid={mid} hidePicker={toggleEmojiPopover} />
</div>
)}
<StyledCmds
ref={cmdsRef}
className={`cmds ${tippyVisible ? "visible" : ""}`}
>
<Tippy
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">
<img src={reactIcon} className="toggler" alt="icon emoji" />
</li>
</Tippy>
{currUid == from_uid ? (
<li className="cmd" onClick={toggleEditMessage}>
<img src={editIcon} alt="icon edit" />
</li>
) : (
<li className="cmd" onClick={handleReply.bind(null, false)}>
<li className="cmd" onClick={handleReply}>
<img src={replyIcon} alt="icon reply" />
</li>
)}
<li className="cmd" onClick={toggleMenu}>
<img src={moreIcon} alt="icon emoji" />
</li>
{menuVisible && (
<StyledMenu className="menu" ref={menuRef}>
{/* <li className="item">Edit Message</li> */}
<li className="item underline">Pin Message</li>
<li className="item" onClick={handleReply.bind(null, true)}>
Reply
</li>
{currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}>
Delete Message
<Tippy
onShow={handleTippyVisible.bind(null, true)}
onHide={handleTippyVisible.bind(null, false)}
interactive
placement="right-start"
trigger="click"
content={
<StyledMenu className="menu">
{/* <li className="item">Edit Message</li> */}
<li className="item underline">Pin Message</li>
<li className="item" onClick={handleReply.bind(null, true)}>
Reply
</li>
)}
</StyledMenu>
)}
{currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}>
Delete Message
</li>
)}
</StyledMenu>
}
>
<li className="cmd">
<img src={moreIcon} alt="icon more" />
</li>
</Tippy>
{deleteModalVisible && (
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
)}
+8 -12
View File
@@ -1,25 +1,25 @@
// import Picker from "../EmojiPicker";
import { useRef } from "react";
import styled from "styled-components";
import { useOutsideClick } from "rooks";
import { useSelector } from "react-redux";
import { useReactMessageMutation } from "../../../app/services/message";
import { Emojis } from "../../../app/config";
import Emoji from "../Emoji";
const StyledPicker = styled.div`
background-color: #fff;
background: none;
z-index: 999;
.emojis {
padding: 4px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
background: #ffffff;
background: #fff;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
&.reacting {
opacity: 0.6;
}
.wrapper {
display: flex;
cursor: pointer;
border-radius: 8px;
padding: 4px;
@@ -36,7 +36,7 @@ const StyledPicker = styled.div`
`;
export default function EmojiPicker({ mid, hidePicker }) {
const wrapperRef = useRef(null);
// const wrapperRef = useRef(null);
const [reactMessage, { isLoading }] = useReactMessageMutation();
const { reactionData, currUid } = useSelector((store) => {
return {
@@ -44,18 +44,14 @@ export default function EmojiPicker({ mid, hidePicker }) {
currUid: store.authData.uid,
};
});
useOutsideClick(wrapperRef, hidePicker);
// useOutsideClick(wrapperRef, hidePicker);
const handleReact = (emoji) => {
console.log("react", emoji);
reactMessage({ mid, action: emoji });
hidePicker();
};
return (
<StyledPicker ref={wrapperRef}>
{/* <Picker
onSelect={handleReact}
className={`picker ${isLoading ? "reacting" : ""}`}
/> */}
<StyledPicker>
<ul className={`emojis ${isLoading ? "reacting" : ""}`}>
{Emojis.map((emoji) => {
let reacted =
+11 -18
View File
@@ -1,6 +1,8 @@
import { useState } from "react";
// import { useState } from "react";
import { useSelector } from "react-redux";
import styled from "styled-components";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import Emoji from "../Emoji";
import EmojiPicker from "./EmojiPicker";
import { useReactMessageMutation } from "../../../app/services/message";
@@ -60,18 +62,11 @@ const StyledWrapper = styled.span`
background-color: #cff9fe;
}
}
.picker {
position: absolute;
right: 0;
top: 0;
transform: translateX(105%);
}
&:hover > .add {
visibility: visible;
}
`;
export default function Reaction({ mid, reactions = null }) {
const [pickerVisible, setPickerVisible] = useState(false);
const [reactWithEmoji] = useReactMessageMutation();
const { currUid } = useSelector((store) => {
return {
@@ -81,10 +76,6 @@ export default function Reaction({ mid, reactions = null }) {
const handleReact = (emoji) => {
reactWithEmoji({ mid, action: emoji });
};
const togglePickerVisible = (wtf) => {
console.log("clicked", wtf);
setPickerVisible((prev) => !prev);
};
console.log("curr reactions", reactions);
if (!reactions || Object.entries(reactions).length == 0) return null;
return (
@@ -108,12 +99,14 @@ export default function Reaction({ mid, reactions = null }) {
</span>
) : null;
})}
<button onClick={togglePickerVisible} className="add"></button>
{pickerVisible && (
<div className="picker">
<EmojiPicker mid={mid} hidePicker={togglePickerVisible} />
</div>
)}
<Tippy
interactive
placement="right-start"
trigger="click"
content={<EmojiPicker mid={mid} hidePicker={hideAll} />}
>
<button className="add"></button>
</Tippy>
</StyledWrapper>
);
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { useSelector } from "react-redux";
import { ContentTypes } from "../../../app/config";
import Avatar from "../Avatar";
const Styled = styled.div`
cursor: pointer;
display: flex;
align-items: center;
padding: 16px;
@@ -68,7 +69,7 @@ export default function Reply({ mid }) {
const currUser = users[data.from_uid];
if (!currUser) return null;
return (
<Styled className="reply">
<Styled data-mid={mid} className="reply">
<div className="user">
<Avatar className="avatar" url={currUser.avatar} name={currUser.name} />
<span className="name">{currUser.name}</span>
+3 -15
View File
@@ -18,8 +18,7 @@ function Message({ contextId = 0, mid = "", context = "user" }) {
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [myRef, inView] = useInViewRef();
const [edit, setEdit] = useState(false);
const [emojiPopVisible, setEmojiPopVisible] = useState(false);
const [menuVisible, setMenuVisible] = useState(false);
const avatarRef = useRef(null);
const {
footprint,
@@ -36,15 +35,10 @@ function Message({ contextId = 0, mid = "", context = "user" }) {
contactsData: store.contacts.byId,
};
});
const toggleMenu = () => {
setMenuVisible((prev) => !prev);
};
const toggleEditMessage = () => {
setEdit((prev) => !prev);
};
const toggleEmojiPopover = () => {
setEmojiPopVisible((prev) => !prev);
};
const {
reply_mid,
@@ -78,9 +72,7 @@ function Message({ contextId = 0, mid = "", context = "user" }) {
<StyledWrapper
data-mid={mid}
ref={myRef}
className={`message ${menuVisible ? "menu" : ""} ${
inView ? "in_view" : ""
}`}
className={`message ${inView ? "in_view" : ""}`}
>
<Tippy
interactive
@@ -122,10 +114,6 @@ function Message({ contextId = 0, mid = "", context = "user" }) {
contextId={contextId}
mid={mid}
from_uid={fromUid}
toggleMenu={toggleMenu}
menuVisible={menuVisible}
emojiPopVisible={emojiPopVisible}
toggleEmojiPopover={toggleEmojiPopover}
toggleEditMessage={toggleEditMessage}
/>
)}
-3
View File
@@ -19,9 +19,6 @@ const StyledMsg = styled.div`
visibility: visible;
}
}
&.menu {
z-index: 9;
}
.avatar {
cursor: pointer;
img {
+1 -1
View File
@@ -3,7 +3,7 @@ import styled from "styled-components";
const StyledWrapper = styled.div`
width: 100%;
position: absolute;
bottom: 50px;
bottom: 64px;
left: 0;
padding: 8px;
display: flex;
+37 -38
View File
@@ -1,6 +1,9 @@
import { useState } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import searchIcon from "../../assets/icons/search.svg?url";
import addIcon from "../../assets/icons/add.svg?url";
import mailIcon from "../../assets/icons/mail.svg?url";
@@ -42,9 +45,6 @@ const StyledWrapper = styled.div`
border-radius: 8px;
color: #616161;
background: #fff;
position: absolute;
top: 50px;
right: 8px;
display: flex;
flex-direction: column;
padding: 4px;
@@ -68,23 +68,21 @@ export default function Search() {
const currentUser = useSelector(
(store) => store.contacts.byId[store.authData.uid]
);
const [popupVisible, setPopupVisible] = useState(false);
const [channelModalVisible, setChannelModalVisible] = useState(false);
const [contactsModalVisible, setContactsModalVisible] = useState(false);
const [isPrivate, setIsPrivate] = useState(false);
const toggleContactsModalVisible = () => {
if (!contactsModalVisible) {
togglePopupVisible();
}
setContactsModalVisible((prev) => !prev);
};
const togglePopupVisible = () => {
setPopupVisible((prev) => !prev);
setContactsModalVisible((prevVisible) => {
if (!prevVisible) {
hideAll();
}
return !prevVisible;
});
};
const handleOpenChannelModal = (isPrivate) => {
setIsPrivate(isPrivate);
setChannelModalVisible(true);
togglePopupVisible();
hideAll();
};
const handleCloseModal = () => {
setChannelModalVisible(false);
@@ -102,36 +100,37 @@ export default function Search() {
<img src={searchIcon} />
<input placeholder="Search..." className="input" />
</div>
<img
src={addIcon}
alt="add icon"
className="add"
onClick={togglePopupVisible}
/>
{popupVisible && (
<ul className="popup">
{currentUser?.is_admin && (
<Tippy
interactive
placement="bottom-end"
trigger="click"
content={
<ul className="popup">
{currentUser?.is_admin && (
<li
className="item"
onClick={handleOpenChannelModal.bind(null, false)}
>
<ChannelIcon />
New Channel
</li>
)}
<li
className="item"
onClick={handleOpenChannelModal.bind(null, false)}
onClick={handleOpenChannelModal.bind(null, true)}
>
<ChannelIcon />
New Channel
<ChannelIcon personal={true} />
New Private Channel
</li>
)}
<li
className="item"
onClick={handleOpenChannelModal.bind(null, true)}
>
<ChannelIcon personal={true} />
New Private Channel
</li>
<li className="item" onClick={toggleContactsModalVisible}>
<img src={mailIcon} alt="icon mail" />
New Message
</li>
</ul>
)}
<li className="item" onClick={toggleContactsModalVisible}>
<img src={mailIcon} alt="icon mail" />
New Message
</li>
</ul>
}
>
<img src={addIcon} alt="add icon" className="add" />
</Tippy>
</StyledWrapper>
);
}
@@ -53,7 +53,7 @@ export default function UploadModal({
uploadFn({
id: sendTo,
content: files[0],
properties: btoa(JSON.stringify(properties[0])),
properties: { ...properties[0], local_id: new Date().getTime() },
type: "image",
from_uid,
});
+7 -1
View File
@@ -96,7 +96,12 @@ export default function Send({
});
dispatch(removeReplyingMessage(id));
} else {
sendMessage({ id, content: msg, from_uid });
sendMessage({
id,
content: msg,
from_uid,
properties: { local_id: new Date().getTime() },
});
}
setMsg("");
};
@@ -106,6 +111,7 @@ export default function Send({
id,
content: markdown,
from_uid,
properties: { local_id: new Date().getTime() },
type: "markdown",
});
setMarkdown("");
+86 -86
View File
@@ -1,3 +1,4 @@
import { useSelector } from "react-redux";
import MyAccount from "./MyAccount";
import Overview from "./Overview";
import ConfigFirebase from "./config/Firebase";
@@ -6,93 +7,92 @@ import Notifications from "./Notifications";
import ManageMembers from "../ManageMembers";
import FAQ from "../FAQ";
import ConfigAgora from "./config/Agora";
const navs = [
{
title: "General",
items: [
{
name: "overview",
title: "Overview",
component: <Overview />,
},
{
name: "members",
title: "Members",
component: <ManageMembers />,
admin: true,
},
{
name: "notification",
title: "Notification",
component: <Notifications />,
},
],
},
{
title: "User",
items: [
{
name: "my_account",
title: "My Account",
component: <MyAccount />,
},
],
},
{
title: "Configuration",
items: [
{
name: "firebase",
title: "Firebase",
component: <ConfigFirebase />,
},
{
name: "agora",
title: "Agora",
component: <ConfigAgora />,
},
{
name: "smtp",
title: "SMTP",
component: <ConfigSMTP />,
},
],
admin: true,
},
{
title: "About",
items: [
{
name: "faq",
title: "FAQ",
component: <FAQ />,
},
{
name: "terms",
title: "Terms & Privacy",
component: "Terms & Privacy",
},
{
name: "feedback",
title: "Feedback",
component: "feedback",
},
],
},
];
const useNavs = () => {
const navs = [
{
title: "General",
items: [
{
name: "overview",
title: "Overview",
component: <Overview />,
},
{
name: "roles",
title: "Roles",
},
{
name: "notification",
title: "Notification",
component: <Notifications />,
},
],
},
{
title: "User",
items: [
{
name: "my_account",
title: "My Account",
component: <MyAccount />,
},
{
name: "auth_apps",
title: "Authorized Apps",
},
],
},
{
title: "User Management",
items: [
{
name: "members",
title: "Members",
component: <ManageMembers />,
},
],
},
{
title: "Configuration",
items: [
{
name: "firebase",
title: "Firebase",
component: <ConfigFirebase />,
},
{
name: "agora",
title: "Agora",
component: <ConfigAgora />,
},
{
name: "smtp",
title: "SMTP",
component: <ConfigSMTP />,
},
],
},
{
title: "About",
items: [
{
name: "faq",
title: "FAQ",
component: <FAQ />,
},
{
name: "terms",
title: "Terms & Privacy",
component: "Terms & Privacy",
},
{
name: "feedback",
title: "Feedback",
component: "feedback",
},
],
},
];
return navs;
const loginUser = useSelector((store) => {
return store.contacts.byId[store.authData.uid];
});
const Navs = navs.filter((nav) => {
if (loginUser.is_admin) {
return true;
} else {
return !nav.admin;
}
});
return Navs;
};
export default useNavs;
@@ -85,7 +85,7 @@ export default function StyledSettingContainer({
};
return (
<Modal>
<StyledWrapper className="animate__animated animate__fadeInUp animate__faster">
<StyledWrapper>
<div className="left">
<h2 onClick={closeModal} className="title">
{title}
+1
View File
@@ -11,6 +11,7 @@ const StyledInput = styled.input`
color: #333;
padding: 8px;
&.none {
outline: none;
border: none;
background: none;
box-shadow: none;
-3
View File
@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { useDispatch, batch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { resetAuthData } from "../../app/slices/auth.data";
import { resetFootprint } from "../../app/slices/footprint";
import { resetChannels } from "../../app/slices/channels";
@@ -12,7 +11,6 @@ import { resetMessage } from "../../app/slices/message";
import { useLazyLogoutQuery } from "../../app/services/auth";
export default function useLogout() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery();
const clearLocalData = () => {
batch(() => {
@@ -29,7 +27,6 @@ export default function useLogout() {
useEffect(() => {
if (isSuccess) {
dispatch(resetAuthData());
navigate("/login");
}
}, [isSuccess]);
+18 -6
View File
@@ -64,9 +64,15 @@ const handler = (data, dispatch, currState) => {
);
// 未推送完 or 不是自己发的消息
console.log("curr state", ready, loginUid, common.from_uid);
if (!ready || loginUid != common.from_uid) {
dispatch(appendMessage({ id, mid }));
}
// if (!ready || loginUid != common.from_uid) {
dispatch(
appendMessage({
id,
mid,
local_id: properties ? properties.local_id : null,
})
);
// }
});
}
break;
@@ -83,9 +89,15 @@ const handler = (data, dispatch, currState) => {
})
);
// 未推送完 or 不是自己发的消息
if (!ready || loginUid != common.from_uid) {
dispatch(appendMessage({ id, mid }));
}
// if (!ready || loginUid != common.from_uid) {
dispatch(
appendMessage({
id,
mid,
local_id: properties ? properties.local_id : null,
})
);
// }
});
}
break;
+34 -12
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useCallback } from "react";
import {
fetchEventSource,
EventStreamContentType,
@@ -6,6 +6,7 @@ import {
import toast from "react-hot-toast";
import BASE_URL from "../../../app/config";
import { setReady } from "../../../app/slices/ui";
import { useRenewMutation } from "../../../app/services/auth";
import {
fullfillChannels,
addChannel,
@@ -24,7 +25,7 @@ import {
import { resetAuthData } from "../../../app/slices/auth.data";
import chatMessageHandler from "./chat.handler";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
class RetriableError extends Error {}
class FatalError extends Error {}
const getQueryString = (params = {}) => {
@@ -42,17 +43,20 @@ const StreamStatus = {
streaming: "streaming",
};
export default function useStreaming() {
// const store = useSelector((store) => store);
const store = useSelector((store) => store);
const [renewToken] = useRenewMutation();
const dispatch = useDispatch();
const [status, setStatus] = useState(StreamStatus.waiting);
const startStreaming = (store) => {
const startStreaming = useCallback(() => {
if (status !== StreamStatus.waiting) return;
const controller = new AbortController();
setStatus(StreamStatus.initializing);
const {
authData: { token },
authData: { token, uid: loginUid },
footprint: { afterMid, usersVersion },
} = store;
console.log("set uid use");
fetchEventSource(
`${BASE_URL}/user/events?${getQueryString({
"api-key": token,
@@ -77,8 +81,9 @@ export default function useStreaming() {
) {
// 重新登录
if (response.status == 401) {
dispatch(resetAuthData());
return;
renewToken();
// dispatch(resetAuthData());
// return;
}
// client-side errors are usually non-retriable:
throw new FatalError();
@@ -95,7 +100,6 @@ export default function useStreaming() {
}
const {
ui: { ready },
authData: { uid: loginUid },
footprint: { readUsers, readChannels },
channels: { byId: channelData },
} = store;
@@ -103,7 +107,7 @@ export default function useStreaming() {
const { type } = data;
switch (type) {
case "heartbeat":
console.log("heartbeat");
console.log("heartbeat", store, loginUid);
break;
case "ready":
console.log("streaming ready");
@@ -168,13 +172,31 @@ export default function useStreaming() {
break;
case "user_joined_group":
console.log("new user joined group", data.gid);
// 去重
dispatch(
updateChannel({
id: data.gid,
members: [...channelData[data.gid].members, ...data.uid],
members: [
...channelData[data.gid].members,
...data.uid,
].filter((v, i, a) => a.indexOf(v) === i),
})
);
break;
case "user_leaved_group":
{
const { gid, uid: uids } = data;
const leftMembers = channelData[gid].members.filter(
(id) => uids.findIndex((uid) => id == uid) == -1
);
dispatch(
updateChannel({
id: data.gid,
members: [...leftMembers],
})
);
}
break;
case "kick_from_group":
console.log("kicked from group", data.gid);
dispatch(removeChannel(data.gid));
@@ -203,7 +225,7 @@ export default function useStreaming() {
if (err instanceof FatalError) {
// 重连
setTimeout(() => {
startStreaming(store);
startStreaming();
}, 500);
throw err; // rethrow to stop the operation
} else {
@@ -215,7 +237,7 @@ export default function useStreaming() {
);
// for controlling
return controller;
};
}, [store, status]);
return {
initializing: status == StreamStatus.initializing,
streaming: status == StreamStatus.streaming,
+12 -3
View File
@@ -16,11 +16,20 @@ export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
});
return tmp;
};
export function getDefaultSize(size = null, width = 240) {
export function getDefaultSize(size = null, min = 240) {
if (!size) return { width: 0, height: 0 };
const { width: oWidth, height: oHeight } = size;
const dWidth = oWidth >= width ? width : oWidth;
const dHeight = (oHeight / oWidth) * dWidth;
const isVertical = oWidth > oHeight ? false : true;
let dWidth = 0;
let dHeight = 0;
if (isVertical) {
dHeight = oHeight >= min ? min : oHeight;
dWidth = (oWidth / oHeight) * dHeight;
} else {
dWidth = oWidth >= min ? min : oWidth;
// dHeight = oHeight >= min ? min : oHeight;
dHeight = (oHeight / oWidth) * dWidth;
}
return { width: dWidth, height: dHeight };
}
export function formatBytes(bytes, decimals = 2) {
+7 -6
View File
@@ -1,8 +1,9 @@
import { NavLink, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import { useDispatch, useSelector } from "react-redux";
// import { useDebounce} from "rooks";
import StyledLink from "./styled";
import { toggleChannelSetting } from "../../../app/slices/ui";
import ChannelIcon from "../../../common/component/ChannelIcon";
import { getUnreadCount } from "../utils";
@@ -47,7 +48,7 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
const { is_public, name } = channel;
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
return (
<NavLink
<StyledLink
data-cid={id}
onContextMenu={(evt) => {
contextMenuEventHandler(evt, id);
@@ -57,10 +58,10 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
className={`link ${isActive ? "drop_over" : ""}`}
to={`/chat/channel/${id}`}
>
<span className="txt">
<div className="name" title={name}>
<ChannelIcon personal={!is_public} />
{name}
</span>
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
</div>
<div className="icons">
<i className="setting" onClick={handleChannelSetting}></i>
{unreads > 0 && (
@@ -69,7 +70,7 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
</i>
)}
</div>
</NavLink>
</StyledLink>
);
};
+69
View File
@@ -0,0 +1,69 @@
import styled from "styled-components";
import { NavLink } from "react-router-dom";
import settingIcon from "../../../assets/icons/setting.svg?url";
const Styled = styled(NavLink)`
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 4px;
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
> .name {
display: flex;
align-items: center;
gap: 8px;
.txt {
color: #1c1c1e;
font-weight: 600;
font-size: 14px;
line-height: 20px;
width: 160px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.read {
color: #616161;
}
}
}
> .icons {
display: flex;
align-items: center;
gap: 4px;
> .setting {
visibility: hidden;
display: flex;
width: 16px;
height: 16px;
background-image: url(${settingIcon});
background-size: 16px;
}
> .badge {
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
min-width: 20px;
border-radius: 50%;
background: #bfbfbf;
font-weight: 900;
font-size: 10px;
line-height: 10px;
&.dot {
min-width: unset;
width: 6px;
height: 6px;
padding: 0;
}
}
}
&:hover > .icons > .setting {
visibility: visible;
}
`;
export default Styled;
-57
View File
@@ -1,5 +1,4 @@
import styled from "styled-components";
import settingIcon from "../../assets/icons/setting.svg?url";
const StyledWrapper = styled.div`
display: flex;
height: 100%;
@@ -40,62 +39,6 @@ const StyledWrapper = styled.div`
a {
text-decoration: none;
}
.link {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 4px;
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
> .txt {
display: flex;
align-items: center;
gap: 8px;
color: #1c1c1e;
font-weight: 600;
font-size: 14px;
line-height: 20px;
}
> .icons {
display: flex;
align-items: center;
gap: 4px;
> .setting {
visibility: hidden;
display: flex;
width: 16px;
height: 16px;
background-image: url(${settingIcon});
background-size: 16px;
}
> .badge {
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
min-width: 20px;
border-radius: 50%;
background: #bfbfbf;
font-weight: 900;
font-size: 10px;
line-height: 10px;
&.dot {
min-width: unset;
width: 6px;
height: 6px;
padding: 0;
}
}
}
&:hover > .icons > .setting {
visibility: visible;
}
}
.session {
display: flex;
align-items: center;
+1 -1
View File
@@ -24,7 +24,7 @@ export function getUnreadCount({
console.log({ mids, loginUid, readIndex });
// 先过滤掉from自己的
const others = mids.filter((mid) => {
const { from_uid } = messageData[mid];
const { from_uid = 0 } = messageData[mid] || {};
return from_uid != loginUid;
});
if (others.length == 0) return 0;
+1 -1
View File
@@ -26,9 +26,9 @@ const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
white-space: nowrap;
.title {
font-weight: bold;
white-space: nowrap;
font-size: 14px;
line-height: 100%;
color: #374151;
+2 -2
View File
@@ -5,7 +5,7 @@ import { useSelector, useDispatch } from "react-redux";
import { toggleMenuExpand } from "../../app/slices/ui";
import StyledWrapper from "./styled";
import ServerDropList from "./ServerDropList";
import Tools from "./Tools";
// import Tools from "./Tools";
import Loading from "./Loading";
import Menu from "./Menu";
import usePreload from "./usePreload";
@@ -60,7 +60,7 @@ export default function HomePage() {
</NavLink>
</nav>
<div className="divider"></div>
<Tools expand={menuExpand} />
{/* <Tools expand={menuExpand} /> */}
<Menu toggle={toggleExpand} expand={menuExpand} />
{/* <CurrentUser expand={menuExpand} /> */}
</div>
+3 -12
View File
@@ -1,11 +1,7 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
// import { useNavigate } from "react-router-dom";
import initCache, { useRehydrate } from "../../app/cache";
import { useLazyGetContactsQuery } from "../../app/services/contact";
// import { useLazyInitStreamingQuery } from "../../app/services/streaming";
// import { useGetChannelsQuery } from "../../app/services/channel";
import { useLazyGetServerQuery } from "../../app/services/server";
import useStreaming from "../../common/hook/useStreaming";
// pollingInterval: 0,
@@ -15,9 +11,7 @@ import useStreaming from "../../common/hook/useStreaming";
let request = null;
export default function usePreload() {
const { rehydrate, rehydrated } = useRehydrate();
// const [initStreaming, { isLoading: streaming }] = useLazyInitStreamingQuery();
// const navigate = useNavigate();
const store = useSelector((store) => store);
const loginUid = useSelector((store) => store.authData.uid);
const { startStreaming, streaming, initializing } = useStreaming();
const [
getContacts,
@@ -48,21 +42,18 @@ export default function usePreload() {
}, []);
useEffect(() => {
// rehydrate();
if (rehydrated) {
getContacts();
getServer();
}
}, [rehydrated]);
const canStreaming =
contactsSuccess && rehydrated && !initializing && !streaming;
const canStreaming = loginUid && rehydrated && !initializing && !streaming;
useEffect(() => {
if (canStreaming) {
request = startStreaming(store);
request = startStreaming();
}
}, [canStreaming]);
// console.log("loading", contactsLoading, serverLoading, !checked);
return {
loading: contactsLoading || serverLoading || !rehydrated,
error: contactsError && serverError,