refactor: prefetch basic data and window visibility change event in streaming

This commit is contained in:
Tristan Yang
2023-08-23 23:06:12 +08:00
parent 1ae9b0e55f
commit 488b9b301c
12 changed files with 452 additions and 285 deletions
+1
View File
@@ -217,6 +217,7 @@ export const {
useLazyGuestLoginQuery,
useGuestLoginQuery,
useLazyCheckEmailQuery,
useLazyGetInitializedQuery,
useGetInitializedQuery,
useSendLoginMagicLinkMutation,
useSendRegMagicLinkMutation,
+11 -1
View File
@@ -237,7 +237,17 @@ export const serverApi = createApi({
})
}),
getLoginConfig: builder.query<LoginConfig, void>({
query: () => ({ url: `/admin/login/config` })
query: () => ({ url: `/admin/login/config` }),
async onQueryStarted(data, { dispatch, queryFulfilled }) {
try {
const resp = await queryFulfilled;
if (resp.data) {
dispatch(updateInfo({ loginConfig: resp.data }));
}
} catch {
console.error("get login config error");
}
}
}),
getFiles: builder.query<VoceChatFile[], GetFilesDTO>({
query: (params) => ({
+8 -4
View File
@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Server } from "@/types/server";
import { LoginConfig, Server } from "@/types/server";
export interface StoredServer extends Server {
version: string;
@@ -9,6 +9,7 @@ export interface StoredServer extends Server {
link: string;
expire: number;
};
loginConfig: LoginConfig | null;
}
const initialState: StoredServer = {
version: "",
@@ -23,7 +24,8 @@ const initialState: StoredServer = {
show_user_online_status: false,
webclient_auto_update: true,
contact_verification_enable: false,
chat_layout_mode: "Left"
chat_layout_mode: "Left",
loginConfig: null
};
const serverSlice = createSlice({
@@ -47,7 +49,8 @@ const serverSlice = createSlice({
show_user_online_status = true,
webclient_auto_update = true,
contact_verification_enable = false,
chat_layout_mode = "Left"
chat_layout_mode = "Left",
loginConfig = state.loginConfig || null
} = action.payload || {};
return {
version,
@@ -59,7 +62,8 @@ const serverSlice = createSlice({
show_user_online_status,
webclient_auto_update,
contact_verification_enable,
chat_layout_mode
chat_layout_mode,
loginConfig
};
},
updateInfo(state, action: PayloadAction<Partial<StoredServer>>) {
+9 -2
View File
@@ -3,7 +3,9 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { UploadFileData } from "@/hooks/useUploadFile";
export type ListView = "item" | "grid";
export interface State {
export type SSEStatus = "connecting" | "connected" | "disconnected";
export interface UIState {
SSEStatus: SSEStatus;
online: boolean;
ready: boolean;
inputMode: "text";
@@ -22,7 +24,8 @@ export interface State {
};
}
const initialState: State = {
const initialState: UIState = {
SSEStatus: "disconnected",
online: true,
ready: false,
inputMode: "text",
@@ -49,6 +52,9 @@ const uiSlice = createSlice({
setReady(state) {
state.ready = true;
},
updateSSEStatus(state, action: PayloadAction<SSEStatus>) {
state.SSEStatus = action.payload;
},
updateOnline(state, action: PayloadAction<boolean>) {
state.online = action.payload;
},
@@ -171,6 +177,7 @@ const uiSlice = createSlice({
});
export const {
updateSSEStatus,
fillUI,
setReady,
updateOnline,
+12 -12
View File
@@ -2,8 +2,6 @@ import { FC, ReactElement } from "react";
import { matchRoutes, Navigate, useLocation } from "react-router-dom";
import { GuestRoutes, KEY_LOCAL_TRY_PATH } from "@/app/config";
import { useGetInitializedQuery } from "@/app/services/auth";
import { useGetLoginConfigQuery } from "@/app/services/server";
import { useAppSelector } from "@/app/store";
import Loading from "./Loading";
@@ -18,16 +16,18 @@ const RequireAuth: FC<Props> = ({ children, redirectTo = "/login" }) => {
const location = useLocation();
const matches = matchRoutes(GuestAllows, location);
const allowGuest = matches ? !!matches[0].pathname : false;
const { data: loginConfig, isLoading: loginConfigLoading } = useGetLoginConfigQuery(undefined, {
refetchOnMountOrArgChange: true
const {
authData: { token, guest, initialized },
loginConfig
} = useAppSelector((store) => {
return {
authData: store.authData,
loginConfig: store.server.loginConfig
};
});
const { isLoading: initLoading } = useGetInitializedQuery(undefined, {
refetchOnMountOrArgChange: true
});
const { token, guest, initialized } = useAppSelector((store) => store.authData);
console.info("uninitialized", loginConfigLoading, initLoading);
// 初始化和login配置检查
if (loginConfigLoading || initLoading) return <Loading fullscreen={true} context="auth-route" />;
console.info("check basic info", loginConfig);
// 初始化login配置检查
if (!loginConfig) return <Loading fullscreen={true} context="auth-route" />;
// 未初始化 则先走setup 流程
if (!initialized) return <Navigate to={`/onboarding`} replace />;
// 开启guest 并且没token 而且是允许guest访问的路由 则先去过渡页登录
@@ -38,7 +38,7 @@ const RequireAuth: FC<Props> = ({ children, redirectTo = "/login" }) => {
KEY_LOCAL_TRY_PATH,
ignorePath == location.pathname ? "/" : `${location.pathname}${location.search}`
);
const guestLogin = loginConfig?.guest && allowGuest;
const guestLogin = loginConfig.guest && allowGuest;
return guestLogin ? (
<Navigate to={"/guest_login"} replace />
) : (
+23
View File
@@ -0,0 +1,23 @@
import { useAppSelector } from "@/app/store";
import clsx from "clsx";
// import React from "react";
type Props = {};
const StreamStatus = (props: Props) => {
const status = useAppSelector((store) => store.ui.SSEStatus);
return (
<aside className="fixed right-2 bottom-2">
<div
className={clsx(
"w-1 h-1 rounded-full",
status === "connected" && "bg-green-500",
status === "disconnected" && "bg-red-500",
status === "connecting" && "bg-yellow-500"
)}
></div>
</aside>
);
};
export default StreamStatus;
+20
View File
@@ -0,0 +1,20 @@
// import React from 'react'
import { useLazyGetInitializedQuery } from "@/app/services/auth";
import { useLazyGetLoginConfigQuery } from "@/app/services/server";
import { useEffect } from "react";
// type Props = {}
const usePrefetchData = () => {
const [loadLoginConfig] = useLazyGetLoginConfigQuery();
const [loadOnboardingSetting] = useLazyGetInitializedQuery();
useEffect(() => {
loadLoginConfig();
loadOnboardingSetting();
}, []);
return null;
};
export default usePrefetchData;
+32 -4
View File
@@ -33,7 +33,7 @@ import { resetFileMessage } from "@/app/slices/message.file";
import { resetReactionMessage } from "@/app/slices/message.reaction";
import { removeUserSession, resetUserMsg } from "@/app/slices/message.user";
import { updateInfo } from "@/app/slices/server";
import { setReady } from "@/app/slices/ui";
import { setReady, updateSSEStatus } from "@/app/slices/ui";
import { updateContactStatus, updateUsersByLogs, updateUsersStatus } from "@/app/slices/users";
import { updateCallInfo } from "@/app/slices/voice";
import { useAppDispatch, useAppSelector } from "@/app/store";
@@ -55,7 +55,7 @@ const getQueryString = (params: { [key: string]: string }) => {
});
return sp.toString();
};
let hiddenTime: number = 0;
let SSE: EventSource | undefined;
let ready = false; //是否已完成初始推送
let aliveInter: number | ReturnType<typeof setTimeout> = 0;
@@ -102,6 +102,7 @@ export default function useStreaming() {
if (dayjs().isAfter(new Date(expireTime - 20 * 1000))) {
const resp = await renewToken({ token, refresh_token: refreshToken });
if ("error" in resp) {
console.error("renew error from sse", resp.error);
// 还有网,而且在当前页,则停止循环
if (navigator.onLine || !document.hidden) {
stopStreaming();
@@ -131,14 +132,16 @@ export default function useStreaming() {
params.users_version = `${usersVersion}`;
}
// 开始初始化推送
dispatch(updateSSEStatus("connecting"));
SSE = new EventSource(`${BASE_URL}/user/events?${getQueryString(params)}`);
SSE.onopen = () => {
dispatch(updateSSEStatus("connected"));
ready = false;
};
SSE.onerror = (err) => {
// 断网了
if (!navigator.onLine) {
// 断网了 或者 页面隐藏了
if (!navigator.onLine || document.hidden) {
stopStreaming();
return;
}
@@ -419,6 +422,7 @@ export default function useStreaming() {
if (SSE) {
SSE.close();
SSE = undefined;
dispatch(updateSSEStatus("disconnected"));
}
ready = false;
};
@@ -431,11 +435,35 @@ export default function useStreaming() {
stopStreaming();
}
};
const handleWindowVisibilityChange = () => {
console.info("debug SSE: visibility changed", document.hidden);
if (document.hidden) {
// 记录隐藏时间
hiddenTime = new Date().getTime();
} else {
const elapsedTime = (new Date().getTime() - hiddenTime) / 1000;
const canReconnect = elapsedTime > 30 * 60 || !SSE;
// 超过半小时或者已断线,强制重连
if (canReconnect) {
if (SSE) {
// 先停掉
stopStreaming();
setTimeout(() => {
startStreaming();
}, 1500);
}
// 直接重连
startStreaming();
}
}
};
document.addEventListener("visibilitychange", handleWindowVisibilityChange);
window.addEventListener("online", handleNetworkChange);
window.addEventListener("offline", handleNetworkChange);
return () => {
window.removeEventListener("online", handleNetworkChange);
window.removeEventListener("offline", handleNetworkChange);
document.removeEventListener("visibilitychange", handleWindowVisibilityChange);
};
}, []);
+2
View File
@@ -20,6 +20,7 @@ import UserIcon from "@/assets/icons/user.svg";
import Menu from "./Menu";
import MobileNavs from "./MobileNavs";
import User from "./User";
import StreamStatus from "@/components/StreamStatus";
function HomePage() {
const { t } = useTranslation();
@@ -65,6 +66,7 @@ function HomePage() {
const linkClass = `flex items-center gap-2.5 px-3 py-2 font-semibold text-sm text-gray-600 rounded-lg md:hover:bg-gray-800/10`;
return (
<>
<StreamStatus />
{roleChanged && <ReLoginModal />}
{!guest && <UnreadTabTip />}
{!guest && <Voice />}
+3 -1
View File
@@ -17,6 +17,7 @@ import NotFoundPage from "./404";
import InvitePrivate from "./invitePrivate";
import LazyIt from "./lazy";
import InviteInMobile from "./reg/InviteInMobile";
import usePrefetchData from "@/hooks/usePrefetchData";
const RegBasePage = lazy(() => import("./reg"));
const RegWithUsernamePage = lazy(() => import("./reg/RegWithUsername"));
@@ -47,7 +48,8 @@ const PageRoutes = () => {
}, isEqual);
// 提前获取device token
useDeviceToken(vapidKey);
// 初始化元信息
usePrefetchData();
// 掉线检测
useEffect(() => {
if (!online) {