refactor: prefetch basic data and window visibility change event in streaming
This commit is contained in:
@@ -217,6 +217,7 @@ export const {
|
||||
useLazyGuestLoginQuery,
|
||||
useGuestLoginQuery,
|
||||
useLazyCheckEmailQuery,
|
||||
useLazyGetInitializedQuery,
|
||||
useGetInitializedQuery,
|
||||
useSendLoginMagicLinkMutation,
|
||||
useSendRegMagicLinkMutation,
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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>>) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user