Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(flat-stores): load more history rooms on scroll end #2055

Merged
merged 2 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -38,6 +38,7 @@
}

&.is-active::after {
animation: loading 1.5s infinite linear;
opacity: 1;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -20,6 +20,7 @@ export interface RoomListProps<T extends string> {
activeTab?: T;
onTabActive?: (key: T) => void;
style?: React.CSSProperties;
onScrollToBottom?: () => void;
}

export function RoomList<T extends string>({
Expand All @@ -29,12 +30,30 @@ export function RoomList<T extends string>({
onTabActive,
children,
style,
onScrollToBottom,
}: PropsWithChildren<RoomListProps<T>>): ReactElement {
const activeTabTitle = useMemo(
() => filters?.find(tab => tab.key === activeTab)?.title,
[filters, activeTab],
);

const isAtTheBottomRef = useRef(false);
const roomListContainerRef = useRef<HTMLDivElement>(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 (
<div className="room-list" style={style}>
<div className="room-list-header">
Expand All @@ -57,7 +76,13 @@ export function RoomList<T extends string>({
</Dropdown>
)}
</div>
<div className="room-list-body fancy-scrollbar">{children}</div>
<div
ref={roomListContainerRef}
className="room-list-body fancy-scrollbar"
onScroll={onScroll}
>
{children}
</div>
</div>
);
}
37 changes: 26 additions & 11 deletions packages/flat-pages/src/HomePage/MainRoomHistoryPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<RoomList title={t("history")}>
<MainRoomList isLogin={isLogin} listRoomsType={ListRoomsType.History} />
</RoomList>
);
});
interface MainRoomHistoryPanelProps {
isLogin: boolean;
}

export const MainRoomHistoryPanel = observer<MainRoomHistoryPanelProps>(
function MainRoomHistoryPanel({ isLogin }) {
const t = useTranslate();
const roomStore = useContext(RoomStoreContext);

const onScrollToBottom = useCallback((): void => {
void roomStore.fetchMoreRooms(ListRoomsType.History);
}, [roomStore]);

return (
<RoomList title={t("history")} onScrollToBottom={onScrollToBottom}>
<MainRoomList
isLogin={isLogin}
listRoomsType={ListRoomsType.History}
roomStore={roomStore}
/>
</RoomList>
);
},
);

export default MainRoomHistoryPanel;
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<MainRoomListProps>(function MainRoomList({
roomStore,
listRoomsType,
isLogin,
}) {
const t = useTranslate();
const roomStore = useContext(RoomStoreContext);
const [skeletonsVisible, setSkeletonsVisible] = useState(false);
const [roomUUIDs, setRoomUUIDs] = useState<string[]>();
const [cancelModalVisible, setCancelModalVisible] = useState(false);
const [stopModalVisible, setStopModalVisible] = useState(false);
const [inviteModalVisible, setInviteModalVisible] = useState(false);
Expand All @@ -57,10 +57,8 @@ export const MainRoomList = observer<MainRoomListProps>(function MainRoomList({
const refreshRooms = useCallback(
async function refreshRooms(): Promise<void> {
try {
const roomUUIDs = await sp(roomStore.listRooms(listRoomsType, { page: 1 }));
setRoomUUIDs(roomUUIDs);
await sp(roomStore.listRooms(listRoomsType));
} catch (e) {
setRoomUUIDs([]);
errorTips(e);
}
},
Expand All @@ -81,6 +79,8 @@ export const MainRoomList = observer<MainRoomListProps>(function MainRoomList({
};
}, [refreshRooms, isLogin]);

const roomUUIDs = roomStore.roomUUIDs[listRoomsType];

if (!roomUUIDs) {
return skeletonsVisible ? <RoomListSkeletons /> : null;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/flat-pages/src/HomePage/MainRoomListPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Array<{ key: "all" | "today" | "periodic"; title: string }>>(
() => [
Expand All @@ -35,7 +37,11 @@ export const MainRoomListPanel = observer<{ isLogin: boolean }>(function MainRoo
title={t("room-list")}
onTabActive={setActiveTab}
>
<MainRoomList isLogin={isLogin} listRoomsType={activeTab as ListRoomsType} />
<MainRoomList
isLogin={isLogin}
listRoomsType={activeTab as ListRoomsType}
roomStore={roomStore}
/>
</RoomList>
);
});
Expand Down
88 changes: 85 additions & 3 deletions packages/flat-stores/src/room-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
joinRoom,
JoinRoomResult,
listRooms,
ListRoomsPayload,
ListRoomsType,
ordinaryRoomInfo,
periodicRoomInfo,
Expand All @@ -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;
Expand Down Expand Up @@ -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<string, RoomItem>();
public periodicRooms = observable.map<string, PeriodicRoomItem>();

/** If `fetchMoreRooms()` returns 0 rooms, stop fetching it */
public hasMoreRooms: Record<ListRoomsType, boolean> = {
all: true,
periodic: true,
history: true,
today: true,
};

/** Don't invoke `fetchMoreRooms()` too many times */
public isFetchingRooms = false;

public get roomUUIDs(): Record<ListRoomsType, string[]> {
const roomUUIDs: Record<ListRoomsType, string[]> = {
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);
}
Expand Down Expand Up @@ -111,8 +150,8 @@ export class RoomStore {
/**
* @returns a list of room uuids
*/
public async listRooms(type: ListRoomsType, payload: ListRoomsPayload): Promise<string[]> {
const rooms = await listRooms(type, payload);
public async listRooms(type: ListRoomsType): Promise<string[]> {
const rooms = await listRooms(type, { page: 1 });
const roomUUIDs: string[] = [];
runInAction(() => {
for (const room of rooms) {
Expand All @@ -126,6 +165,45 @@ export class RoomStore {
return roomUUIDs;
}

public async fetchMoreRooms(type: ListRoomsType): Promise<void> {
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<void> {
await cancelRoom(payload);
}
Expand Down Expand Up @@ -218,3 +296,7 @@ export class RoomStore {
}

export const roomStore = new RoomStore();

if (process.env.DEV) {
(window as any).roomStore = roomStore;
}