refactor: 升级依赖: react18与react-router v6 以及其他

pull/49/head
moonrailgun 3 years ago
parent da521fc638
commit 8f6adb13d4

@ -47,7 +47,7 @@
"p-series": "2.1.0",
"plop": "^3.0.5",
"pretty-ms": "7.0.1",
"react": "17.0.2",
"react": "18.2.0",
"tailchat-server-sdk": "^0.0.12",
"tailchat-shared": "workspace:*",
"yargs": "^17.4.0"
@ -57,7 +57,7 @@
"@types/inquirer": "^8.2.1",
"@types/lodash": "^4.14.170",
"@types/node": "16.11.7",
"@types/react": "^17.0.39",
"@types/react": "18.0.20",
"@types/yargs": "^17.0.10",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"

@ -31,12 +31,13 @@ export const App: React.FC = React.memo(() => {
<Box>
<Tabs flexDirection="column" onChange={() => {}}>
<Tab name="tab1">
{/* 因为react版本问题暂时注释 */}
{/* <Tab name="tab1">
<Text>Foo</Text>
</Tab>
<Tab name="tab2">
<Text>Bar</Text>
</Tab>
</Tab> */}
</Tabs>
</Box>
</Box>

@ -1,6 +1,12 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, {
PropsWithChildren,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
interface AutoFolderProps {
interface AutoFolderProps extends PropsWithChildren {
maxHeight: number;
showFullText?: React.ReactNode;
backgroundColor?: string;

@ -1,7 +1,7 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
import styles from './index.module.less';
export const Highlight: React.FC = React.memo((props) => {
export const Highlight: React.FC<PropsWithChildren> = React.memo((props) => {
return <span className={styles.highLight}>{props.children}</span>;
});
Highlight.displayName = 'Highlight';

@ -39,16 +39,16 @@
"@storybook/react": "^6.4.22",
"@storybook/testing-library": "^0.0.11",
"@types/lodash": "^4.14.170",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.8",
"@types/react": "18.0.20",
"@types/react-dom": "18.0.6",
"babel-loader": "^8.2.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "^4.5.2",
"webpack": "^5.72.0"
},
"peerDependencies": {
"react": "17.0.2",
"react-dom": "17.0.2"
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

@ -1,4 +1,4 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { isDevelopment } from '../utils/environment';
@ -7,19 +7,21 @@ import { queryClient } from './';
/**
*
*/
export const CacheProvider: React.FC = React.memo((props) => {
return (
<QueryClientProvider client={queryClient}>
{props.children}
export const CacheProvider: React.FC<PropsWithChildren> = React.memo(
(props) => {
return (
<QueryClientProvider client={queryClient}>
{props.children}
{/* TODO: 待放到web上 */}
{isDevelopment && (
<ReactQueryDevtools
position="bottom-left"
toggleButtonProps={{ style: { left: 8, bottom: 50 } }}
/>
)}
</QueryClientProvider>
);
});
{/* TODO: 待放到web上 */}
{isDevelopment && (
<ReactQueryDevtools
position="bottom-left"
toggleButtonProps={{ style: { left: 8, bottom: 50 } }}
/>
)}
</QueryClientProvider>
);
}
);
CacheProvider.displayName = 'CacheProvider';

@ -1,4 +1,10 @@
import React, { useCallback, useRef, Fragment, useContext } from 'react';
import React, {
useCallback,
useRef,
Fragment,
useContext,
PropsWithChildren,
} from 'react';
import { useEffect } from 'react';
import { PortalManager } from './Manager';
import { createPortalContext } from './context';
@ -64,7 +70,7 @@ export function buildPortal(options: BuildPortalOptions) {
const PortalContext = createPortalContext(hostName);
const PortalHost = React.memo((props) => {
const PortalHost: React.FC<PropsWithChildren> = React.memo((props) => {
const managerRef = useRef<PortalManager>();
const nextKeyRef = useRef<number>(0);
const queueRef = useRef<Operation[]>([]);
@ -180,7 +186,7 @@ export function buildPortal(options: BuildPortalOptions) {
});
PortalHost.displayName = 'PortalHost-' + hostName;
const PortalRender = React.memo((props) => {
const PortalRender: React.FC<PropsWithChildren> = React.memo((props) => {
const manager = useContext(PortalContext);
if (_isNil(manager)) {

@ -1,8 +1,8 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { CacheProvider } from '../cache/Provider';
import { ColorSchemeContextProvider } from '../contexts/ColorSchemeContext';
export const TcProvider: React.FC = React.memo((props) => {
export const TcProvider: React.FC<PropsWithChildren> = React.memo((props) => {
return (
<CacheProvider>
<ColorSchemeContextProvider>{props.children}</ColorSchemeContextProvider>

@ -1,4 +1,9 @@
import React, { useCallback, useContext, useState } from 'react';
import React, {
PropsWithChildren,
useCallback,
useContext,
useState,
} from 'react';
import _noop from 'lodash/noop';
import type { ReplyMsgType } from '../utils/message-helper';
@ -16,20 +21,22 @@ const ChatBoxContext = React.createContext<ChatBoxContextProps>({
});
ChatBoxContext.displayName = 'ChatBoxContext';
export const ChatBoxContextProvider: React.FC = React.memo((props) => {
const [replyMsg, setReplyMsg] = useState<ReplyMsgType | null>(null);
export const ChatBoxContextProvider: React.FC<PropsWithChildren> = React.memo(
(props) => {
const [replyMsg, setReplyMsg] = useState<ReplyMsgType | null>(null);
return (
<ChatBoxContext.Provider
value={{
replyMsg,
setReplyMsg,
}}
>
{props.children}
</ChatBoxContext.Provider>
);
});
return (
<ChatBoxContext.Provider
value={{
replyMsg,
setReplyMsg,
}}
>
{props.children}
</ChatBoxContext.Provider>
);
}
);
ChatBoxContextProvider.displayName = 'ChatBoxContextProvider';
export function useChatBoxContext(): ChatBoxContextProps & {

@ -1,4 +1,4 @@
import React, { useContext, useEffect } from 'react';
import React, { PropsWithChildren, useContext, useEffect } from 'react';
import { parseColorScheme } from '../utils/color-scheme-helper';
import { sharedEvent } from '../event';
import { useStorage } from '../manager/storage';
@ -15,22 +15,23 @@ const ColorSchemeContext = React.createContext<{
});
ColorSchemeContext.displayName = 'ColorSchemeContext';
export const ColorSchemeContextProvider: React.FC = React.memo((props) => {
const [colorScheme = 'dark', { save: setColorScheme }] = useStorage(
'colorScheme',
'dark'
);
export const ColorSchemeContextProvider: React.FC<PropsWithChildren> =
React.memo((props) => {
const [colorScheme = 'dark', { save: setColorScheme }] = useStorage(
'colorScheme',
'dark'
);
useEffect(() => {
sharedEvent.emit('loadColorScheme', colorScheme);
}, [colorScheme]);
useEffect(() => {
sharedEvent.emit('loadColorScheme', colorScheme);
}, [colorScheme]);
return (
<ColorSchemeContext.Provider value={{ colorScheme, setColorScheme }}>
{props.children}
</ColorSchemeContext.Provider>
);
});
return (
<ColorSchemeContext.Provider value={{ colorScheme, setColorScheme }}>
{props.children}
</ColorSchemeContext.Provider>
);
});
ColorSchemeContextProvider.displayName = 'ColorSchemeContextProvider';
export function useColorScheme() {

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { PropsWithChildren, useContext } from 'react';
import type { GroupInfo } from '..';
/**
@ -13,9 +13,11 @@ const GroupInfoContext = React.createContext<GroupInfoContextProps>({
});
GroupInfoContext.displayName = 'GroupInfoContext';
export const GroupInfoContextProvider: React.FC<{
groupInfo: GroupInfo;
}> = React.memo((props) => {
export const GroupInfoContextProvider: React.FC<
PropsWithChildren<{
groupInfo: GroupInfo;
}>
> = React.memo((props) => {
return (
<GroupInfoContext.Provider
value={{

@ -1,4 +1,9 @@
import i18next, { TFunction, TOptionsBase } from 'i18next';
import i18next, {
StringMap,
TFunctionKeys,
TOptions,
TOptionsBase,
} from 'i18next';
import {
useTranslation as useI18NTranslation,
initReactI18next,
@ -36,6 +41,32 @@ i18next
},
} as any);
type TFunctionResult = string | React.ReactNode;
// Fork from i18next
interface TFunction {
// basic usage
<TKeys extends TFunctionKeys = string>(key: TKeys): string;
<
TResult extends TFunctionResult = string,
TKeys extends TFunctionKeys = string,
TInterpolationMap extends object = StringMap
>(
key: TKeys,
options?: TOptions<TInterpolationMap> | string
): TResult;
// overloaded usage
<
TResult extends TFunctionResult = string,
TKeys extends TFunctionKeys = string,
TInterpolationMap extends object = StringMap
>(
key: TKeys,
defaultValue?: string,
options?: TOptions<TInterpolationMap> | string
): TResult;
}
/**
*
*/

@ -18,7 +18,10 @@ async function getLanguage(): Promise<string> {
* hook
*/
export function useLanguage() {
const [language, { save }] = useStorage(LANGUAGE_KEY, defaultLanguage);
const [language, { save }] = useStorage<AllowedLanguage>(
LANGUAGE_KEY,
defaultLanguage
);
const originLanguageRef = useRef<string>();

@ -43,6 +43,7 @@ export {
getLanguage,
useTranslation,
} from './i18n';
export type { AllowedLanguage } from './i18n';
export { Trans } from './i18n/Trans';
export { useLanguage } from './i18n/language';

@ -69,7 +69,7 @@ export interface GroupBasicInfo {
name: string;
avatar?: string;
owner: string;
memberCount: GroupMember[];
memberCount: number;
}
export interface GroupInvite {

@ -17,12 +17,12 @@
"filesize": "^8.0.7",
"flatted": "^3.2.4",
"formik": "^2.2.9",
"i18next": "^20.3.2",
"i18next-http-backend": "^1.2.6",
"i18next": "^21.9.2",
"i18next-http-backend": "^1.4.1",
"lodash": "^4.17.21",
"react-i18next": "^11.15.1",
"react-i18next": "^11.18.6",
"react-native-storage": "npm:@trpgengine/react-native-storage@^1.0.1",
"react-redux": "^7.2.6",
"react-redux": "^8.0.2",
"regenerator-runtime": "^0.13.9",
"socket.io-client": "^4.1.2",
"url-regex": "^5.0.0",
@ -31,12 +31,11 @@
"devDependencies": {
"@types/crc": "^3.4.0",
"@types/lodash": "^4.14.170",
"@types/react": "^17.0.39",
"@types/react-redux": "^7.1.24",
"react": "17.0.2"
"@types/react": "18.0.20",
"react": "18.2.0"
},
"peerDependencies": {
"react": "17.0.2",
"react-dom": "17.0.2"
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

@ -42,14 +42,14 @@
"p-min-delay": "^4.0.0",
"qs": "^6.10.3",
"rc-tree": "^5.3.6",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-easy-crop": "^3.5.2",
"react-helmet": "^6.1.0",
"react-markdown": "6",
"react-mentions": "^4.3.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router": "^6.4.0",
"react-router-dom": "^6.4.0",
"react-split": "^2.0.14",
"react-transition-group": "^4.4.2",
"react-use-gesture": "^9.1.3",
@ -72,19 +72,19 @@
"@types/emoji-mart": "^3.0.8",
"@types/fs-extra": "^9.0.13",
"@types/is-hotkey": "^0.1.5",
"@types/jest": "^27.0.3",
"@types/jest": "^29.0.3",
"@types/loadable__component": "^5.13.4",
"@types/lodash": "^4.14.170",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/minimatch": "^3.0.5",
"@types/node": "^15.12.5",
"@types/qs": "^6.9.7",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.8",
"@types/react": "18.0.20",
"@types/react-dom": "18.0.6",
"@types/react-helmet": "^6.1.2",
"@types/react-mentions": "^4.1.5",
"@types/react-router": "^5.1.15",
"@types/react-router-dom": "^5.1.7",
"@types/react-router": "^5.1.19",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.2",
"@types/react-virtualized": "^9.21.14",
"@types/react-virtualized-auto-sizer": "^1.0.1",

@ -11,7 +11,7 @@
},
"devDependencies": {
"@types/react-highlight": "^0.12.5",
"react": "17.0.2",
"react-dom": "17.0.2"
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

@ -8,6 +8,6 @@
},
"devDependencies": {
"@types/react-canvas-draw": "^1.1.1",
"react": "17.0.2"
"react": "18.2.0"
}
}

@ -8,6 +8,6 @@
"html-react-parser": "^1.4.5"
},
"devDependencies": {
"react": "17.0.2"
"react": "18.2.0"
}
}

@ -2,7 +2,7 @@ import {
postRequest,
appendUrlSearch,
useAsyncRefresh,
useHistory,
useLocation,
urlSearchParse,
isValidStr,
} from '@capital/common';
@ -24,11 +24,11 @@ export function useOpenAppList() {
return data ?? [];
}, []);
const history = useHistory();
const location = useLocation();
useEffect(() => {
// 仅初始化的时候才处理
const { appId } = urlSearchParse(history.location.search, {
const { appId } = urlSearchParse(location.search, {
ignoreQueryPrefix: true,
});

@ -1,10 +1,10 @@
import React, { Suspense } from 'react';
import React, { PropsWithChildren, Suspense } from 'react';
import {
BrowserRouter,
HashRouter,
Redirect,
Navigate,
Route,
Switch,
Routes,
} from 'react-router-dom';
import { TcProvider, useColorScheme, useLanguage } from 'tailchat-shared';
import clsx from 'clsx';
@ -28,7 +28,7 @@ const PanelRoute = Loadable(() => import('./routes/Panel'));
const InviteRoute = Loadable(() => import('./routes/Invite'));
const AppProvider: React.FC = React.memo((props) => {
const AppProvider: React.FC<PropsWithChildren> = React.memo((props) => {
return (
<Suspense fallback={<LoadingSpinner />}>
<AppRouter>
@ -43,7 +43,7 @@ const AppProvider: React.FC = React.memo((props) => {
});
AppProvider.displayName = 'AppProvider';
const AppContainer: React.FC = React.memo((props) => {
const AppContainer: React.FC<PropsWithChildren> = React.memo((props) => {
const { isDarkMode, extraSchemeName } = useColorScheme();
return (
@ -83,29 +83,36 @@ export const App: React.FC = React.memo(() => {
<AppProvider>
<AppHeader />
<AppContainer>
<Switch>
<Route path="/entry">
<FallbackPortalHost>
<EntryRoute />
</FallbackPortalHost>
</Route>
<Route path="/main" component={MainRoute} />
<Route path="/panel" component={PanelRoute} />
<Route path="/invite/:inviteCode" component={InviteRoute} />
<Route path="/plugin/*">
<FallbackPortalHost>
{/* NOTICE: Switch里不能出现动态路由 */}
{pluginRootRoute.map((r, i) => (
<Route
key={r.name}
path={r.path ? `/plugin${r.path}` : `/plugin/fallback${i}`}
component={r.component}
/>
))}
</FallbackPortalHost>
</Route>
<Redirect to="/entry" />
</Switch>
<Routes>
<Route
path="/entry/*"
element={
<FallbackPortalHost>
<EntryRoute />
</FallbackPortalHost>
}
/>
<Route path="/main/*" element={<MainRoute />} />
<Route path="/panel/*" element={<PanelRoute />} />
<Route path="/invite/:inviteCode" element={<InviteRoute />} />
<Route
path="/plugin/*"
element={
<FallbackPortalHost>
{pluginRootRoute.map((r, i) => (
// NOTICE: Switch里不能出现动态路由
<Route
key={r.name}
path={r.path ? `/plugin${r.path}` : `/plugin/fallback${i}`}
element={React.createElement(r.component)}
/>
))}
</FallbackPortalHost>
}
/>
<Route path="/" element={<Navigate to="/entry" replace={true} />} />
</Routes>
</AppContainer>
</AppProvider>
);

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import React, { PropsWithChildren, useRef, useState } from 'react';
import { closeModal, openModal } from './Modal';
import { showToasts, t } from 'tailchat-shared';
import { Avatar } from 'antd';
@ -6,7 +6,7 @@ import { Icon } from '@/components/Icon';
import { ModalAvatarCropper } from './modals/AvatarCropper';
import { isGIF } from '@/utils/file-helper';
interface AvatarPickerProps {
interface AvatarPickerProps extends PropsWithChildren {
className?: string;
imageUrl?: string; // 初始image url, 仅children为空时生效
onChange?: (blobUrl: string) => void;

@ -1,15 +1,17 @@
import { blobUrlToFile } from '@/utils/file-helper';
import { Icon } from '@/components/Icon';
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { PropsWithChildren, useState } from 'react';
import { uploadFile, UploadFileResult, useAsyncRequest } from 'tailchat-shared';
import { AvatarPicker } from './AvatarPicker';
export const AvatarUploader: React.FC<{
circle?: boolean;
className?: string;
onUploadSuccess: (fileInfo: UploadFileResult) => void;
}> = React.memo((props) => {
export const AvatarUploader: React.FC<
PropsWithChildren<{
circle?: boolean;
className?: string;
onUploadSuccess: (fileInfo: UploadFileResult) => void;
}>
> = React.memo((props) => {
const [uploadProgress, setUploadProgress] = useState(0); // 0 - 100
const [{ loading }, handlePickImage] = useAsyncRequest(
async (blobUrl: string) => {

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { PropsWithChildren, useContext } from 'react';
import type { SuggestionDataItem } from 'react-mentions';
import { useShallowObject } from 'tailchat-shared';
@ -22,7 +22,7 @@ export function useChatInputActionContext() {
/**
* Input Mentions
*/
interface ChatInputMentionsContextProps {
interface ChatInputMentionsContextProps extends PropsWithChildren {
users: SuggestionDataItem[];
placeholder?: string;
disabled?: boolean;

@ -1,6 +1,6 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
interface CommonSidebarProps {
interface CommonSidebarProps extends PropsWithChildren {
['data-tc-role']?: string;
}
export const CommonSidebarWrapper: React.FC<CommonSidebarProps> = React.memo(

@ -1,11 +1,11 @@
import React, { Fragment } from 'react';
import React, { Fragment, PropsWithChildren } from 'react';
import { isDevelopment } from 'tailchat-shared';
/**
*
*
*/
export const DevContainer: React.FC = React.memo((props) => {
export const DevContainer: React.FC<PropsWithChildren> = React.memo((props) => {
return isDevelopment ? <Fragment>{props.children}</Fragment> : null;
});
DevContainer.displayName = 'DevContainer';

@ -1,4 +1,4 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { t } from 'tailchat-shared';
import { Problem } from './Problem';
@ -8,7 +8,7 @@ interface ErrorBoundaryProps {
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
PropsWithChildren<ErrorBoundaryProps>,
{
error?: Error | null;
info: {

@ -1,4 +1,4 @@
import React, { useCallback, useRef } from 'react';
import React, { PropsWithChildren, useCallback, useRef } from 'react';
import _isFunction from 'lodash/isFunction';
import _isNil from 'lodash/isNil';
@ -6,7 +6,7 @@ import _isNil from 'lodash/isNil';
*
*/
interface FileSelectorProps {
interface FileSelectorProps extends PropsWithChildren {
fileProps?: React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement

@ -1,6 +1,6 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
interface FullModalCommonTitleProps {
interface FullModalCommonTitleProps extends PropsWithChildren {
extra?: React.ReactNode;
}
export const FullModalCommonTitle: React.FC<FullModalCommonTitleProps> =

@ -1,12 +1,17 @@
import React, { useCallback, useEffect, useRef } from 'react';
import React, {
PropsWithChildren,
useCallback,
useEffect,
useRef,
} from 'react';
import _isFunction from 'lodash/isFunction';
import { Icon } from '@/components/Icon';
import { Icon } from 'tailchat-design';
import clsx from 'clsx';
/**
*
*/
interface FullModalProps {
interface FullModalProps extends PropsWithChildren {
visible?: boolean;
onChangeVisible?: (visible: boolean) => void;
}

@ -1,10 +1,12 @@
import { Icon } from '@/components/Icon';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { useReducer } from 'react';
export const GroupSection: React.FC<{
header: string;
}> = React.memo((props) => {
export const GroupSection: React.FC<
PropsWithChildren<{
header: string;
}>
> = React.memo((props) => {
const [isShow, switchShow] = useReducer((v) => !v, true);
return (

@ -16,11 +16,11 @@ export const InviteCodeExpiredAt: React.FC<InviteCodeExpiredAtProps> =
const { invite } = props;
if (!invite.expiredAt) {
return t('该邀请码永不过期');
return <span>{t('该邀请码永不过期')}</span>;
}
if (new Date(invite.expiredAt).valueOf() < Date.now()) {
return t('该邀请码已过期');
return <span>{t('该邀请码已过期')}</span>;
}
return (
@ -28,7 +28,7 @@ export const InviteCodeExpiredAt: React.FC<InviteCodeExpiredAtProps> =
{' '}
<Tooltip title={formatFullTime(invite.expiredAt)}>
<span className="font-bold">
{{ date: datetimeFromNow(invite.expiredAt) }}
{{ date: datetimeFromNow(invite.expiredAt) } as any}
</span>
</Tooltip>{' '}

@ -1,6 +1,7 @@
import { Select, SelectProps } from 'antd';
import React, { useCallback } from 'react';
import { showToasts, t, useLanguage } from 'tailchat-shared';
import type { AllowedLanguage } from 'tailchat-shared';
type LanguageSelectProps = Omit<SelectProps, 'value' | 'onChange'>;
@ -12,7 +13,7 @@ export const LanguageSelect: React.FC<LanguageSelectProps> = React.memo(
const { language, setLanguage } = useLanguage();
const handleChangeLanguage = useCallback(
(newLang: string) => {
(newLang: AllowedLanguage) => {
showToasts(t('刷新页面后生效'), 'info');
setLanguage(newLang);
},

@ -6,6 +6,7 @@ export interface LoadingProps {
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
export const Loading: React.FC<LoadingProps> = React.memo((props) => {
const { spinning = false, className, style } = props;

@ -5,6 +5,7 @@ interface LoadingOnFirstProps extends LoadingProps {
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
/**
* <Loading />

@ -1,4 +1,9 @@
import React, { useCallback, useContext, useState } from 'react';
import React, {
PropsWithChildren,
useCallback,
useContext,
useState,
} from 'react';
import _isFunction from 'lodash/isFunction';
import _isNil from 'lodash/isNil';
import _last from 'lodash/last';
@ -30,7 +35,7 @@ const ModalContext = React.createContext<{
closeModal: _noop,
});
interface ModalProps {
interface ModalProps extends PropsWithChildren {
visible?: boolean;
onChangeVisible?: (visible: boolean) => void;
@ -241,11 +246,13 @@ export function useModalContext() {
/**
*
*/
export const ModalWrapper: React.FC<{
title?: string;
className?: string;
style?: React.CSSProperties;
}> = React.memo((props) => {
export const ModalWrapper: React.FC<
PropsWithChildren<{
title?: string;
className?: string;
style?: React.CSSProperties;
}>
> = React.memo((props) => {
const isMobile = useIsMobile();
const title = _isString(props.title) ? (

@ -0,0 +1,15 @@
import React from 'react';
import { Empty } from 'antd';
import { t } from 'tailchat-shared';
interface NotFoundProps {
message?: string;
}
/**
*
*/
export const NotFound: React.FC<NotFoundProps> = React.memo((props) => {
return <Empty description={props.message ?? t('未找到')} />;
});
NotFound.displayName = 'NotFound';

@ -1,8 +1,8 @@
import { SectionHeader } from '@/components/SectionHeader';
import { Space } from 'antd';
import React from 'react';
import React, { PropsWithChildren } from 'react';
interface PanelCommonHeaderProps {
interface PanelCommonHeaderProps extends PropsWithChildren {
prefix?: React.ReactNode;
suffix?: React.ReactNode;
actions?: React.ReactNode[];

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { PropsWithChildren, useState } from 'react';
import { PanelCommonHeader } from '../common/Header';
import clsx from 'clsx';
import { useIsMobile } from '@/hooks/useIsMobile';
@ -12,7 +12,7 @@ interface RightPanelType {
/**
*
*/
interface CommonPanelWrapperProps {
interface CommonPanelWrapperProps extends PropsWithChildren {
header: React.ReactNode;
actions?: (
setRightPanel: (info: RightPanelType) => void

@ -29,11 +29,11 @@ function useChatInputInfo(groupId: string) {
if (muteUntil) {
setPlaceholder(
muteUntil
? t('禁言中, 还剩 {{remain}}', {
? (t('禁言中, 还剩 {{remain}}', {
remain: humanizeMsDuration(
new Date().valueOf() - new Date(muteUntil).valueOf()
),
})
}) as string)
: undefined
);
} else {

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { PropsWithChildren, useEffect } from 'react';
import { t, useGroupPanelInfo } from 'tailchat-shared';
import _isNil from 'lodash/isNil';
import { MembersPanel } from './MembersPanel';
@ -31,7 +31,7 @@ function useRecordGroupPanel(groupId: string, panelId: string) {
/**
*
*/
interface GroupPanelWrapperProps {
interface GroupPanelWrapperProps extends PropsWithChildren {
groupId: string;
panelId: string;

@ -1,17 +1,16 @@
import { useHistory } from 'react-router';
import { NavigateOptions, To, useNavigate } from 'react-router';
export interface QuickActionContext {
navigate: (url: string) => void;
navigate: (to: To, options?: NavigateOptions) => void;
}
/**
*
*/
export function useQuickSwitcherActionContext(): QuickActionContext {
const history = useHistory();
const navigate = useNavigate();
return {
navigate: (url) => {
history.push(url);
},
navigate,
};
}

@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { PropsWithChildren, useState } from 'react';
import { Dropdown } from 'antd';
import { Icon } from '@/components/Icon';
import { Icon } from 'tailchat-design';
import clsx from 'clsx';
interface SectionHeaderProps {
interface SectionHeaderProps extends PropsWithChildren {
menu?: React.ReactElement;
'data-testid'?: string;
}

@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, PropsWithChildren } from 'react';
import _get from 'lodash/get';
import { DevContainer } from './DevContainer';
import clsx from 'clsx';
@ -26,11 +26,13 @@ interface SidebarViewLinkType {
isDanger?: boolean;
}
const SidebarViewMenuItemTitle: React.FC<{
active?: boolean;
isDanger?: boolean;
onClick: () => void;
}> = (props) => (
const SidebarViewMenuItemTitle: React.FC<
PropsWithChildren<{
active?: boolean;
isDanger?: boolean;
onClick: () => void;
}>
> = (props) => (
<div
className={clsx(
'rounded-sm px-1.5 py-2.5 mb-1 text-gray-700 dark:text-gray-300 cursor-pointer hover:bg-black hover:bg-opacity-10 hover:text-gray-800 dark:hover:text-gray-200',

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import Split from 'react-split';
import { useStorage } from 'tailchat-shared';
import './SplitPanel.less';
@ -8,7 +8,7 @@ import './SplitPanel.less';
* Reference: https://split.js.org/#/
*/
interface SplitPanelProps {
interface SplitPanelProps extends PropsWithChildren {
className?: string;
}
export const SplitPanel: React.FC<SplitPanelProps> = React.memo((props) => {

@ -14,7 +14,7 @@ export const TcPopover: React.FC<PopoverProps> = React.memo((props) => {
const [visible, setVisible] = useState(false);
const handleVisibleChange = useCallback(
(v) => {
(v: boolean) => {
setVisible(v);
typeof props.onVisibleChange === 'function' && props.onVisibleChange(v);

@ -1,42 +1,43 @@
import { fetchImagePrimaryColor } from '@/utils/image-helper';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { AvatarWithPreview, getTextColorHex } from 'tailchat-design';
import { useAsync, UserBaseInfo } from 'tailchat-shared';
/**
*
*/
export const UserProfileContainer: React.FC<{ userInfo: UserBaseInfo }> =
React.memo((props) => {
const { userInfo } = props;
const { value: bannerColor } = useAsync(async () => {
if (!userInfo.avatar) {
return getTextColorHex(userInfo.nickname);
}
export const UserProfileContainer: React.FC<
PropsWithChildren<{ userInfo: UserBaseInfo }>
> = React.memo((props) => {
const { userInfo } = props;
const { value: bannerColor } = useAsync(async () => {
if (!userInfo.avatar) {
return getTextColorHex(userInfo.nickname);
}
const rgba = await fetchImagePrimaryColor(userInfo.avatar);
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
}, [userInfo.avatar]);
return (
<div className="relative bg-inherit">
<div
style={{
width: '100%',
height: 60,
backgroundColor: bannerColor,
}}
/>
<div className="absolute p-1 rounded-1/2 -mt-11 ml-3 bg-inherit">
<AvatarWithPreview
size={80}
src={userInfo.avatar}
name={userInfo.nickname}
/>
</div>
const rgba = await fetchImagePrimaryColor(userInfo.avatar);
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
}, [userInfo.avatar]);
return (
<div className="relative bg-inherit">
<div
style={{
width: '100%',
height: 60,
backgroundColor: bannerColor,
}}
/>
<div className="p-2 mt-10">{props.children}</div>
<div className="absolute p-1 rounded-1/2 -mt-11 ml-3 bg-inherit">
<AvatarWithPreview
size={80}
src={userInfo.avatar}
name={userInfo.nickname}
/>
</div>
);
});
<div className="p-2 mt-10">{props.children}</div>
</div>
);
});
UserProfileContainer.displayName = 'UserProfileContainer';

@ -1,6 +1,6 @@
import { Button } from 'antd';
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { createDMConverse, t, useAsyncRequest } from 'tailchat-shared';
import { FriendPicker } from '../UserPicker/FriendPicker';
import { closeModal, ModalWrapper } from '../Modal';
@ -17,7 +17,7 @@ export const CreateDMConverse: React.FC<CreateDMConverseProps> = React.memo(
(props) => {
const { hiddenUserIds = [] } = props;
const [selectedFriendIds, setSelectedFriendIds] = useState<string[]>([]);
const history = useHistory();
const navigate = useNavigate();
const [{ loading }, handleCreate] = useAsyncRequest(async () => {
const converse = await createDMConverse([
@ -25,7 +25,7 @@ export const CreateDMConverse: React.FC<CreateDMConverseProps> = React.memo(
...selectedFriendIds,
]);
closeModal();
history.push(`/main/personal/converse/${converse._id}`);
navigate(`/main/personal/converse/${converse._id}`);
}, [selectedFriendIds]);
return (

@ -12,7 +12,7 @@ import type { GroupPanel } from 'tailchat-shared';
import { Avatar } from '../Avatar';
import { closeModal, ModalWrapper } from '../Modal';
import { Slides, SlidesRef } from '../Slides';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { applyDefaultFallbackGroupPermission } from 'tailchat-shared';
const panelTemplate: {
@ -78,7 +78,7 @@ export const ModalCreateGroup: React.FC = React.memo(() => {
const [panels, setPanels] = useState<GroupPanel[]>([]);
const [name, setName] = useState('');
const dispatch = useAppDispatch();
const history = useHistory();
const navigate = useNavigate();
const handleSelectTemplate = useCallback((panels: GroupPanel[]) => {
setPanels(panels);
@ -94,7 +94,7 @@ export const ModalCreateGroup: React.FC = React.memo(() => {
dispatch(groupActions.appendGroups([data]));
history.push(`/main/group/${data._id}`); // 创建完成后跳转到新建的群组
navigate(`/main/group/${data._id}`); // 创建完成后跳转到新建的群组
// 应用默认权限
await applyDefaultFallbackGroupPermission(String(data._id));

@ -60,7 +60,7 @@ export const GroupPanelTree: React.FC<GroupPanelTree> = React.memo((props) => {
(node: DataNode): React.ReactNode => {
return (
<div className="flex group">
<span>{node.title}</span>
<span>{node.title as string}</span>
<div className="opacity-0 group-hover:opacity-100 ml-2">
<Space size="small">
<IconBtn

@ -13,12 +13,9 @@ export function useGroupPanelTreeDrag(
) {
const draggingNode = useRef<DataNode | null>(null);
const handleDragStart = useCallback(
(info: NodeDragEventParams<HTMLDivElement>) => {
draggingNode.current = info.node;
},
[]
);
const handleDragStart = useCallback((info: NodeDragEventParams<any>) => {
draggingNode.current = info.node;
}, []);
const handleDragEnd = useCallback(() => {
draggingNode.current = null;
@ -49,7 +46,7 @@ export function useGroupPanelTreeDrag(
const handleDrop = useCallback(
(
info: NodeDragEventParams & {
dragNode: EventDataNode;
dragNode: EventDataNode<any>;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap: boolean;

@ -1,10 +1,12 @@
import clsx from 'clsx';
import React from 'react';
import React, { PropsWithChildren } from 'react';
export const RoleItem: React.FC<{
active: boolean;
onClick?: () => void;
}> = React.memo((props) => {
export const RoleItem: React.FC<
PropsWithChildren<{
active: boolean;
onClick?: () => void;
}>
> = React.memo((props) => {
return (
<div
className={clsx(

@ -21,7 +21,7 @@ interface SettingsViewProps {
export const GroupDetail: React.FC<SettingsViewProps> = React.memo((props) => {
const groupId = props.groupId;
const handleChangeVisible = useCallback(
(visible) => {
(visible: boolean) => {
if (visible === false && typeof props.onClose === 'function') {
props.onClose();
}

@ -11,7 +11,7 @@ import { setUserJWT } from '@/utils/jwt-helper';
import { setGlobalUserLoginInfo } from '@/utils/user-helper';
import { Button, Divider, Typography } from 'antd';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import {
modifyUserField,
showToasts,
@ -27,7 +27,7 @@ import { ModifyPassword } from '../ModifyPassword';
export const SettingsAccount: React.FC = React.memo(() => {
const userInfo = useUserInfo();
const dispatch = useAppDispatch();
const history = useHistory();
const navigate = useNavigate();
const [, handleUserAvatarChange] = useAsyncRequest(
async (fileInfo: UploadFileResult) => {
@ -66,7 +66,7 @@ export const SettingsAccount: React.FC = React.memo(() => {
await setUserJWT(null);
getGlobalSocket()?.disconnect();
setGlobalUserLoginInfo(null);
history.push('/');
navigate('/');
}, []);
if (!userInfo) {

@ -19,7 +19,7 @@ interface SettingsViewProps {
}
export const SettingsView: React.FC<SettingsViewProps> = React.memo((props) => {
const handleChangeVisible = useCallback(
(visible) => {
(visible: boolean) => {
if (visible === false && typeof props.onClose === 'function') {
props.onClose();
}

@ -1,17 +1,18 @@
import React, { useContext } from 'react';
import React, { PropsWithChildren, useContext } from 'react';
import type { AppSocket } from 'tailchat-shared';
const SocketContext = React.createContext<AppSocket>({} as AppSocket);
SocketContext.displayName = 'SocketContext';
export const SocketContextProvider: React.FC<{ socket: AppSocket }> =
React.memo((props) => {
return (
<SocketContext.Provider value={props.socket}>
{props.children}
</SocketContext.Provider>
);
});
export const SocketContextProvider: React.FC<
PropsWithChildren<{ socket: AppSocket }>
> = React.memo((props) => {
return (
<SocketContext.Provider value={props.socket}>
{props.children}
</SocketContext.Provider>
);
});
SocketContextProvider.displayName = 'SocketContextProvider';
export function useSocketContext(): AppSocket {

@ -1,7 +1,7 @@
import './init';
import './dev';
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { initPlugins } from './plugin/loader';
import { installServiceWorker } from './utils/sw-helper';
@ -13,11 +13,12 @@ installServiceWorker();
// 先加载插件再开启应用
initPlugins()
.then(() => {
ReactDOM.render(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = createRoot(document.querySelector('#app')!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.querySelector('#app')
</React.StrictMode>
);
})
.catch(() => {

@ -50,7 +50,7 @@ export {
sendMessage,
} from 'tailchat-shared';
export { useLocation, useHistory } from 'react-router';
export { useLocation, useNavigate } from 'react-router';
export {
/**

@ -4,7 +4,7 @@ import { setUserJWT } from '@/utils/jwt-helper';
import { setGlobalUserLoginInfo } from '@/utils/user-helper';
import { Icon } from '@/components/Icon';
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import {
createTemporaryUser,
isValidStr,
@ -15,7 +15,7 @@ import { string } from 'yup';
import { useNavToView } from './utils';
export const GuestView: React.FC = React.memo(() => {
const history = useHistory();
const navigate = useNavigate();
const navToView = useNavToView();
const navRedirect = useSearchParam('redirect');
const [nickname, setNickname] = useState('');
@ -29,9 +29,9 @@ export const GuestView: React.FC = React.memo(() => {
await setUserJWT(data.token);
if (isValidStr(navRedirect)) {
history.push(decodeURIComponent(navRedirect));
navigate(decodeURIComponent(navRedirect));
} else {
history.push('/main');
navigate('/main');
}
}, [nickname, history, navRedirect]);

@ -4,7 +4,7 @@ import { isValidStr, loginWithEmail, t, useAsyncFn } from 'tailchat-shared';
import React, { useEffect, useState } from 'react';
import { Spinner } from '../../components/Spinner';
import { string } from 'yup';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { setUserJWT } from '../../utils/jwt-helper';
import { setGlobalUserLoginInfo, tryAutoLogin } from '../../utils/user-helper';
import { useSearchParam } from '@/hooks/useSearchParam';
@ -37,13 +37,13 @@ OAuthLoginView.displayName = 'OAuthLoginView';
export const LoginView: React.FC = React.memo(() => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const history = useHistory();
const navigate = useNavigate();
const navRedirect = useSearchParam('redirect');
useEffect(() => {
tryAutoLogin()
.then(() => {
history.push('/main');
navigate('/main');
})
.catch(() => {});
}, []);
@ -65,9 +65,9 @@ export const LoginView: React.FC = React.memo(() => {
await setUserJWT(data.token);
if (isValidStr(navRedirect)) {
history.push(decodeURIComponent(navRedirect));
navigate(decodeURIComponent(navRedirect));
} else {
history.push('/main');
navigate('/main');
}
}, [email, password, history, navRedirect]);

@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { Spinner } from '../../components/Spinner';
import { string } from 'yup';
import { Icon } from '@/components/Icon';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { setUserJWT } from '../../utils/jwt-helper';
import { setGlobalUserLoginInfo } from '../../utils/user-helper';
import { useSearchParam } from '@/hooks/useSearchParam';
@ -15,7 +15,7 @@ import { useNavToView } from './utils';
export const RegisterView: React.FC = React.memo(() => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const history = useHistory();
const navigate = useNavigate();
const navRedirect = useSearchParam('redirect');
const [{ loading, error }, handleRegister] = useAsyncFn(async () => {
@ -35,9 +35,9 @@ export const RegisterView: React.FC = React.memo(() => {
await setUserJWT(data.token);
if (isValidStr(navRedirect)) {
history.push(decodeURIComponent(navRedirect));
navigate(decodeURIComponent(navRedirect));
} else {
history.push('/main');
navigate('/main');
}
}, [email, password, navRedirect]);

@ -1,5 +1,5 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import { LoginView } from './LoginView';
import clsx from 'clsx';
import styles from './index.module.less';
@ -21,14 +21,16 @@ const EntryRoute = React.memo(() => {
)}
style={{ backgroundImage: `url(${loginPatternUrl})` }}
>
<Switch>
<Route path="/entry/login" component={LoginView} />
<Route path="/entry/register" component={RegisterView} />
<Route path="/entry/guest" component={GuestView} />
<Route path="/entry/forget" component={ForgetPasswordView} />
<Redirect to="/entry/login" />
</Switch>
<Routes>
<Route path="/login" element={<LoginView />} />
<Route path="/register" element={<RegisterView />} />
<Route path="/guest" element={<GuestView />} />
<Route path="/forget" element={<ForgetPasswordView />} />
<Route
path="/"
element={<Navigate to="/entry/login" replace={true} />}
/>
</Routes>
</div>
<div className="flex-1 sm:hidden tc-background" />

@ -1,17 +1,18 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router';
import { useLocation, useNavigate } from 'react-router';
/**
*
*/
export function useNavToView() {
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
const navToView = useCallback(
(pathname: string) => {
// 携带上下文切换路由
history.push({
...history.location,
navigate({
...location,
pathname,
});
},

@ -2,7 +2,7 @@ import { openModal } from '@/components/Modal';
import { getUserJWT } from '@/utils/jwt-helper';
import { Button } from 'antd';
import React, { useCallback, useState } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import {
applyGroupInvite,
checkTokenValid,
@ -18,7 +18,7 @@ interface Props {
expired?: string;
}
export const JoinBtn: React.FC<Props> = React.memo((props) => {
const history = useHistory();
const navigate = useNavigate();
const { loading, value: isTokenValid } = useAsync(async () => {
const token = await getUserJWT();
if (!token) {
@ -31,7 +31,7 @@ export const JoinBtn: React.FC<Props> = React.memo((props) => {
const [isJoined, setIsJoined] = useState(false);
const handleRegister = useCallback(() => {
history.push(
navigate(
`/entry/register?redirect=${encodeURIComponent(location.pathname)}`
);
}, []);

@ -1,7 +1,7 @@
import { ModalWrapper, useModalContext } from '@/components/Modal';
import { Button } from 'antd';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { t } from 'tailchat-shared';
interface Props {
@ -9,10 +9,11 @@ interface Props {
}
export const SuccessModal: React.FC<Props> = React.memo((props) => {
const { closeModal } = useModalContext();
const history = useHistory();
const navigate = useNavigate();
const handleNav = useCallback(() => {
closeModal();
history.push(`/main/group/${props.groupId}`);
navigate(`/main/group/${props.groupId}`);
}, [closeModal, props.groupId]);
return (

@ -8,7 +8,7 @@ import { useRecordMeasure } from '@/utils/measure-helper';
*
*/
const InviteRoute: React.FC = React.memo(() => {
const { inviteCode } = useParams<{ inviteCode: string }>();
const { inviteCode = '' } = useParams<{ inviteCode: string }>();
useRecordMeasure('AppInviteRenderStart');
return (

@ -1,14 +1,14 @@
import React, { useEffect } from 'react';
import { useHistory, useParams } from 'react-router';
import { useNavigate, useParams } from 'react-router';
import { GroupPanelType, useGroupInfo, useUpdateRef } from 'tailchat-shared';
import _isNil from 'lodash/isNil';
import { useUserSessionPreference } from '@/hooks/useUserPreference';
export const GroupPanelRedirect: React.FC = React.memo(() => {
const { groupId } = useParams<{
const { groupId = '' } = useParams<{
groupId: string;
}>();
const history = useHistory();
const navigate = useNavigate();
const [lastVisitPanel] = useUserSessionPreference('groupLastVisitPanel');
const lastVisitPanelRef = useUpdateRef(lastVisitPanel);
@ -32,7 +32,7 @@ export const GroupPanelRedirect: React.FC = React.memo(() => {
*/
const panelExist = panels.some((p) => p.id === lastVisitPanelId);
if (panelExist) {
history.replace(`/main/group/${groupId}/${lastVisitPanelId}`);
navigate(`/main/group/${groupId}/${lastVisitPanelId}`, { replace: true });
return;
}
@ -40,7 +40,9 @@ export const GroupPanelRedirect: React.FC = React.memo(() => {
(panel) => panel.type !== GroupPanelType.GROUP
);
if (!_isNil(firstAvailablePanel)) {
history.replace(`/main/group/${groupId}/${firstAvailablePanel.id}`);
navigate(`/main/group/${groupId}/${firstAvailablePanel.id}`, {
replace: true,
});
}
}, [groupInfo]);

@ -6,15 +6,11 @@ import { GroupSection } from '@/components/GroupSection';
import { CommonSidebarWrapper } from '@/components/CommonSidebarWrapper';
import { SidebarItem } from './SidebarItem';
interface GroupParams {
groupId: string;
}
/**
*
*/
export const Sidebar: React.FC = React.memo(() => {
const { groupId } = useParams<GroupParams>();
const { groupId = '' } = useParams<{ groupId: string }>();
const groupInfo = useGroupInfo(groupId);
const groupPanels = groupInfo?.panels ?? [];

@ -2,7 +2,7 @@ import { LoadingSpinner } from '@/components/LoadingSpinner';
import { SplitPanel } from '@/components/SplitPanel';
import { GroupIdContextProvider } from '@/context/GroupIdContext';
import React from 'react';
import { Route, Switch, useParams } from 'react-router-dom';
import { Route, Routes, useParams } from 'react-router-dom';
import { isValidStr, useGroupInfo } from 'tailchat-shared';
import { PageContent } from '../PageContent';
import { GroupPanelRender, GroupPanelRoute } from './Panel';
@ -10,7 +10,7 @@ import { GroupPanelRedirect } from './PanelRedirect';
import { Sidebar } from './Sidebar';
export const Group: React.FC = React.memo(() => {
const { groupId } = useParams<{
const { groupId = '' } = useParams<{
groupId: string;
}>();
const groupInfo = useGroupInfo(groupId);
@ -22,14 +22,10 @@ export const Group: React.FC = React.memo(() => {
const pinnedPanelId = groupInfo.pinnedPanelId;
const routeMatch = (
<Switch>
<Route path="/main/group/:groupId/:panelId" component={GroupPanelRoute} />
<Route
path="/main/group/:groupId"
exact={true}
component={GroupPanelRedirect}
/>
</Switch>
<Routes>
<Route path="/:panelId" element={<GroupPanelRoute />} />
<Route path="/" element={<GroupPanelRedirect />} />
</Routes>
);
return (

@ -3,7 +3,7 @@ import { GroupDetail } from '@/components/modals/GroupDetail';
import { CreateGroupInvite } from '@/components/modals/CreateGroupInvite';
import React from 'react';
import { useCallback } from 'react';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { quitGroup, showAlert, t, useIsGroupOwner } from 'tailchat-shared';
import { useLocationNav } from '@/hooks/useHistoryNav';
@ -12,7 +12,7 @@ import { useLocationNav } from '@/hooks/useHistoryNav';
*/
export function useGroupHeaderAction(groupId: string) {
const isOwner = useIsGroupOwner(groupId);
const history = useHistory();
const navigate = useNavigate();
const handleShowGroupDetail = useCallback(() => {
const key = openModal(
@ -36,7 +36,9 @@ export function useGroupHeaderAction(groupId: string) {
: t('确定要退出群组么?'),
async onConfirm() {
await quitGroup(groupId);
history.replace('/main'); // 返回到主页
navigate('/main', {
replace: true,
}); // 返回到主页
},
});
}, [groupId, isOwner]);

@ -1,5 +1,4 @@
import { useParams } from 'react-router';
import { GroupPanel, useGroupPanelInfo } from 'tailchat-shared';
/**
*
@ -8,7 +7,7 @@ export function useGroupPanelParams(): {
groupId: string;
panelId: string;
} {
const { groupId, panelId } = useParams<{
const { groupId = '', panelId = '' } = useParams<{
groupId: string;
panelId: string;
}>();

@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import React, { PropsWithChildren, useCallback, useEffect } from 'react';
import { useSidebarContext } from '../SidebarContext';
import _isNil from 'lodash/isNil';
import { useDrag } from 'react-use-gesture';
@ -6,11 +6,11 @@ import { useIsMobile } from '@/hooks/useIsMobile';
import clsx from 'clsx';
import { ErrorBoundary } from '@/components/ErrorBoundary';
const PageContentRoot: React.FC = (props) => (
const PageContentRoot: React.FC<PropsWithChildren> = (props) => (
<div className="flex flex-row flex-1 overflow-hidden">{props.children}</div>
);
const PageGestureWrapper: React.FC = React.memo((props) => {
const PageGestureWrapper: React.FC<PropsWithChildren> = React.memo((props) => {
const { setShowSidebar } = useSidebarContext();
const bind = useDrag(
@ -40,82 +40,83 @@ interface PageContentProps {
/**
*
*/
export const PageContent: React.FC<PageContentProps> = React.memo((props) => {
const { sidebar, children } = props;
const { showSidebar, setShowSidebar } = useSidebarContext();
const isMobile = useIsMobile();
const handleHideSidebar = useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
setShowSidebar(false);
},
[]
);
useEffect(() => {
if (isMobile === false) {
// 如果不为移动端, 则一定显示侧边栏
setShowSidebar(true);
}
}, [isMobile]);
const sidebarEl = _isNil(sidebar) ? null : (
<div
className={clsx(
'bg-sidebar-light dark:bg-sidebar-dark flex-shrink-0 transition-width',
{
'w-60': showSidebar,
'w-0': !showSidebar,
}
)}
>
{props.sidebar}
</div>
);
// 是否显示遮罩层
const showMask =
isMobile === true && showSidebar === true && !_isNil(sidebarEl);
const contentMaskEl = showMask ? (
<div
className="absolute left-0 top-0 bottom-0 right-0 z-10"
onClick={handleHideSidebar}
/>
) : null;
const contentEl = children;
export const PageContent: React.FC<PropsWithChildren<PageContentProps>> =
React.memo((props) => {
const { sidebar, children } = props;
const { showSidebar, setShowSidebar } = useSidebarContext();
const isMobile = useIsMobile();
const handleHideSidebar = useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
setShowSidebar(false);
},
[]
);
const el = (
<ErrorBoundary>
{sidebarEl}
useEffect(() => {
if (isMobile === false) {
// 如果不为移动端, 则一定显示侧边栏
setShowSidebar(true);
}
}, [isMobile]);
const sidebarEl = _isNil(sidebar) ? null : (
<div
className={clsx(
'flex flex-auto bg-content-light dark:bg-content-dark relative overflow-hidden'
'bg-sidebar-light dark:bg-sidebar-dark flex-shrink-0 transition-width',
{
'w-60': showSidebar,
'w-0': !showSidebar,
}
)}
data-tc-role={props['data-tc-role']}
>
<div className="tc-content-background" />
{props.sidebar}
</div>
);
// 是否显示遮罩层
const showMask =
isMobile === true && showSidebar === true && !_isNil(sidebarEl);
const contentMaskEl = showMask ? (
<div
className="absolute left-0 top-0 bottom-0 right-0 z-10"
onClick={handleHideSidebar}
/>
) : null;
const contentEl = children;
const el = (
<ErrorBoundary>
{sidebarEl}
<div
className={clsx('flex flex-auto relative', {
'overflow-auto': !showMask,
'overflow-hidden': showMask,
})}
className={clsx(
'flex flex-auto bg-content-light dark:bg-content-dark relative overflow-hidden'
)}
data-tc-role={props['data-tc-role']}
>
{contentMaskEl}
{contentEl}
<div className="tc-content-background" />
<div
className={clsx('flex flex-auto relative', {
'overflow-auto': !showMask,
'overflow-hidden': showMask,
})}
>
{contentMaskEl}
{contentEl}
</div>
</div>
</div>
</ErrorBoundary>
);
</ErrorBoundary>
);
if (isMobile) {
return <PageGestureWrapper>{el}</PageGestureWrapper>;
} else {
return <PageContentRoot>{el}</PageContentRoot>;
}
});
if (isMobile) {
return <PageGestureWrapper>{el}</PageGestureWrapper>;
} else {
return <PageContentRoot>{el}</PageContentRoot>;
}
});
PageContent.displayName = 'PageContent';

@ -1,13 +1,14 @@
import { NotFound } from '@/components/NotFound';
import { ConversePanel } from '@/components/Panel/personal/ConversePanel';
import React from 'react';
import { useParams } from 'react-router';
interface UserConversePanelParams {
converseId: string;
}
export const PersonalConverse: React.FC = React.memo(() => {
const params = useParams<UserConversePanelParams>();
const params = useParams<{ converseId: string }>();
if (!params.converseId) {
return <NotFound />;
}
return <ConversePanel converseId={params.converseId} />;
});

@ -14,7 +14,7 @@ import {
import { UserListItem } from '@/components/UserListItem';
import { IconBtn } from '@/components/IconBtn';
import { Button, Dropdown, Menu, Tooltip } from 'antd';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { Problem } from '@/components/Problem';
/**
@ -24,13 +24,13 @@ export const FriendList: React.FC<{
onSwitchToAddFriend: () => void;
}> = React.memo((props) => {
const friends = useAppSelector((state) => state.user.friends);
const history = useHistory();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [, handleCreateConverse] = useAsyncRequest(
async (targetId: string) => {
const converse = await createDMConverse([targetId]);
history.push(`/main/personal/converse/${converse._id}`);
navigate(`/main/personal/converse/${converse._id}`);
},
[history]
);

@ -1,4 +1,4 @@
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { Icon } from '@/components/Icon';
import { SidebarItem } from '../SidebarItem';
import { t, useDMConverseList, useUserInfo } from 'tailchat-shared';
@ -10,9 +10,11 @@ import { SectionHeader } from '@/components/SectionHeader';
import { CommonSidebarWrapper } from '@/components/CommonSidebarWrapper';
import { pluginCustomPanel } from '@/plugin/common';
const SidebarSection: React.FC<{
action: React.ReactNode;
}> = React.memo((props) => {
const SidebarSection: React.FC<
PropsWithChildren<{
action: React.ReactNode;
}>
> = React.memo((props) => {
return (
<div className="h-10 text-gray-900 dark:text-white flex pt-4 px-2">
<span className="flex-1 overflow-hidden overflow-ellipsis text-xs text-gray-700 dark:text-gray-300">

@ -1,7 +1,7 @@
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { pluginCustomPanel } from '@/plugin/common';
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import { PageContent } from '../PageContent';
import { PersonalConverse } from './Converse';
import { FriendPanel } from './Friends';
@ -12,28 +12,22 @@ export const Personal: React.FC = React.memo(() => {
return (
<PageContent data-tc-role="content-personal" sidebar={<PersonalSidebar />}>
<ErrorBoundary>
<Switch>
<Route path="/main/personal/friends" component={FriendPanel} />
<Route path="/main/personal/plugins" component={PluginsPanel} />
<Route
path="/main/personal/converse/:converseId"
component={PersonalConverse}
/>
<Routes>
<Route path="/friends" element={<FriendPanel />} />
<Route path="/plugins" element={<PluginsPanel />} />
<Route path="/converse/:converseId" element={<PersonalConverse />} />
{pluginCustomPanel
.filter((p) => p.position === 'personal')
.map((p) => (
<Route
key={p.name}
path={`/main/personal/custom/${p.name}`}
component={p.render}
path={`/custom/${p.name}`}
element={React.createElement(p.render)}
/>
))}
<Redirect to="/main/personal/friends" />
</Switch>
<Route path="/" element={<Navigate to="/main/personal/friends" />} />
</Routes>
</ErrorBoundary>
</PageContent>
);

@ -1,15 +1,18 @@
import React from 'react';
import { Personal } from './Personal';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Group } from './Group';
export const MainContent: React.FC = React.memo(() => {
return (
<Switch>
<Route path="/main/personal" component={Personal} />
<Route path="/main/group/:groupId" component={Group} />
<Redirect to="/main/personal" />
</Switch>
<Routes>
<Route path="/personal/*" element={<Personal />} />
<Route path="/group/:groupId/*" element={<Group />} />
<Route
path="/"
element={<Navigate to="/main/personal" replace={true} />}
/>
</Routes>
);
});
MainContent.displayName = 'MainContent';

@ -1,19 +1,21 @@
import { Tooltip, Badge } from 'antd';
import type { ClassValue } from 'clsx';
import clsx from 'clsx';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { useLocation } from 'react-router';
import { Link } from 'react-router-dom';
export const NavbarNavItem: React.FC<{
name: string;
className?: ClassValue;
to?: string;
showPill?: boolean;
badge?: boolean;
onClick?: () => void;
['data-testid']?: string;
}> = React.memo((props) => {
export const NavbarNavItem: React.FC<
PropsWithChildren<{
name: string;
className?: ClassValue;
to?: string;
showPill?: boolean;
badge?: boolean;
onClick?: () => void;
['data-testid']?: string;
}>
> = React.memo((props) => {
const { name, className, to, showPill = false, badge = false } = props;
const location = useLocation();
const isActive = typeof to === 'string' && location.pathname.startsWith(to);

@ -8,11 +8,11 @@ import {
ReduxProvider,
UserLoginInfo,
} from 'tailchat-shared';
import React from 'react';
import React, { PropsWithChildren } from 'react';
import { LoadingSpinner } from '@/components/LoadingSpinner';
import { tryAutoLogin } from '@/utils/user-helper';
import _isNil from 'lodash/isNil';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router';
import { SidebarContextProvider } from './SidebarContext';
import { PortalHost } from '@/components/Portal';
import { setGlobalSocket, setGlobalStore } from '@/utils/global-state-helper';
@ -23,7 +23,7 @@ import { Problem } from '@/components/Problem';
* hooks
*/
function useAppState() {
const history = useHistory();
const navigate = useNavigate();
const { value, loading, error } = useAsync(async () => {
let userLoginInfo: UserLoginInfo;
@ -31,8 +31,9 @@ function useAppState() {
userLoginInfo = await tryAutoLogin();
} catch (e) {
// 当前 Token 不存在或已过期
history.replace(
`/entry/login?redirect=${encodeURIComponent(location.pathname)}`
navigate(
`/entry/login?redirect=${encodeURIComponent(location.pathname)}`,
{ replace: true }
);
return;
}
@ -63,7 +64,7 @@ function useAppState() {
* Provider
*
*/
export const MainProvider: React.FC = React.memo((props) => {
export const MainProvider: React.FC<PropsWithChildren> = React.memo((props) => {
const { loading, store, error, socket } = useAppState();
if (loading) {

@ -1,4 +1,9 @@
import React, { useContext, useState, useCallback } from 'react';
import React, {
useContext,
useState,
useCallback,
PropsWithChildren,
} from 'react';
import _noop from 'lodash/noop';
interface SidebarContextProps {
@ -13,22 +18,24 @@ const SidebarContext = React.createContext<SidebarContextProps>({
});
SidebarContext.displayName = 'SidebarContext';
export const SidebarContextProvider: React.FC = React.memo((props) => {
const [showSidebar, setShowSidebar] = useState(true);
export const SidebarContextProvider: React.FC<PropsWithChildren> = React.memo(
(props) => {
const [showSidebar, setShowSidebar] = useState(true);
// 切换
const switchSidebar = useCallback(() => {
setShowSidebar(!showSidebar);
}, [showSidebar]);
// 切换
const switchSidebar = useCallback(() => {
setShowSidebar(!showSidebar);
}, [showSidebar]);
return (
<SidebarContext.Provider
value={{ showSidebar, switchSidebar, setShowSidebar }}
>
{props.children}
</SidebarContext.Provider>
);
});
return (
<SidebarContext.Provider
value={{ showSidebar, switchSidebar, setShowSidebar }}
>
{props.children}
</SidebarContext.Provider>
);
}
);
SidebarContextProvider.displayName = 'SidebarContextProvider';
export function useSidebarContext(): SidebarContextProps {

@ -1,11 +1,24 @@
import { useRecordMeasure } from '@/utils/measure-helper';
import React from 'react';
import { Route, Switch } from 'react-router';
import { Route, Routes } from 'react-router';
import { MainProvider } from '../Main/Provider';
import { t } from 'tailchat-shared';
import { PersonalConverse } from '../Main/Content/Personal/Converse';
import { GroupPanelRoute } from '../Main/Content/Group/Panel';
import { GroupDetail } from '@/components/modals/GroupDetail';
import { useParams } from 'react-router';
import { NotFound } from '@/components/NotFound';
const GroupDetailRoute = React.memo(() => {
const { groupId } = useParams<{ groupId: string }>();
if (!groupId) {
return <NotFound />;
}
return <GroupDetail groupId={groupId} onClose={() => {}} />;
});
GroupDetailRoute.displayName = 'GroupDetailRoute';
const PanelRoute: React.FC = React.memo(() => {
useRecordMeasure('AppRouteRenderStart');
@ -13,30 +26,19 @@ const PanelRoute: React.FC = React.memo(() => {
return (
<div className="flex h-full bg-content-light dark:bg-content-dark">
<MainProvider>
<Switch>
<Route
exact={true}
path="/panel/personal/converse/:converseId"
component={PersonalConverse}
/>
<Routes>
<Route
exact={true}
path="/panel/group/:groupId/detail"
render={(props) => (
<GroupDetail
groupId={props.match.params.groupId}
onClose={() => {}}
/>
)}
path="/personal/converse/:converseId"
element={<PersonalConverse />}
/>
<Route path="/group/:groupId/detail" element={<GroupDetailRoute />} />
<Route
exact={true}
path="/panel/group/:groupId/:panelId"
component={GroupPanelRoute}
path="/group/:groupId/:panelId"
element={<GroupPanelRoute />}
/>
<Route>{t('未知的面板')}</Route>
</Switch>
<Route path="/*" element={t('未知的面板')} />
</Routes>
</MainProvider>
</div>
);

@ -7,7 +7,8 @@
"@test/*": ["./test/*"],
"@assets/*": ["./assets/*"],
"tailchat-design": ["../packages/design/components"]
}
},
"typeRoots": ["./node_modules/@types", "./types"]
},
"exclude": ["e2e", "plugins"]
}

File diff suppressed because it is too large Load Diff

@ -8,8 +8,8 @@
"tailchat-meeting-sdk": "1.2.4"
},
"devDependencies": {
"@types/react": "^17.0.38",
"@types/react": "18.0.20",
"@types/react-router": "^5.1.18",
"react": "17.0.2"
"react": "18.2.0"
}
}

@ -4,7 +4,7 @@
"version": "0.0.0",
"private": true,
"devDependencies": {
"@types/react": "^17.0.38",
"react": "17.0.2"
"@types/react": "18.0.20",
"react": "18.2.0"
}
}

Loading…
Cancel
Save