diff --git a/src/app/services/channel.ts b/src/app/services/channel.ts index 03973b00..1acdd222 100644 --- a/src/app/services/channel.ts +++ b/src/app/services/channel.ts @@ -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({ - 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({ query: (gid) => ({ headers: { @@ -211,7 +195,6 @@ export const { useLazyLeaveChannelQuery, useLazyCreateInviteLinkQuery, useCreateInviteLinkQuery, - useLazyGetHistoryMessagesQuery, useGetChannelQuery, useUpdateChannelMutation, useLazyRemoveChannelQuery, diff --git a/src/app/services/message.ts b/src/app/services/message.ts index 0429eb8a..aa087403 100644 --- a/src/app/services/message.ts +++ b/src/app/services/message.ts @@ -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({ + 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; diff --git a/src/app/services/user.ts b/src/app/services/user.ts index 8dd88835..d9b11c19 100644 --- a/src/app/services/user.ts +++ b/src/app/services/user.ts @@ -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({ - 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, diff --git a/src/common/hook/usePreload.ts b/src/common/hook/usePreload.ts index 26b18cf2..8673200b 100644 --- a/src/common/hook/usePreload.ts +++ b/src/common/hook/usePreload.ts @@ -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, diff --git a/src/routes/chat/ChannelChat/index.tsx b/src/routes/chat/ChannelChat/index.tsx index 7d143ce9..bf875154 100644 --- a/src/routes/chat/ChannelChat/index.tsx +++ b/src/routes/chat/ChannelChat/index.tsx @@ -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 ( - <> - - - - } - > -
  • - {pinCount > 0 ? {pinCount} : null} - -
  • -
    -
    - - } - > -
  • - -
  • -
    -
    -
  • - - - + return + + } + > +
  • + {pinCount > 0 ? {pinCount} : null} +
  • - - } - header={ -
    - -
    - - {name} - {description} -
    -
    - } - users={ - - } - /> - - ); + + + + } + > +
  • + +
  • +
    +
    +
  • + + + +
  • + + } + header={ +
    + +
    + + {name} + {description} +
    +
    + } + users={ + + } + />; } export default memo(ChannelChat, (prev, next) => prev.cid == next.cid); diff --git a/src/routes/chat/GuestChannelChat/index.tsx b/src/routes/chat/GuestChannelChat/index.tsx index 6efc1992..56a83d8d 100644 --- a/src/routes/chat/GuestChannelChat/index.tsx +++ b/src/routes/chat/GuestChannelChat/index.tsx @@ -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 ( } - > - <> - {hasMore ? ( - - ) : ( -
    -

    {t("welcome_channel", { name })}

    -

    {t("welcome_desc", { name })}

    -
    - )} - {/*
    */} - {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" - }); - })} - - + /> ); } diff --git a/src/routes/chat/Layout/MessageFeed.tsx b/src/routes/chat/Layout/MessageFeed.tsx index e5428a4e..6566ab8d 100644 --- a/src/routes/chat/Layout/MessageFeed.tsx +++ b/src/routes/chat/Layout/MessageFeed.tsx @@ -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(null); const ref = useRef( 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 ( -
    +
    {context == "channel" &&

    {t("welcome_channel", { name: data?.name })}

    {t("welcome_desc", { name: data?.name })}

    @@ -90,8 +148,11 @@ const MessageFeed = ({ context, id }: Props) => { )}
    } +
    + +
    {isEmptyList ? null : Promise | null; }; -const LoadMore: FC = ({ pullUp = null, pulling }) => { - const [myRef, inView] = useInViewRef(); - useEffect(() => { - if (inView && pullUp && !pulling) { - pullUp(); - } - }, [inView, pullUp, pulling]); +const LoadMore: FC = () => { return ( -
    +
    );