chore: stash updates

This commit is contained in:
zerosoul
2022-05-31 10:59:10 +08:00
parent 2af03fe0d4
commit dd61fe5882
4 changed files with 499 additions and 95 deletions
+41 -65
View File
@@ -1,7 +1,8 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
const getFeedWithPagination = (config) => { const getFeedWithPagination = (config) => {
const { pageNumber = 1, pageSize = 20, mids = [] } = config || {}; const { pageNumber = 1, pageSize = 10, mids = [], isLast = false } =
config || {};
const shadowMids = mids.slice(0); const shadowMids = mids.slice(0);
if (shadowMids.length == 0) if (shadowMids.length == 0)
@@ -12,30 +13,27 @@ const getFeedWithPagination = (config) => {
ids: [], ids: [],
}; };
shadowMids.sort((a, b) => { shadowMids.sort((a, b) => {
return Number(b) - Number(a); return Number(a) - Number(b);
}); });
console.log("message pagination", shadowMids); console.log("message pagination", shadowMids);
const pageCount = Math.ceil(shadowMids.length / pageSize); const pageCount = Math.ceil(shadowMids.length / pageSize);
const computedPageNumber = isLast ? pageCount : pageNumber;
const ids = shadowMids.slice( const ids = shadowMids.slice(
(pageNumber - 1) * pageSize, (computedPageNumber - 1) * pageSize,
pageNumber * pageSize computedPageNumber * pageSize
); );
const info = { return {
isFirst: pageNumber == 1,
isLast: pageNumber == pageCount,
pageCount, pageCount,
pageSize, pageSize,
pageNumber, pageNumber: computedPageNumber,
ids, ids,
}; };
console.log("get page Info", info);
return info;
}; };
export default function useMessageFeed({ context = "channel", id = null }) { export default function useMessageFeed({ context = "channel", id = null }) {
const listRef = useRef([]); const listRef = useRef([]);
const pageRef = useRef(null); const pageRef = useRef(null);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
// const [appends, setAppends] = useState([]); const [appends, setAppends] = useState([]);
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const { mids, messageData } = useSelector((store) => { const { mids, messageData } = useSelector((store) => {
return { return {
@@ -51,72 +49,46 @@ export default function useMessageFeed({ context = "channel", id = null }) {
pageRef.current = []; pageRef.current = [];
setItems([]); setItems([]);
setHasMore(true); setHasMore(true);
// setAppends([]); setAppends([]);
}, [context, id]); }, [context, id]);
// useEffect(() => {
// if (appends.length) {
// const feedsWrapperEle = document.querySelector(
// `#RUSTCHAT_FEED_${context}_${id}`
// );
// if (feedsWrapperEle) {
// feedsWrapperEle.scrollTop = feedsWrapperEle.scrollHeight;
// }
// }
// }, [appends, context, id]);
useEffect(() => { useEffect(() => {
const container = document.querySelector("#ScrollFeedWrapper"); if (appends.length) {
const handler = (e) => { const feedsWrapperEle = document.querySelector(
e.preventDefault(); `#RUSTCHAT_FEED_${context}_${id}`
var n = 0; );
if ("deltaY" in e) n = 1 === e.deltaMode ? 20 * -e.deltaY : -e.deltaY; if (feedsWrapperEle) {
else if ("wheelDeltaY" in e) n = (e.wheelDeltaY / 120) * 20; feedsWrapperEle.scrollTop = feedsWrapperEle.scrollHeight;
else if ("wheelDelta" in e) n = (e.wheelDelta / 120) * 20;
else {
// if (!("detail"in e))
// return void a.v(e, "invalid wheel event: ");
n = (-e.detail / 3) * 20;
} }
container.scrollTop += n;
};
if (container) {
container.addEventListener("wheel", handler);
} }
return () => { }, [appends, context, id]);
if (container) {
container.removeEventListener("wheel", handler);
}
};
}, []);
useEffect(() => { useEffect(() => {
if (listRef.current.length == 0) { if (listRef.current.length == 0) {
// 初次 // 初次
const pageInfo = getFeedWithPagination({ mids }); const pageInfo = getFeedWithPagination({ mids, isLast: true });
console.log("pull down 2", pageInfo); console.log("pull up 2", pageInfo);
pageRef.current = pageInfo; pageRef.current = pageInfo;
listRef.current = pageInfo.ids; listRef.current = pageInfo.ids;
setItems(listRef.current); setItems(listRef.current);
console.log("message pageInfo", mids, pageInfo); console.log("message pageInfo", mids, pageInfo);
} else { } else {
// 追加 // 追加
const lastMid = listRef.current[0]; const lastMid = listRef.current.slice(-1);
const sorteds = mids.slice(0).sort((a, b) => { const sorteds = mids.slice(0).sort((a, b) => {
return Number(b) - Number(a); return Number(a) - Number(b);
}); });
const prepends = sorteds.filter((s) => s > lastMid); const appends = sorteds.filter((s) => s > lastMid);
if (prepends.length) { if (appends.length) {
setItems((prevs) => [...prepends, ...prevs]); setAppends(appends);
} }
console.log("prepends", prepends, items); console.log("appends", appends);
} }
}, [mids]); }, [mids]);
const pullDown = () => { const pullUp = () => {
// 向下加载
const currPageInfo = pageRef.current; const currPageInfo = pageRef.current;
console.log("pull down", currPageInfo); console.log("pull up", currPageInfo);
// 最后一页 // 一页
if (currPageInfo && currPageInfo.isLast) { if (currPageInfo && currPageInfo.pageNumber == 1) {
setHasMore(true); setHasMore(false);
return; return;
} }
let pageInfo = null; let pageInfo = null;
@@ -124,28 +96,32 @@ export default function useMessageFeed({ context = "channel", id = null }) {
// 初始化 // 初始化
pageInfo = getFeedWithPagination({ pageInfo = getFeedWithPagination({
mids, mids,
isLast: true,
}); });
} else { } else {
const nextPageNumber = currPageInfo.pageNumber + 1; const prevPageNumber = currPageInfo.pageNumber - 1;
pageInfo = getFeedWithPagination({ pageInfo = getFeedWithPagination({
mids, mids,
pageNumber: nextPageNumber, pageNumber: prevPageNumber,
}); });
} }
pageRef.current = pageInfo; pageRef.current = pageInfo;
listRef.current = [...listRef.current, ...pageInfo.ids]; listRef.current = [...pageInfo.ids, ...listRef.current];
setTimeout(() => { setTimeout(() => {
setItems(listRef.current); setItems(listRef.current);
console.log("pull up", currPageInfo, listRef.current); console.log("pull up", currPageInfo, listRef.current);
setHasMore(!pageInfo.isLast); setHasMore(pageInfo.pageNumber !== 1);
}, 1000); }, 800);
};
const pullDown = () => {
// 向下加载
}; };
return { return {
mids, mids,
// appends, appends,
hasMore, hasMore,
// pullUp, pullUp,
pullDown, pullDown,
list: items, list: items,
}; };
+29 -30
View File
@@ -1,9 +1,8 @@
import { useRef, useEffect } from "react"; // import { useRef, useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Waveform } from "@uiball/loaders"; import { Waveform } from "@uiball/loaders";
const Styled = styled.div` const Styled = styled.div`
margin-top: 80px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -11,35 +10,35 @@ const Styled = styled.div`
padding: 30px 0; padding: 30px 0;
/* background-color: #eee; */ /* background-color: #eee; */
`; `;
export default function LoadMore({ pullDown = null }) { export default function LoadMore() {
const ref = useRef(undefined); // const ref = useRef(undefined);
useEffect(() => { // useEffect(() => {
const observer = new IntersectionObserver( // const observer = new IntersectionObserver(
(entries) => { // (entries) => {
entries.forEach((entry) => { // entries.forEach((entry) => {
const intersecting = entry.isIntersecting; // const intersecting = entry.isIntersecting;
// const currEle = entry.target; // // const currEle = entry.target;
if (intersecting && pullDown) { // if (intersecting && loadMore) {
// load more // // load more
console.log("inview"); // console.log("inview");
pullDown(); // loadMore();
} // }
}); // });
}, // },
{ threshold: 0 } // { threshold: 0 }
); // );
const currEle = ref?.current; // const currEle = ref?.current;
if (currEle) { // if (currEle) {
observer.observe(ref.current); // observer.observe(ref.current);
} // }
return () => { // return () => {
if (currEle) { // if (currEle) {
observer.unobserve(currEle); // observer.unobserve(currEle);
} // }
}; // };
}, [ref, pullDown]); // }, [ref]);
return ( return (
<Styled ref={ref}> <Styled>
<Waveform <Waveform
className="loading" className="loading"
size={24} size={24}
@@ -0,0 +1,287 @@
import { useState, useEffect } from "react";
import { useDebounce } from "rooks";
import { NavLink, useLocation } from "react-router-dom";
import Tippy from "@tippyjs/react";
import { useDispatch, useSelector } from "react-redux";
// import InfiniteScroll from "react-infinite-scroll-component";
import PinList from "./PinList";
import FavList from "../FavList";
import { useReadMessageMutation } from "../../../app/services/message";
import { updateRemeberedNavs } from "../../../app/slices/ui";
// import useChatScroll from "../../../common/hook/useChatScroll";
import useMessageFeed from "../../../common/hook/useMessageFeed";
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 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 {
// StyledNotification,
StyledContacts,
StyledChannelChat,
StyledHeader,
} from "./styled";
import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "./LoadMore";
export default function ChannelChat({ cid = "", dropFiles = [] }) {
const { list: msgIds, appends, hasMore, pullDown } = useMessageFeed({
context: "channel",
id: cid,
});
const [toolVisible, setToolVisible] = useState("");
const { pathname } = useLocation();
const dispatch = useDispatch();
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [membersVisible, setMembersVisible] = useState(true);
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
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));
// };
useEffect(() => {
dispatch(updateRemeberedNavs());
return () => {
dispatch(updateRemeberedNavs({ path: pathname }));
};
}, [pathname]);
const toggleMembersVisible = () => {
setMembersVisible((prev) => !prev);
};
const toggleAddVisible = () => {
setAddMemberModalVisible((prev) => !prev);
};
if (!data) return null;
const { name, description, is_public, members = [], owner } = data;
const memberIds = is_public
? userIds
: members.slice(0).sort((n) => (n == owner ? -1 : 0));
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 && (
<InviteModal cid={cid} closeModal={toggleAddVisible} />
)}
<Layout
to={cid}
context="channel"
dropFiles={dropFiles}
// ref={containerRef}
aside={
<>
<ul className="tools">
<li className="tool">
<Tooltip tip="Voice/Video Chat" placement="left">
<IconHeadphone />
</Tooltip>
</li>
<Tooltip
tip="Pin"
placement="left"
disabled={toolVisible == "pin"}
>
<Tippy
onShow={() => {
setToolVisible("pin");
}}
onHide={() => {
setToolVisible("");
}}
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("");
}}
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
enableContextMenu={true}
cid={cid}
owner={owner == uid}
key={uid}
uid={uid}
dm
popover
/>
);
})}
</StyledContacts>
</>
) : null
}
>
<StyledChannelChat id="ScrollFeedWrapper">
{/* <div className="feed"> */}
{[...msgIds].map((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev =
idx == msgIds.length - 1 ? 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",
});
})}
{hasMore ? (
<LoadMore pullDown={pullDown} />
) : (
<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> */}
</StyledChannelChat>
{/* {unreads != 0 && (
<StyledNotification>
<div className="content">
{unreads} new messages
{msgs.lastAccess
? `since ${dayjs(msgs.lastAccess).format("YYYY-MM-DD h:mm:ss A")}`
: ""}
</div>
<button onClick={handleClearUnreads} className="clear">
Mark As Read
</button>
</StyledNotification>
)} */}
</Layout>
</>
);
}
+142
View File
@@ -0,0 +1,142 @@
import styled from "styled-components";
export const StyledHeader = styled.header`
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.txt {
display: flex;
align-items: center;
gap: 5px;
.title {
font-size: 16px;
line-height: 24px;
color: #1c1c1e;
}
.desc {
margin-left: 8px;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #616161;
}
}
`;
export const StyledNotification = styled.div`
padding: 3px 8px;
font-style: normal;
font-weight: 600;
font-size: 12px;
line-height: 18px;
color: #fff;
position: absolute;
top: 0;
left: 10px;
width: 900px;
height: 24px;
background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%);
border-radius: 0px 0px 8px 8px;
display: flex;
align-items: center;
justify-content: space-between;
.clear {
cursor: pointer;
color: inherit;
border: none;
background: none;
outline: none;
}
`;
export const StyledContacts = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
/* todo */
width: 226px;
height: calc(100vh - 56px - 22px);
overflow-y: scroll;
padding: 8px;
box-shadow: inset 1px 0px 0px rgba(0, 0, 0, 0.1);
> .add {
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
padding: 10px;
border-radius: 8px;
user-select: none;
&:hover {
background: rgba(116, 127, 141, 0.1);
}
.icon {
width: 24px;
height: 24px;
}
.txt {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
}
}
`;
export const StyledChannelChat = styled.article`
padding: 18px 16px;
width: 100%;
height: 100%;
height: -webkit-fill-available;
overflow-x: hidden;
overflow-y: auto;
transform: rotate(180deg) translateZ(0);
direction: rtl;
/* display: flex;
flex-direction: column-reverse; */
> div,
> hr {
direction: ltr;
transform: rotate(180deg);
}
> .info {
padding-top: 62px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.title {
font-weight: bold;
font-size: 36px;
line-height: 44px;
}
.desc {
color: #78787c;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.edit {
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #3c8ce7;
}
}
padding: 0;
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
}
/* > .feed {
} */
`;