refactor: message feed

This commit is contained in:
Tristan Yang
2023-03-07 11:04:38 +08:00
parent 0aa747ee7c
commit 9b379e9561
7 changed files with 149 additions and 145 deletions
+3 -4
View File
@@ -1,8 +1,8 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { ViewportList } from 'react-viewport-list'; import { ViewportList } from 'react-viewport-list';
import { useTranslation } from 'react-i18next';
import User from "../../../common/component/User"; import User from "../../../common/component/User";
import IconAdd from "../../../assets/icons/add.svg"; import IconAdd from "../../../assets/icons/add.svg";
import { useTranslation } from 'react-i18next';
import InviteModal from "../../../common/component/InviteModal"; import InviteModal from "../../../common/component/InviteModal";
@@ -16,9 +16,7 @@ type Props = {
const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => { const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const ref = useRef<HTMLDivElement | null>( const ref = useRef<HTMLDivElement | null>(null);
null,
);
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false); const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
const toggleAddVisible = () => { const toggleAddVisible = () => {
setAddMemberModalVisible((prev) => !prev); setAddMemberModalVisible((prev) => !prev);
@@ -34,6 +32,7 @@ const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => {
</div> </div>
)} )}
<ViewportList <ViewportList
initialPrerender={15}
viewportRef={ref} viewportRef={ref}
items={uids} items={uids}
> >
+2 -62
View File
@@ -1,24 +1,17 @@
import { useState, useEffect, memo } from "react"; import { useState, useEffect, memo } from "react";
import { useDebounce } from "rooks"; import { useLocation } from "react-router-dom";
import { NavLink, useLocation } from "react-router-dom";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import PinList from "./PinList"; import PinList from "./PinList";
import FavList from "../FavList"; import FavList from "../FavList";
import { useReadMessageMutation } from "../../../app/services/message";
import { updateRememberedNavs } from "../../../app/slices/ui"; import { updateRememberedNavs } from "../../../app/slices/ui";
import useMessageFeed from "../../../common/hook/useMessageFeed";
import ChannelIcon from "../../../common/component/ChannelIcon"; import ChannelIcon from "../../../common/component/ChannelIcon";
import Tooltip from "../../../common/component/Tooltip"; import Tooltip from "../../../common/component/Tooltip";
import Layout from "../Layout"; import Layout from "../Layout";
import { renderMessageFragment } from "../utils";
import EditIcon from "../../../assets/icons/edit.svg";
import IconFav from "../../../assets/icons/bookmark.svg"; import IconFav from "../../../assets/icons/bookmark.svg";
import IconPeople from "../../../assets/icons/people.svg"; import IconPeople from "../../../assets/icons/people.svg";
import IconPin from "../../../assets/icons/pin.svg"; import IconPin from "../../../assets/icons/pin.svg";
import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import GoBackNav from "../../../common/component/GoBackNav"; import GoBackNav from "../../../common/component/GoBackNav";
@@ -29,37 +22,19 @@ type Props = {
}; };
function ChannelChat({ cid = 0, dropFiles = [] }: Props) { function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const {
pulling,
list: msgIds,
appends,
hasMore,
pullUp
} = useMessageFeed({
context: "channel",
id: cid
});
const { pathname } = useLocation(); const { pathname } = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const [membersVisible, setMembersVisible] = useState(true); const [membersVisible, setMembersVisible] = useState(true);
const { const {
selects,
userIds, userIds,
data, data,
messageData,
loginUser, loginUser,
footprint
} = useAppSelector((store) => { } = useAppSelector((store) => {
return { return {
selects: store.ui.selectMessages[`channel_${cid}`],
footprint: store.footprint,
loginUser: store.authData.user, loginUser: store.authData.user,
userIds: store.users.ids, userIds: store.users.ids,
data: store.channels.byId[cid], data: store.channels.byId[cid],
messageData: store.message || {}
}; };
}); });
useEffect(() => { useEffect(() => {
@@ -77,9 +52,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
const { name, description, is_public, members = [], owner } = data; const { name, description, is_public, members = [], owner } = data;
const memberIds = is_public ? userIds : members.slice(0).sort((n) => (n == owner ? -1 : 0)); const memberIds = is_public ? userIds : members.slice(0).sort((n) => (n == owner ? -1 : 0));
const addVisible = loginUser?.is_admin || owner == loginUser?.uid; const addVisible = loginUser?.is_admin || owner == loginUser?.uid;
const readIndex = footprint.readChannels[cid];
const pinCount = data?.pinned_messages?.length || 0; const pinCount = data?.pinned_messages?.length || 0;
const feeds = [...msgIds, ...appends];
const toolClass = `relative cursor-pointer`; const toolClass = `relative cursor-pointer`;
return ( return (
<> <>
@@ -142,40 +115,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
users={ users={
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} /> <Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} />
} }
> />
<>
{hasMore ? (
<LoadMore pullUp={pullUp} pulling={pulling} />
) : (
<div className="pt-14 px-1 md:px-0 flex flex-col items-start gap-2">
<h2 className="font-bold text-4xl dark:text-white">{t("welcome_channel", { name })}</h2>
<p className="text-gray-600 dark:text-gray-300">{t("welcome_desc", { name })} </p>
{loginUser?.is_admin && (
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="flex items-center gap-1 bg-clip-text text-fill-transparent bg-gradient-to-r from-blue-500 to-primary-400 ">
<EditIcon className="w-4 h-4 fill-blue-500" />
{t("edit_channel")}
</NavLink>
)}
</div>
)}
{feeds.map((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev = isFirst ? null : messageData[feeds[idx - 1]];
const read = curr?.from_uid == loginUser?.uid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
prev,
curr,
contextId: cid,
context: "channel"
});
})}
</>
</Layout>
</> </>
); );
} }
+2 -43
View File
@@ -1,15 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { useDebounce } from "rooks";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import FavList from "../FavList"; import FavList from "../FavList";
import Tooltip from "../../../common/component/Tooltip"; import Tooltip from "../../../common/component/Tooltip";
import FavIcon from "../../../assets/icons/bookmark.svg"; import FavIcon from "../../../assets/icons/bookmark.svg";
import { useReadMessageMutation } from "../../../app/services/message";
import User from "../../../common/component/User"; import User from "../../../common/component/User";
import Layout from "../Layout"; import Layout from "../Layout";
import LoadMore from "../LoadMore";
import { renderMessageFragment } from "../utils";
import useMessageFeed from "../../../common/hook/useMessageFeed";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
import GoBackNav from "../../../common/component/GoBackNav"; import GoBackNav from "../../../common/component/GoBackNav";
type Props = { type Props = {
@@ -17,30 +12,12 @@ type Props = {
dropFiles?: File[]; dropFiles?: File[];
}; };
const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => { const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
const { const { currUser } = useAppSelector((store) => {
pulling,
list: msgIds,
appends,
hasMore,
pullUp
} = useMessageFeed({
context: "user",
id: uid
});
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const { currUser, messageData, footprint, loginUid, selects } = useAppSelector((store) => {
return { return {
selects: store.ui.selectMessages[`user_${uid}`],
loginUid: store.authData.user?.uid,
footprint: store.footprint,
currUser: store.users.byId[uid], currUser: store.users.byId[uid],
messageData: store.message
}; };
}); });
if (!currUser) return null; if (!currUser) return null;
const readIndex = footprint.readUsers[uid];
const feeds = [...msgIds, ...appends];
return ( return (
<Layout <Layout
to={uid} to={uid}
@@ -70,25 +47,7 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
<User interactive={false} uid={currUser.uid} /> <User interactive={false} uid={currUser.uid} />
</header> </header>
} }
> />
<>
{hasMore ? <LoadMore pullUp={pullUp} pulling={pulling} /> : null}
{[...feeds].map((mid, idx) => {
const curr = messageData[mid];
const prev = idx == 0 ? null : messageData[feeds[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
prev,
curr,
contextId: uid,
context: "user"
});
})}
</>
</Layout>
); );
}; };
export default DMChat; export default DMChat;
+95
View File
@@ -0,0 +1,95 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, useLocation } from 'react-router-dom';
import { useDebounce } from 'rooks';
import { ViewportList } from 'react-viewport-list';
import { useReadMessageMutation } from '../../../app/services/message';
import { useAppSelector } from '../../../app/store';
import EditIcon from "../../../assets/icons/edit.svg";
import { renderMessageFragment } from '../utils';
type Props = {
context: "user" | "channel",
id: number
}
const MessageFeed = ({ context, id }: Props) => {
// const listRef = useRef<ViewportListRef | null>(null);
const ref = useRef<HTMLDivElement | null>(
null,
);
const { t } = useTranslation("chat");
const { pathname } = useLocation();
const [updateReadIndex] = useReadMessageMutation();
const updateReadDebounced = useDebounce(updateReadIndex, 300);
const {
mids,
selects,
data,
messageData,
loginUser,
footprint
} = useAppSelector((store) => {
return {
mids: context == "user" ? store.userMessage.byId[id] : store.channelMessage[id],
selects: store.ui.selectMessages[`${context}_${id}`],
footprint: store.footprint,
loginUser: store.authData.user,
data: context == "channel" ? store.channels.byId[id] : undefined,
messageData: store.message || {}
};
});
// useEffect(() => {
// // 滚动到记忆位置
// if (listRef && listRef.current) {
// listRef.current.scrollToIndex({ index: 5 });
// }
// }, [context, id]);
const readIndex = context == "channel" ? footprint.readChannels[id] : Number.POSITIVE_INFINITY;
const isEmptyList = !mids || mids.length == 0;
return (
<article ref={ref} id={`VOCECHAT_FEED_${context}_${id}`} className="w-full h-full px-1 md:px-4 py-4.5 overflow-x-hidden overflow-y-scroll will-change-contents">
{context == "channel" && <div className="pt-14 px-1 md:px-0 flex flex-col items-start gap-2">
<h2 className="font-bold text-4xl dark:text-white">{t("welcome_channel", { name: data?.name })}</h2>
<p className="text-gray-600 dark:text-gray-300">{t("welcome_desc", { name: data?.name })} </p>
{loginUser?.is_admin && (
<NavLink to={`/setting/channel/${id}/overview?f=${pathname}`} className="flex items-center gap-1 bg-clip-text text-fill-transparent bg-gradient-to-r from-blue-500 to-primary-400 ">
<EditIcon className="w-4 h-4 fill-blue-500" />
{t("edit_channel")}
</NavLink>
)}
</div>}
{isEmptyList ? null : <ViewportList
overscan={10}
// itemSize={100}
initialPrerender={50}
scrollThreshold={2000}
// ref={listRef}
viewportRef={ref}
items={mids}
>
{((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev = isFirst ? null : messageData[mids[idx - 1]];
const read = curr?.from_uid == loginUser?.uid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
updateReadIndex: updateReadDebounced,
read,
prev,
curr,
contextId: id,
context
});
})}
</ViewportList>}
</article>
);
};
export default MessageFeed;
+2 -5
View File
@@ -15,10 +15,10 @@ import LicenseUpgradeTip from "./LicenseOutdatedTip";
import DnDTip from "./DnDTip"; import DnDTip from "./DnDTip";
import IconWarning from '../../../assets/icons/warning.svg'; import IconWarning from '../../../assets/icons/warning.svg';
import ImagePreview from "../../../common/component/ImagePreview"; import ImagePreview from "../../../common/component/ImagePreview";
import MessageFeed from "./MessageFeed";
interface Props { interface Props {
readonly?: boolean; readonly?: boolean;
children: ReactElement;
header: ReactElement; header: ReactElement;
aside?: ReactElement | null; aside?: ReactElement | null;
users?: ReactElement | null; users?: ReactElement | null;
@@ -29,7 +29,6 @@ interface Props {
const Layout: FC<Props> = ({ const Layout: FC<Props> = ({
readonly = false, readonly = false,
children,
header, header,
aside = null, aside = null,
users = null, users = null,
@@ -97,9 +96,7 @@ const Layout: FC<Props> = ({
<div className="w-full h-full flex items-start justify-between relative" > <div className="w-full h-full flex items-start justify-between relative" >
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}> <div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
{/* 消息流 */} {/* 消息流 */}
<article id={`VOCECHAT_FEED_${context}_${to}`} className="w-full h-full px-1 md:px-4 py-4.5 overflow-x-hidden overflow-y-scroll"> <MessageFeed context={context} id={to} />
{children}
</article>
{/* 发送框 */} {/* 发送框 */}
<div className={`px-2 py-0 md:p-4 ${selects ? "selecting" : ""}`}> <div className={`px-2 py-0 md:p-4 ${selects ? "selecting" : ""}`}>
{readonly ? ( {readonly ? (
+37 -23
View File
@@ -1,20 +1,21 @@
import { FC } from "react"; import { FC, useRef } from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ViewportList } from "react-viewport-list";
import Session from "./Session"; import Session from "./Session";
import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal"; import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
import InviteModal from "../../../common/component/InviteModal"; import InviteModal from "../../../common/component/InviteModal";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
export interface ChatSession { export interface ChatSession {
key: string;
type: "user" | "channel"; type: "user" | "channel";
id: number; id: number;
mid?: number; mid: number;
unread?: number; unread: number;
} }
type Props = { type Props = {
tempSession?: ChatSession; tempSession?: ChatSession;
}; };
const SessionList: FC<Props> = ({ tempSession }) => { const SessionList: FC<Props> = ({ tempSession }) => {
const ref = useRef<HTMLUListElement | null>(null);
const [deleteId, setDeleteId] = useState<number>(); const [deleteId, setDeleteId] = useState<number>();
const [inviteChannelId, setInviteChannelId] = useState<number>(); const [inviteChannelId, setInviteChannelId] = useState<number>();
const [sessions, setSessions] = useState<ChatSession[]>([]); const [sessions, setSessions] = useState<ChatSession[]>([]);
@@ -35,27 +36,31 @@ const SessionList: FC<Props> = ({ tempSession }) => {
const cSessions = channelIDs.map((id) => { const cSessions = channelIDs.map((id) => {
const mids = channelMessage[id]; const mids = channelMessage[id];
if (!mids || mids.length == 0) { if (!mids || mids.length == 0) {
return { key: `channel_${id}`, unreads: 0, id, type: "channel" }; return { unreads: 0, id, type: "channel" };
} }
// 先转换成数字,再排序 // 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop(); const mid = [...mids].sort((a, b) => +a - +b).pop();
return { key: `channel_${id}`, id, mid, type: "channel" }; return { id, mid, type: "channel" };
}); });
const uSessions = DMs.map((id) => { const uSessions = DMs.map((id) => {
console.log("adddd", id);
const mids = userMessage[id]; const mids = userMessage[id];
if (!mids || mids.length == 0) { if (!mids || mids.length == 0) {
return { key: `user_${id}`, unreads: 0, id, type: "user" }; return { unreads: 0, id, type: "user" };
} }
// 先转换成数字,再排序 // 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop(); const mid = [...mids].sort((a, b) => +a - +b).pop();
return { key: `user_${id}`, type: "user", id, mid }; return { type: "user", id, mid };
}); });
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => { const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
const { mid: aMid = 0 } = a; const { mid: aMid = 0 } = a;
const { mid: bMid = 0 } = b; const { mid: bMid = 0 } = b;
return bMid - aMid; return bMid - aMid;
}); });
setSessions(tempSession ? [tempSession, ...temps] : temps); console.log("before qqqq", temps);
const newSessions = tempSession ? [tempSession, ...temps] : temps;
console.log("qqqq", newSessions);
setSessions(newSessions);
}, [ }, [
channelIDs, channelIDs,
DMs, DMs,
@@ -66,22 +71,31 @@ const SessionList: FC<Props> = ({ tempSession }) => {
userMessage, userMessage,
tempSession tempSession
]); ]);
return ( return (
<> <>
<ul className="flex flex-col gap-0.5 p-2 overflow-auto"> <ul ref={ref} className="flex flex-col gap-0.5 p-2 overflow-auto">
{sessions.map((s) => { <ViewportList
const { key, type, id, mid = 0 } = s; initialPrerender={10}
return ( viewportRef={ref}
<Session items={sessions}
key={key} >
type={type} {(s) => {
id={id} console.log("ssss", sessions);
mid={mid} const { type, id, mid = 0 } = s;
setInviteChannelId={setInviteChannelId} const key = `${type}_${id}`;
setDeleteChannelId={setDeleteId} return (
/> <Session
); key={key}
})} type={type}
id={id}
mid={mid}
setInviteChannelId={setInviteChannelId}
setDeleteChannelId={setDeleteId}
/>
);
}}
</ViewportList>
</ul> </ul>
{!!deleteId && ( {!!deleteId && (
<DeleteChannelConfirmModal <DeleteChannelConfirmModal
+8 -8
View File
@@ -36,15 +36,15 @@ function ChatPage() {
const toggleSessionList = () => { const toggleSessionList = () => {
setSessionListVisible(prev => !prev); setSessionListVisible(prev => !prev);
}; };
const tmpSession = const tmpSession = user_id == 0 ? undefined :
sessionUids.findIndex((i) => i == user_id) > -1 sessionUids.findIndex((i) => i == +user_id) == -1
? undefined ? {
: { mid: 0,
key: `user_${user_id}`, unread: 0,
unreads: 0,
id: +user_id, id: +user_id,
type: "user" as "user" | "channel" type: "user" as const
}; }
: undefined;
// console.log("temp uid", tmpUid); // console.log("temp uid", tmpUid);
const placeholderVisible = channel_id == 0 && user_id == 0; const placeholderVisible = channel_id == 0 && user_id == 0;
const isMainPath = isHomePath || isChatHomePath; const isMainPath = isHomePath || isChatHomePath;