From b576a2c7d9547a5fabdc1ab6924749a358df047d Mon Sep 17 00:00:00 2001 From: hyrious Date: Tue, 24 Oct 2023 17:16:24 +0800 Subject: [PATCH 1/2] refactor(flat-stores): load more history rooms on scroll to bottom --- .../components/HomePage/RoomList/index.tsx | 29 +++++- .../HomePage/MainRoomHistoryPanel/index.tsx | 37 +++++--- .../MainRoomListPanel/MainRoomList.tsx | 14 +-- .../src/HomePage/MainRoomListPanel/index.tsx | 10 ++- packages/flat-stores/src/room-store.ts | 88 ++++++++++++++++++- 5 files changed, 153 insertions(+), 25 deletions(-) diff --git a/packages/flat-components/src/components/HomePage/RoomList/index.tsx b/packages/flat-components/src/components/HomePage/RoomList/index.tsx index 71d200efffd..0ea04f2e438 100644 --- a/packages/flat-components/src/components/HomePage/RoomList/index.tsx +++ b/packages/flat-components/src/components/HomePage/RoomList/index.tsx @@ -1,6 +1,6 @@ import "./style.less"; -import React, { PropsWithChildren, ReactElement, useMemo } from "react"; +import React, { PropsWithChildren, ReactElement, useCallback, useMemo, useRef } from "react"; import { Dropdown, Menu } from "antd"; import { SVGDown } from "../../FlatIcons"; @@ -20,6 +20,7 @@ export interface RoomListProps { activeTab?: T; onTabActive?: (key: T) => void; style?: React.CSSProperties; + onScrollToBottom?: () => void; } export function RoomList({ @@ -29,12 +30,30 @@ export function RoomList({ onTabActive, children, style, + onScrollToBottom, }: PropsWithChildren>): ReactElement { const activeTabTitle = useMemo( () => filters?.find(tab => tab.key === activeTab)?.title, [filters, activeTab], ); + const isAtTheBottomRef = useRef(false); + const roomListContainerRef = useRef(null); + + const onScroll = useCallback((): void => { + if (roomListContainerRef.current) { + const { scrollTop, clientHeight, scrollHeight } = roomListContainerRef.current; + const threshold = scrollHeight - 30; + const isAtTheBottom = scrollTop + clientHeight >= threshold; + if (isAtTheBottomRef.current !== isAtTheBottom) { + isAtTheBottomRef.current = isAtTheBottom; + if (isAtTheBottom && onScrollToBottom) { + onScrollToBottom(); + } + } + } + }, [onScrollToBottom]); + return (
@@ -57,7 +76,13 @@ export function RoomList({ )}
-
{children}
+
+ {children} +
); } diff --git a/packages/flat-pages/src/HomePage/MainRoomHistoryPanel/index.tsx b/packages/flat-pages/src/HomePage/MainRoomHistoryPanel/index.tsx index 5c94cc32529..d257193e4d0 100644 --- a/packages/flat-pages/src/HomePage/MainRoomHistoryPanel/index.tsx +++ b/packages/flat-pages/src/HomePage/MainRoomHistoryPanel/index.tsx @@ -1,21 +1,36 @@ // import "../MainRoomListPanel/MainRoomList.less"; -import React from "react"; +import React, { useCallback, useContext } from "react"; import { observer } from "mobx-react-lite"; import { MainRoomList } from "../MainRoomListPanel/MainRoomList"; import { ListRoomsType } from "@netless/flat-server-api"; import { RoomList } from "flat-components"; import { useTranslate } from "@netless/flat-i18n"; +import { RoomStoreContext } from "../../components/StoreProvider"; -export const MainRoomHistoryPanel = observer<{ isLogin: boolean }>(function MainRoomHistoryPanel({ - isLogin, -}) { - const t = useTranslate(); - return ( - - - - ); -}); +interface MainRoomHistoryPanelProps { + isLogin: boolean; +} + +export const MainRoomHistoryPanel = observer( + function MainRoomHistoryPanel({ isLogin }) { + const t = useTranslate(); + const roomStore = useContext(RoomStoreContext); + + const onScrollToBottom = useCallback((): void => { + void roomStore.fetchMoreRooms(ListRoomsType.History); + }, [roomStore]); + + return ( + + + + ); + }, +); export default MainRoomHistoryPanel; diff --git a/packages/flat-pages/src/HomePage/MainRoomListPanel/MainRoomList.tsx b/packages/flat-pages/src/HomePage/MainRoomListPanel/MainRoomList.tsx index 3282887b86c..e2bc37b72d4 100644 --- a/packages/flat-pages/src/HomePage/MainRoomListPanel/MainRoomList.tsx +++ b/packages/flat-pages/src/HomePage/MainRoomListPanel/MainRoomList.tsx @@ -15,8 +15,8 @@ import { errorTips, } from "flat-components"; import { ListRoomsType, RoomStatus, RoomType, stopClass } from "@netless/flat-server-api"; -import { GlobalStoreContext, RoomStoreContext } from "../../components/StoreProvider"; -import { RoomItem } from "@netless/flat-stores"; +import { GlobalStoreContext } from "../../components/StoreProvider"; +import { RoomItem, RoomStore } from "@netless/flat-stores"; import { useSafePromise } from "../../utils/hooks/lifecycle"; import { RouteNameType, usePushHistory } from "../../utils/routes"; import { joinRoomHandler } from "../../utils/join-room-handler"; @@ -25,18 +25,18 @@ import { FLAT_WEB_BASE_URL } from "../../constants/process"; import { generateAvatar } from "../../utils/generate-avatar"; export interface MainRoomListProps { + roomStore: RoomStore; listRoomsType: ListRoomsType; isLogin: boolean; } export const MainRoomList = observer(function MainRoomList({ + roomStore, listRoomsType, isLogin, }) { const t = useTranslate(); - const roomStore = useContext(RoomStoreContext); const [skeletonsVisible, setSkeletonsVisible] = useState(false); - const [roomUUIDs, setRoomUUIDs] = useState(); const [cancelModalVisible, setCancelModalVisible] = useState(false); const [stopModalVisible, setStopModalVisible] = useState(false); const [inviteModalVisible, setInviteModalVisible] = useState(false); @@ -57,10 +57,8 @@ export const MainRoomList = observer(function MainRoomList({ const refreshRooms = useCallback( async function refreshRooms(): Promise { try { - const roomUUIDs = await sp(roomStore.listRooms(listRoomsType, { page: 1 })); - setRoomUUIDs(roomUUIDs); + await sp(roomStore.listRooms(listRoomsType)); } catch (e) { - setRoomUUIDs([]); errorTips(e); } }, @@ -81,6 +79,8 @@ export const MainRoomList = observer(function MainRoomList({ }; }, [refreshRooms, isLogin]); + const roomUUIDs = roomStore.roomUUIDs[listRoomsType]; + if (!roomUUIDs) { return skeletonsVisible ? : null; } diff --git a/packages/flat-pages/src/HomePage/MainRoomListPanel/index.tsx b/packages/flat-pages/src/HomePage/MainRoomListPanel/index.tsx index eb9e98e892d..d09718e97cb 100644 --- a/packages/flat-pages/src/HomePage/MainRoomListPanel/index.tsx +++ b/packages/flat-pages/src/HomePage/MainRoomListPanel/index.tsx @@ -1,14 +1,16 @@ -import React, { useMemo, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; import { RoomList } from "flat-components"; import { MainRoomList } from "./MainRoomList"; import { ListRoomsType } from "@netless/flat-server-api"; import { useTranslate } from "@netless/flat-i18n"; +import { RoomStoreContext } from "../../components/StoreProvider"; export const MainRoomListPanel = observer<{ isLogin: boolean }>(function MainRoomListPanel({ isLogin, }) { const t = useTranslate(); + const roomStore = useContext(RoomStoreContext); const [activeTab, setActiveTab] = useState<"all" | "today" | "periodic">("all"); const filters = useMemo>( () => [ @@ -35,7 +37,11 @@ export const MainRoomListPanel = observer<{ isLogin: boolean }>(function MainRoo title={t("room-list")} onTabActive={setActiveTab} > - + ); }); diff --git a/packages/flat-stores/src/room-store.ts b/packages/flat-stores/src/room-store.ts index aad3ce5fea9..aad18e6c272 100644 --- a/packages/flat-stores/src/room-store.ts +++ b/packages/flat-stores/src/room-store.ts @@ -11,7 +11,6 @@ import { joinRoom, JoinRoomResult, listRooms, - ListRoomsPayload, ListRoomsType, ordinaryRoomInfo, periodicRoomInfo, @@ -24,6 +23,7 @@ import { } from "@netless/flat-server-api"; import { globalStore } from "./global-store"; import { preferencesStore } from "./preferences-store"; +import { isToday } from "date-fns"; export interface RoomRecording { beginTime: number; @@ -66,9 +66,48 @@ export interface PeriodicRoomItem { * This should be the only central store for all the room info. */ export class RoomStore { + public readonly singlePageSize = 50; + public rooms = observable.map(); public periodicRooms = observable.map(); + /** If `fetchMoreRooms()` returns 0 rooms, stop fetching it */ + public hasMoreRooms: Record = { + all: true, + periodic: true, + history: true, + today: true, + }; + + /** Don't invoke `fetchMoreRooms()` too many times */ + public isFetchingRooms = false; + + public get roomUUIDs(): Record { + const roomUUIDs: Record = { + all: [], + history: [], + periodic: [], + today: [], + }; + for (const room of this.rooms.values()) { + const beginTime = room.beginTime ?? Date.now(); + const isHistory = room.roomStatus === RoomStatus.Stopped; + const isPeriodic = Boolean(room.periodicUUID); + if (isHistory) { + roomUUIDs.history.push(room.roomUUID); + } else { + roomUUIDs.all.push(room.roomUUID); + } + if (isPeriodic) { + roomUUIDs.periodic.push(room.roomUUID); + } + if (isToday(beginTime)) { + roomUUIDs.today.push(room.roomUUID); + } + } + return roomUUIDs; + } + public constructor() { makeAutoObservable(this); } @@ -111,8 +150,8 @@ export class RoomStore { /** * @returns a list of room uuids */ - public async listRooms(type: ListRoomsType, payload: ListRoomsPayload): Promise { - const rooms = await listRooms(type, payload); + public async listRooms(type: ListRoomsType): Promise { + const rooms = await listRooms(type, { page: 1 }); const roomUUIDs: string[] = []; runInAction(() => { for (const room of rooms) { @@ -126,6 +165,45 @@ export class RoomStore { return roomUUIDs; } + public async fetchMoreRooms(type: ListRoomsType): Promise { + if (this.isFetchingRooms) { + return; + } + + const counts = this.roomUUIDs; + + const page = Math.ceil(counts[type].length / this.singlePageSize); + const fullPageSize = page * this.singlePageSize; + if (counts[type].length >= fullPageSize && this.hasMoreRooms[type]) { + runInAction(() => { + this.isFetchingRooms = true; + }); + + try { + const rooms = await listRooms(type, { + page: page + 1, + }); + + this.hasMoreRooms[type] = rooms.length > 0; + + runInAction(() => { + this.isFetchingRooms = false; + + for (const room of rooms) { + this.updateRoom(room.roomUUID, room.ownerUUID, { + ...room, + periodicUUID: room.periodicUUID || void 0, + }); + } + }); + } catch { + runInAction(() => { + this.isFetchingRooms = false; + }); + } + } + } + public async cancelRoom(payload: CancelRoomPayload): Promise { await cancelRoom(payload); } @@ -218,3 +296,7 @@ export class RoomStore { } export const roomStore = new RoomStore(); + +if (process.env.DEV) { + (window as any).roomStore = roomStore; +} From 7daf289697b5611e68fc3be137652b79fa655768 Mon Sep 17 00:00:00 2001 From: hyrious Date: Tue, 24 Oct 2023 17:38:16 +0800 Subject: [PATCH 2/2] perf(flat-components): stop animation when invisible --- .../src/components/ClassroomPage/RaiseHand/style.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flat-components/src/components/ClassroomPage/RaiseHand/style.less b/packages/flat-components/src/components/ClassroomPage/RaiseHand/style.less index cc3293410f5..24e0a0ec0ee 100644 --- a/packages/flat-components/src/components/ClassroomPage/RaiseHand/style.less +++ b/packages/flat-components/src/components/ClassroomPage/RaiseHand/style.less @@ -23,7 +23,7 @@ left: 50%; transform: translate(-50%, -50%); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 50 50'%3E%3Cg filter='url(%23a)'%3E%3Cpath fill='%233381FF' fill-rule='evenodd' d='M2 25c0 12.703 10.297 23 23 23v2C11.193 50 0 38.807 0 25h2Zm46 0C48 12.297 37.703 2 25 2V0c13.807 0 25 11.193 25 25h-2Z' clip-rule='evenodd'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='a' width='60.873' height='60.873' x='-5.437' y='-5.437' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeGaussianBlur in='BackgroundImageFix' stdDeviation='2.718'/%3E%3CfeComposite in2='SourceAlpha' operator='in' result='effect1_backgroundBlur_4735_3980'/%3E%3CfeBlend in='SourceGraphic' in2='effect1_backgroundBlur_4735_3980' result='shape'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E"); // cspell:disable-line - animation: loading 1.5s infinite linear; + animation: none; opacity: 0; transition: opacity 0.3s ease-in-out; } @@ -38,6 +38,7 @@ } &.is-active::after { + animation: loading 1.5s infinite linear; opacity: 1; } }