refactor: load more in msg feed
This commit is contained in:
@@ -8,10 +8,9 @@ import { removeMessage } from "../slices/message";
|
||||
import { removeChannelSession } from "../slices/message.channel";
|
||||
import { removeReactionMessage } from "../slices/message.reaction";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
|
||||
import { Channel, ChannelDTO, CreateChannelDTO } from "../../types/channel";
|
||||
import { RootState } from "../store";
|
||||
import { ContentTypeKey, ChatMessage } from "../../types/message";
|
||||
import { ContentTypeKey } from "../../types/message";
|
||||
|
||||
export const channelApi = createApi({
|
||||
reducerPath: "channelApi",
|
||||
@@ -76,21 +75,6 @@ export const channelApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
getHistoryMessages: builder.query<ChatMessage[], { id: number; mid?: number; limit?: number }>({
|
||||
query: ({ id, mid = null, limit = 100 }) => ({
|
||||
url: mid
|
||||
? `/group/${id}/history?before=${mid}&limit=${limit}`
|
||||
: `/group/${id}/history?limit=${limit}`
|
||||
}),
|
||||
async onQueryStarted(params, { dispatch, getState, queryFulfilled }) {
|
||||
const { data: messages } = await queryFulfilled;
|
||||
if (messages?.length) {
|
||||
messages.forEach((msg) => {
|
||||
handleChatMessage(msg, dispatch, getState() as RootState);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
createInviteLink: builder.query<string, number | void>({
|
||||
query: (gid) => ({
|
||||
headers: {
|
||||
@@ -211,7 +195,6 @@ export const {
|
||||
useLazyLeaveChannelQuery,
|
||||
useLazyCreateInviteLinkQuery,
|
||||
useCreateInviteLinkQuery,
|
||||
useLazyGetHistoryMessagesQuery,
|
||||
useGetChannelQuery,
|
||||
useUpdateChannelMutation,
|
||||
useLazyRemoveChannelQuery,
|
||||
|
||||
@@ -6,7 +6,8 @@ import { onMessageSendStarted } from "./handlers";
|
||||
import { normalizeArchiveData } from "../../common/utils";
|
||||
import baseQuery from "./base.query";
|
||||
import { Archive, FavoriteArchive, OG } from "../../types/resource";
|
||||
import { ContentTypeKey, UploadFileResponse } from "../../types/message";
|
||||
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "../../types/message";
|
||||
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
|
||||
import { RootState } from "../store";
|
||||
|
||||
export const messageApi = createApi({
|
||||
@@ -147,7 +148,25 @@ export const messageApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
loadMoreMessages: builder.query<ChatMessage[], { context?: "channel" | "user", id: number; mid?: number; limit?: number }>({
|
||||
query: ({ context = "channel", id, mid = "", limit = 100 }) => {
|
||||
const url = context == "channel" ?
|
||||
`/group/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}`
|
||||
:
|
||||
`/user/${id}/history?limit=${limit}${mid ? `&before=${mid}` : ""}`;
|
||||
return {
|
||||
url
|
||||
};
|
||||
},
|
||||
async onQueryStarted(params, { dispatch, getState, queryFulfilled }) {
|
||||
const { data: messages } = await queryFulfilled;
|
||||
if (messages?.length) {
|
||||
messages.forEach((msg) => {
|
||||
handleChatMessage(msg, dispatch, getState() as RootState);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
replyWithChatGPT: builder.mutation<{ message: string, prompt: string }, string>({
|
||||
query: (prompt) => ({
|
||||
url: `https://official.voce.chat/chatgpt/complete`,
|
||||
@@ -218,5 +237,6 @@ export const {
|
||||
useLazyDeleteMessageQuery,
|
||||
useReadMessageMutation,
|
||||
useCreateArchiveMutation,
|
||||
useReplyWithChatGPTMutation
|
||||
useReplyWithChatGPTMutation,
|
||||
useLazyLoadMoreMessagesQuery
|
||||
} = messageApi;
|
||||
|
||||
@@ -5,10 +5,8 @@ import { updateAutoDeleteSetting, updateMute } from "../slices/footprint";
|
||||
import { fillUsers } from "../slices/users";
|
||||
import BASE_URL, { ContentTypes } from "../config";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
|
||||
import { AutoDeleteMsgDTO, BotAPIKey, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
|
||||
import { ChatMessage, ContentTypeKey, MuteDTO } from "../../types/message";
|
||||
import { RootState } from "../store";
|
||||
import { ContentTypeKey, MuteDTO } from "../../types/message";
|
||||
|
||||
export const userApi = createApi({
|
||||
reducerPath: "userApi",
|
||||
@@ -157,21 +155,6 @@ export const userApi = createApi({
|
||||
await onMessageSendStarted.call(this, param1, param2, "user");
|
||||
}
|
||||
}),
|
||||
getHistoryMessages: builder.query<ChatMessage[], { id: number; mid?: number; limit?: number }>({
|
||||
query: ({ id, mid = null, limit = 100 }) => ({
|
||||
url: mid
|
||||
? `/user/${id}/history?before=${mid}&limit=${limit}`
|
||||
: `/user/${id}/history?limit=${limit}`
|
||||
}),
|
||||
async onQueryStarted(params, { dispatch, getState, queryFulfilled }) {
|
||||
const { data: messages } = await queryFulfilled;
|
||||
if (messages?.length) {
|
||||
messages.forEach((msg) => {
|
||||
handleChatMessage(msg, dispatch, getState() as RootState);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
})
|
||||
});
|
||||
@@ -181,7 +164,6 @@ export const {
|
||||
useUpdateAvatarByAdminMutation,
|
||||
useUpdateAutoDeleteMsgMutation,
|
||||
useCreateUserMutation,
|
||||
useLazyGetHistoryMessagesQuery,
|
||||
useUpdateUserMutation,
|
||||
useUpdateMuteSettingMutation,
|
||||
useLazyDeleteUserQuery,
|
||||
|
||||
@@ -6,13 +6,13 @@ import { useLazyGetUsersQuery } from "../../app/services/user";
|
||||
import { useLazyGetServerQuery } from "../../app/services/server";
|
||||
import useStreaming from "./useStreaming";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel";
|
||||
import { useLazyLoadMoreMessagesQuery } from "../../app/services/message";
|
||||
// type Props={
|
||||
// guest?:boolean
|
||||
// }
|
||||
let preloadChannelMsgs = false;
|
||||
export default function usePreload() {
|
||||
const [preloadChannelMessages] = useLazyGetHistoryMessagesQuery();
|
||||
const [preloadChannelMessages] = useLazyLoadMoreMessagesQuery();
|
||||
const { rehydrate, rehydrated } = useRehydrate();
|
||||
const {
|
||||
loginUid,
|
||||
|
||||
@@ -54,69 +54,64 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const addVisible = loginUser?.is_admin || owner == loginUser?.uid;
|
||||
const pinCount = data?.pinned_messages?.length || 0;
|
||||
const toolClass = `relative cursor-pointer`;
|
||||
return (
|
||||
<>
|
||||
|
||||
<Layout
|
||||
to={cid}
|
||||
context="channel"
|
||||
dropFiles={dropFiles}
|
||||
aside={
|
||||
<ul className="flex flex-col gap-6">
|
||||
<Tooltip tip={t("pin")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[0, 150]}
|
||||
interactive
|
||||
trigger="click"
|
||||
content={<PinList id={cid} />}
|
||||
>
|
||||
<li className={`${toolClass}`}>
|
||||
{pinCount > 0 ? <span className="absolute -top-2 -right-2 flex-center w-4 h-4 rounded-full bg-primary-400 text-white font-bold text-[10px]">{pinCount}</span> : null}
|
||||
<IconPin className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<Tooltip tip={t("fav")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[0, 164]}
|
||||
interactive
|
||||
trigger="click"
|
||||
content={<FavList cid={cid} />}
|
||||
>
|
||||
<li className={`${toolClass}`}>
|
||||
<IconFav className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<li
|
||||
className={`${toolClass}`}
|
||||
onClick={toggleMembersVisible}
|
||||
>
|
||||
<Tooltip tip={t("channel_members")} placement="left">
|
||||
<IconPeople className={membersVisible ? "fill-gray-600" : ""} />
|
||||
</Tooltip>
|
||||
return <Layout
|
||||
to={cid}
|
||||
context="channel"
|
||||
dropFiles={dropFiles}
|
||||
aside={
|
||||
<ul className="flex flex-col gap-6">
|
||||
<Tooltip tip={t("pin")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[0, 150]}
|
||||
interactive
|
||||
trigger="click"
|
||||
content={<PinList id={cid} />}
|
||||
>
|
||||
<li className={`${toolClass}`}>
|
||||
{pinCount > 0 ? <span className="absolute -top-2 -right-2 flex-center w-4 h-4 rounded-full bg-primary-400 text-white font-bold text-[10px]">{pinCount}</span> : null}
|
||||
<IconPin className="fill-gray-500" />
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
header={
|
||||
<header className="px-5 py-4 flex items-center justify-center md:justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
|
||||
<GoBackNav />
|
||||
<div className="flex items-center gap-1">
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className="text-gray-800 dark:text-white">{name}</span>
|
||||
<span className="ml-2 text-gray-500 hidden md:block">{description}</span>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
users={
|
||||
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<Tooltip tip={t("fav")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[0, 164]}
|
||||
interactive
|
||||
trigger="click"
|
||||
content={<FavList cid={cid} />}
|
||||
>
|
||||
<li className={`${toolClass}`}>
|
||||
<IconFav className="fill-gray-500" />
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<li
|
||||
className={`${toolClass}`}
|
||||
onClick={toggleMembersVisible}
|
||||
>
|
||||
<Tooltip tip={t("channel_members")} placement="left">
|
||||
<IconPeople className={membersVisible ? "fill-gray-600" : ""} />
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
header={
|
||||
<header className="px-5 py-4 flex items-center justify-center md:justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
|
||||
<GoBackNav />
|
||||
<div className="flex items-center gap-1">
|
||||
<ChannelIcon personal={!is_public} />
|
||||
<span className="text-gray-800 dark:text-white">{name}</span>
|
||||
<span className="ml-2 text-gray-500 hidden md:block">{description}</span>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
users={
|
||||
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} />
|
||||
}
|
||||
/>;
|
||||
}
|
||||
export default memo(ChannelChat, (prev, next) => prev.cid == next.cid);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// import { useState, useEffect } from "react";
|
||||
// import { NavLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMessageFeed from "../../../common/hook/useMessageFeed";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import Layout from "../Layout";
|
||||
import { renderMessageFragment } from "../utils";
|
||||
import LoadMore from "../LoadMore";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import GoBackNav from "../../../common/component/GoBackNav";
|
||||
|
||||
@@ -13,27 +10,14 @@ type Props = {
|
||||
cid?: number;
|
||||
};
|
||||
export default function GuestChannelChat({ cid = 0 }: Props) {
|
||||
const { t } = useTranslation("chat");
|
||||
const {
|
||||
list: msgIds,
|
||||
appends,
|
||||
hasMore,
|
||||
pullUp,
|
||||
pulling
|
||||
} = useMessageFeed({
|
||||
context: "channel",
|
||||
id: cid
|
||||
});
|
||||
const { data, messageData } = useAppSelector((store) => {
|
||||
// const { t } = useTranslation("chat");
|
||||
const { data } = useAppSelector((store) => {
|
||||
return {
|
||||
footprint: store.footprint,
|
||||
data: store.channels.byId[cid],
|
||||
messageData: store.message || {}
|
||||
};
|
||||
});
|
||||
if (!data) return null;
|
||||
const { name, description, is_public } = data;
|
||||
const feeds = [...msgIds, ...appends];
|
||||
return (
|
||||
<Layout
|
||||
readonly
|
||||
@@ -49,32 +33,6 @@ export default function GuestChannelChat({ cid = 0 }: Props) {
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{hasMore ? (
|
||||
<LoadMore pullUp={pullUp} pulling={pulling} />
|
||||
) : (
|
||||
<div className="pt-14 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>
|
||||
</div>
|
||||
)}
|
||||
{/* <div className="feed"> */}
|
||||
{feeds.map((mid, idx) => {
|
||||
const curr = messageData[mid];
|
||||
if (!curr) return null;
|
||||
const isFirst = idx == 0;
|
||||
const prev = isFirst ? null : messageData[feeds[idx - 1]];
|
||||
return renderMessageFragment({
|
||||
readonly: true,
|
||||
selectMode: false,
|
||||
prev,
|
||||
curr,
|
||||
contextId: cid,
|
||||
context: "channel"
|
||||
});
|
||||
})}
|
||||
</>
|
||||
</Layout>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useDebounce } from 'rooks';
|
||||
import { useDebounce, useLocalstorageState } from 'rooks';
|
||||
import { ViewportList, ViewportListRef } from 'react-viewport-list';
|
||||
import { Waveform } from '@uiball/loaders';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useReadMessageMutation } from '../../../app/services/message';
|
||||
import { useLazyLoadMoreMessagesQuery, useReadMessageMutation } from '../../../app/services/message';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import EditIcon from "../../../assets/icons/edit.svg";
|
||||
import { renderMessageFragment } from '../utils';
|
||||
import { KEY_UID } from '../../../app/config';
|
||||
// import LoadMore from '../LoadMore';
|
||||
|
||||
const checkHistory = (key: string) => {
|
||||
const currUid = localStorage.getItem(KEY_UID) ?? "";
|
||||
const _key = `${currUid}_${key}`;
|
||||
const local = Number(localStorage.getItem(_key) ?? 0);
|
||||
return local == 0;
|
||||
};
|
||||
|
||||
const setHistory = (key: string) => {
|
||||
const currUid = localStorage.getItem(KEY_UID) ?? "";
|
||||
const _key = `${currUid}_${key}`;
|
||||
localStorage.setItem(_key, "1");
|
||||
};
|
||||
type Props = {
|
||||
context: "user" | "channel",
|
||||
id: number
|
||||
}
|
||||
const triggerScrollHeight = 400;
|
||||
const MessageFeed = ({ context, id }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] = useLazyLoadMoreMessagesQuery();
|
||||
const [historyMid, setHistoryMid] = useLocalstorageState(`history_mid_${context}_${id}`, "");
|
||||
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);
|
||||
@@ -41,21 +58,49 @@ const MessageFeed = ({ context, id }: Props) => {
|
||||
messageData: store.message || {}
|
||||
};
|
||||
});
|
||||
const debouncedScrollHandler = useDebounce(() => {
|
||||
const container = ref ? ref.current : null;
|
||||
if (container && historyMid && !loadingMore) {
|
||||
if (container.scrollTop <= 200) {
|
||||
// pull up
|
||||
console.log("start pul up");
|
||||
loadMoreMessage({ context, id, mid: +historyMid });
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
useEffect(() => {
|
||||
if (isSuccess && historyData) {
|
||||
if (historyData.length == 0) {
|
||||
// 到顶了
|
||||
setHistory(`history_${context}_${id}`);
|
||||
} else {
|
||||
// 记录最新的mid
|
||||
const [{ mid }] = historyData;
|
||||
setHistoryMid(String(mid));
|
||||
}
|
||||
}
|
||||
}, [isSuccess, historyData, context, id]);
|
||||
|
||||
useEffect(() => {
|
||||
// context changed, scroll to bottom
|
||||
console.log("listRef", listRef);
|
||||
if (listRef && listRef.current) {
|
||||
const list = listRef.current;
|
||||
ref.current?.classList.remove("scroll-smooth");
|
||||
list.scrollToIndex({
|
||||
prerender: 40,
|
||||
index: Number.POSITIVE_INFINITY
|
||||
});
|
||||
setTimeout(() => {
|
||||
// tricky
|
||||
ref.current?.classList.add("scroll-smooth");
|
||||
}, 150);
|
||||
}
|
||||
|
||||
const hasHistory = checkHistory(`history_${context}_${id}`);
|
||||
const container = ref ? ref.current : null;
|
||||
if (hasHistory && container) {
|
||||
container.addEventListener("scroll", debouncedScrollHandler);
|
||||
}
|
||||
return () => {
|
||||
if (hasHistory && container) {
|
||||
container.removeEventListener("scroll", debouncedScrollHandler);
|
||||
}
|
||||
};
|
||||
}, [context, id]);
|
||||
useEffect(() => {
|
||||
// check current scroll position first, scroll to bottom only when under the trigger number
|
||||
@@ -64,22 +109,35 @@ const MessageFeed = ({ context, id }: Props) => {
|
||||
const { scrollHeight, scrollTop, offsetHeight } = container;
|
||||
const deltaNum = scrollHeight - scrollTop - offsetHeight;
|
||||
console.log("delta", deltaNum);
|
||||
|
||||
// sent by myself (tricky!)
|
||||
const [lastMid] = mids ? mids.slice(-1) : [0];
|
||||
const ts = new Date().getTime();
|
||||
const isSentByMyself = ts - lastMid < 1000;
|
||||
if (deltaNum < triggerScrollHeight) {
|
||||
container.classList.add("scroll-smooth");
|
||||
// scroll to bottom
|
||||
container.scrollTop = container.scrollHeight;
|
||||
container.classList.remove("scroll-smooth");
|
||||
} else if (isSentByMyself) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
}, [mids]);
|
||||
// 首次进入,就用mids
|
||||
const lastMid = mids ? mids.slice(0, 1)[0] : "";
|
||||
if (!historyMid && lastMid) {
|
||||
setHistoryMid(`${lastMid}`);
|
||||
}
|
||||
}, [mids, historyMid]);
|
||||
|
||||
const handleMessageListChange = (data: [number, number]) => {
|
||||
const [first, last] = data;
|
||||
console.log("index changed", data);
|
||||
};
|
||||
const readIndex = context == "channel" ? footprint.readChannels[id] : Number.POSITIVE_INFINITY;
|
||||
// const handleMessageListChange = (data: [number, number]) => {
|
||||
// const [first, last] = data;
|
||||
// // todo
|
||||
// // console.log("index changed", data);
|
||||
// };
|
||||
const readIndex = context == "channel" ? footprint.readChannels[id] : footprint.readUsers[id];
|
||||
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 scroll-smooth will-change-contents">
|
||||
<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>
|
||||
@@ -90,8 +148,11 @@ const MessageFeed = ({ context, id }: Props) => {
|
||||
</NavLink>
|
||||
)}
|
||||
</div>}
|
||||
<div className={clsx("mt-2 w-full py-2", loadingMore ? "flex-center" : "hidden")} >
|
||||
<Waveform size={18} lineWeight={4} speed={1} color="#ccc" />
|
||||
</div>
|
||||
{isEmptyList ? null : <ViewportList
|
||||
onViewportIndexesChange={handleMessageListChange}
|
||||
// onViewportIndexesChange={handleMessageListChange}
|
||||
overscan={10}
|
||||
// itemSize={100}
|
||||
initialPrerender={40}
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { useEffect, FC } from "react";
|
||||
import { FC } from "react";
|
||||
import { Waveform } from "@uiball/loaders";
|
||||
import { useInViewRef } from "rooks";
|
||||
|
||||
type Props = {
|
||||
pulling?: boolean;
|
||||
pullUp: () => Promise<void> | null;
|
||||
};
|
||||
const LoadMore: FC<Props> = ({ pullUp = null, pulling }) => {
|
||||
const [myRef, inView] = useInViewRef();
|
||||
useEffect(() => {
|
||||
if (inView && pullUp && !pulling) {
|
||||
pullUp();
|
||||
}
|
||||
}, [inView, pullUp, pulling]);
|
||||
const LoadMore: FC<Props> = () => {
|
||||
return (
|
||||
<div data-load-more className="mt-2 flex-center w-full py-2" ref={myRef}>
|
||||
<div data-load-more className="mt-2 flex-center w-full py-2" >
|
||||
<Waveform size={18} lineWeight={4} speed={1} color="#ccc" />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user