feat: merge alex version
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// import React from 'react';
|
||||
import IconAdd from '../../../assets/icons/add.person.svg';
|
||||
import IconBlock from '../../../assets/icons/block.svg';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import { useUpdateContactStatusMutation } from '../../../app/services/user';
|
||||
import { ContactAction } from '../../../types/user';
|
||||
import ServerVersionChecker from '../../../common/component/ServerVersionChecker';
|
||||
// import toast from 'react-hot-toast';
|
||||
|
||||
type Props = {
|
||||
uid: number
|
||||
}
|
||||
|
||||
const AddContactTip = (props: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const [updateContactStatus] = useUpdateContactStatusMutation();
|
||||
const targetUser = useAppSelector(store => store.users.byId[props.uid]);
|
||||
const handleContactStatus = (action: ContactAction) => {
|
||||
updateContactStatus({ target_uid: props.uid, action });
|
||||
};
|
||||
// useEffect(() => {
|
||||
// if(isSuccess){
|
||||
// toast.success(t("tip"))
|
||||
// }
|
||||
// }, [isSuccess])
|
||||
|
||||
const itemClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-50 dark:bg-gray-800 text-sm pt-3.5 pb-3`;
|
||||
if (!targetUser) return null;
|
||||
if (targetUser.status == "added") return null;
|
||||
const blocked = targetUser.status == "blocked";
|
||||
return (
|
||||
<ServerVersionChecker version='0.3.7' empty={true}>
|
||||
<div className="py-4 px-10 flex flex-col items-center gap-3 bg-slate-100 dark:bg-slate-600">
|
||||
<h3 className='text-gray-700 dark:text-gray-300 text-sm font-semibold'>{blocked ? t("contact_block_tip") : t("contact_tip")}</h3>
|
||||
<ul className='flex gap-4'>
|
||||
{!blocked && <li className={itemClass} onClick={handleContactStatus.bind(null, "add")}>
|
||||
<IconAdd className="fill-primary-400" />
|
||||
<span>{t("add_contact")}</span>
|
||||
</li>}
|
||||
<li className={itemClass} onClick={blocked ? handleContactStatus.bind(null, "unblock") : handleContactStatus.bind(null, "block")}>
|
||||
<IconBlock className="stroke-primary-400" />
|
||||
<span>{blocked ? t("unblock") : t("block")}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ServerVersionChecker>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddContactTip;
|
||||
@@ -18,6 +18,7 @@ import ImagePreview from "../../../common/component/ImagePreview";
|
||||
|
||||
import VirtualMessageFeed from "./VirtualMessageFeed";
|
||||
import DMVoice from "./DMVoicing";
|
||||
import AddContactTip from "./AddContactTip";
|
||||
|
||||
interface Props {
|
||||
readonly?: boolean;
|
||||
@@ -100,6 +101,7 @@ const Layout: FC<Props> = ({
|
||||
<div className="w-full h-full flex items-start justify-between relative" >
|
||||
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
|
||||
{context == "user" && <DMVoice uid={to} />}
|
||||
{context == "user" && <AddContactTip uid={to} />}
|
||||
{/* 消息流 */}
|
||||
<VirtualMessageFeed key={`${context}_${to}`} context={context} id={to} />
|
||||
{/* 发送框 */}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, ReactElement } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useLocation, useMatch } from "react-router-dom";
|
||||
import { useUpdateMuteSettingMutation } from "../../../app/services/user";
|
||||
import { usePinChatMutation, useUnpinChatMutation, useUpdateMuteSettingMutation } from "../../../app/services/user";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { removeUserSession } from "../../../app/slices/message.user";
|
||||
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
|
||||
@@ -10,8 +10,10 @@ import useUserOperation from "../../../common/hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChatContext } from "../../../types/common";
|
||||
import { compareVersion } from "../../../common/utils";
|
||||
type Props = {
|
||||
context: ChatContext;
|
||||
pinned: boolean;
|
||||
id: number;
|
||||
visible: boolean;
|
||||
mid: number;
|
||||
@@ -21,6 +23,7 @@ type Props = {
|
||||
children: ReactElement;
|
||||
};
|
||||
const SessionContextMenu: FC<Props> = ({
|
||||
pinned,
|
||||
context = "dm",
|
||||
id,
|
||||
visible,
|
||||
@@ -36,13 +39,16 @@ const SessionContextMenu: FC<Props> = ({
|
||||
cid: context == "channel" ? id : undefined
|
||||
});
|
||||
const [muteChannel] = useUpdateMuteSettingMutation();
|
||||
const [pinChat] = usePinChatMutation();
|
||||
const [unpinChat] = useUnpinChatMutation();
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const pathMatched = useMatch(`/chat/dm/${id}`);
|
||||
const dispatch = useDispatch();
|
||||
const navigateTo = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const { channelMuted } = useAppSelector((store) => {
|
||||
const { channelMuted, serverVersion } = useAppSelector((store) => {
|
||||
return {
|
||||
serverVersion: store.server.version,
|
||||
channelMuted: context == "channel" ? store.footprint.muteChannels[id] : false
|
||||
};
|
||||
});
|
||||
@@ -70,13 +76,27 @@ const SessionContextMenu: FC<Props> = ({
|
||||
const data = channelMuted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
|
||||
muteChannel(data);
|
||||
};
|
||||
const handlePinChat = () => {
|
||||
const data = context == "dm" ? { uid: id } : { gid: id };
|
||||
if (pinned) {
|
||||
unpinChat(data);
|
||||
} else {
|
||||
pinChat(data);
|
||||
}
|
||||
};
|
||||
const handleDMSetting = () => {
|
||||
navigateTo(`/setting/dm/${id}/overview?f=${pathname}`);
|
||||
};
|
||||
const pinTxt = t(pinned ? "unpin_chat" : "pin_chat", { ns: "chat" });
|
||||
|
||||
const pinVisible = compareVersion(serverVersion, "0.3.7") >= 0;
|
||||
const items =
|
||||
context == "dm"
|
||||
? [
|
||||
pinVisible && {
|
||||
title: pinTxt,
|
||||
handler: handlePinChat
|
||||
},
|
||||
{
|
||||
title: t("action.mark_read"),
|
||||
handler: handleReadAll
|
||||
@@ -96,6 +116,10 @@ const SessionContextMenu: FC<Props> = ({
|
||||
}
|
||||
]
|
||||
: [
|
||||
pinVisible && {
|
||||
title: pinTxt,
|
||||
handler: handlePinChat
|
||||
},
|
||||
{
|
||||
title: t("setting"),
|
||||
underline: true,
|
||||
|
||||
@@ -21,11 +21,13 @@ interface IProps {
|
||||
type?: ChatContext;
|
||||
id: number;
|
||||
mid: number;
|
||||
pinned?: boolean;
|
||||
setDeleteChannelId: (param: number) => void;
|
||||
setInviteChannelId: (param: number) => void;
|
||||
}
|
||||
const Session: FC<IProps> = ({
|
||||
type = "dm",
|
||||
pinned = false,
|
||||
id,
|
||||
mid,
|
||||
setDeleteChannelId,
|
||||
@@ -85,7 +87,7 @@ const Session: FC<IProps> = ({
|
||||
useEffect(() => {
|
||||
const tmp = type == "dm" ? userData[id] : channelData[id];
|
||||
if (!tmp) return;
|
||||
if ("avatar" in tmp) {
|
||||
if (type == "dm") {
|
||||
// user
|
||||
const { name, avatar } = tmp;
|
||||
setData({ name, icon: avatar, mid, is_public: true });
|
||||
@@ -110,13 +112,14 @@ const Session: FC<IProps> = ({
|
||||
console.log("unreads", unreads, isCurrentPath);
|
||||
|
||||
return (
|
||||
<li className="session">
|
||||
<li className={clsx("session")}>
|
||||
<ContextMenu
|
||||
visible={contextMenuVisible}
|
||||
hide={hideContextMenu}
|
||||
context={type}
|
||||
id={id}
|
||||
mid={mid}
|
||||
pinned={pinned}
|
||||
setInviteChannelId={setInviteChannelId}
|
||||
deleteChannel={setDeleteChannelId}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { ChatContext } from "../../../types/common";
|
||||
import { PinChatTargetChannel, PinChatTargetUser } from "../../../types/sse";
|
||||
export interface ChatSession {
|
||||
type: ChatContext;
|
||||
id: number;
|
||||
@@ -20,9 +21,11 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
||||
const [deleteId, setDeleteId] = useState<number>();
|
||||
const [inviteChannelId, setInviteChannelId] = useState<number>();
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const { channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
|
||||
const [pinSessions, setPinSessions] = useState<ChatSession[]>([]);
|
||||
const { pins, channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
|
||||
useAppSelector((store) => {
|
||||
return {
|
||||
pins: store.footprint.pinChats,
|
||||
loginUid: store.authData.user?.uid,
|
||||
channelIDs: store.channels.ids,
|
||||
DMs: store.userMessage.ids,
|
||||
@@ -34,24 +37,49 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const cSessions = channelIDs.map((id) => {
|
||||
const mids = channelMessage[id];
|
||||
// const pinDMs=
|
||||
const getSessionObj = (id: number, type: "dm" | "channel") => {
|
||||
const mids = type == "dm" ? userMessage[id] : channelMessage[id];
|
||||
if (!mids || mids.length == 0) {
|
||||
return { unreads: 0, id, type: "channel" };
|
||||
return { unread: 0, id, type } as ChatSession;
|
||||
}
|
||||
// 先转换成数字,再排序
|
||||
const mid = [...mids].sort((a, b) => +a - +b).pop();
|
||||
return { id, mid, type: "channel" };
|
||||
return { id, mid, type } as ChatSession;
|
||||
};
|
||||
const pinTmps = pins.map((p) => {
|
||||
const { target } = p;
|
||||
if ("uid" in target) {
|
||||
return getSessionObj((target as PinChatTargetUser).uid, "dm");
|
||||
}
|
||||
if ("gid" in target) {
|
||||
return getSessionObj((target as PinChatTargetChannel).gid, "channel");
|
||||
}
|
||||
return null;
|
||||
}).filter((p) => !!p);
|
||||
const channelPinIds = pins.map(p => {
|
||||
if (p.target.gid) {
|
||||
return p.target.gid;
|
||||
}
|
||||
return null;
|
||||
|
||||
}).filter(id => !!id);
|
||||
const dmPinIds = pins.map(p => {
|
||||
if (p.target.uid) {
|
||||
return p.target.uid;
|
||||
}
|
||||
return null;
|
||||
|
||||
}).filter(id => !!id);
|
||||
const cSessions = channelIDs.filter(id => {
|
||||
return !channelPinIds.includes(id);
|
||||
}).map((id) => {
|
||||
return getSessionObj(id, "channel");
|
||||
});
|
||||
const uSessions = DMs.map((id) => {
|
||||
// console.log("adddd", id);
|
||||
const mids = userMessage[id];
|
||||
if (!mids || mids.length == 0) {
|
||||
return { unreads: 0, id, type: "dm" };
|
||||
}
|
||||
// 先转换成数字,再排序
|
||||
const mid = [...mids].sort((a, b) => +a - +b).pop();
|
||||
return { type: "dm", id, mid };
|
||||
const uSessions = DMs.filter(id => {
|
||||
return !dmPinIds.includes(id);
|
||||
}).map((id) => {
|
||||
return getSessionObj(id, "dm");
|
||||
});
|
||||
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
|
||||
const { mid: aMid = 0 } = a;
|
||||
@@ -62,6 +90,7 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
||||
const newSessions = tempSession ? [tempSession, ...temps] : temps;
|
||||
// console.log("qqqq", newSessions);
|
||||
setSessions(newSessions);
|
||||
setPinSessions(pinTmps);
|
||||
}, [
|
||||
channelIDs,
|
||||
DMs,
|
||||
@@ -70,11 +99,29 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
||||
readUsers,
|
||||
loginUid,
|
||||
userMessage,
|
||||
tempSession
|
||||
tempSession,
|
||||
pins
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pinSessions.length ? <ul className="flex flex-col gap-0.5 p-2 bg-primary-500/10">
|
||||
{pinSessions.map((p) => {
|
||||
const { type, id, mid = 0 } = p;
|
||||
const key = `${type}_${id}`;
|
||||
return (
|
||||
<Session
|
||||
key={key}
|
||||
type={type}
|
||||
pinned={true}
|
||||
id={id}
|
||||
mid={mid}
|
||||
setInviteChannelId={setInviteChannelId}
|
||||
setDeleteChannelId={setDeleteId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul> : null}
|
||||
<ul ref={ref} className="flex flex-1 flex-col gap-0.5 p-2 overflow-auto">
|
||||
<ViewportList
|
||||
initialPrerender={10}
|
||||
|
||||
@@ -72,10 +72,7 @@ function ChatPage() {
|
||||
)}>
|
||||
<Server readonly={isGuest} />
|
||||
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
|
||||
|
||||
{isGuest ? <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
|
||||
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
|
||||
</footer> : <RTCWidget id={+contextId} context={context} />}
|
||||
<RTCWidget id={+contextId} context={context} />
|
||||
</div>
|
||||
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
|
||||
{voiceFullscreenVisible && <VoiceFullscreen id={contextId} context={context} />}
|
||||
|
||||
@@ -32,11 +32,10 @@ import LazyIt from './lazy';
|
||||
import store, { useAppSelector } from "../app/store";
|
||||
import useDeviceToken from "../common/component/Notification/useDeviceToken";
|
||||
import { vapidKey } from "../app/config";
|
||||
import useTabBroadcast from "../common/hook/useTabBroadcast";
|
||||
import InactiveScreen from "../common/component/InactiveScreen";
|
||||
import RequireSingleTab from "../common/component/RequireSingleTab";
|
||||
import InvitePrivate from "./invitePrivate";
|
||||
let toastId: string;
|
||||
const PageRoutes = () => {
|
||||
|
||||
const {
|
||||
ui: { online },
|
||||
fileMessages
|
||||
@@ -59,6 +58,7 @@ const PageRoutes = () => {
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/guest_login" element={<LazyIt><GuestLogin /></LazyIt>} />
|
||||
<Route path="/invite_private/:channel_id" element={<LazyIt><RequireAuth><InvitePrivate /></RequireAuth></LazyIt>} />
|
||||
<Route path="/cb/:type/:payload" element={<LazyIt><CallbackPage /></LazyIt>} />
|
||||
<Route path="/oauth/:token" element={<LazyIt><OAuthPage /></LazyIt>} />
|
||||
<Route
|
||||
@@ -112,7 +112,10 @@ const PageRoutes = () => {
|
||||
element={
|
||||
<LazyIt>
|
||||
<RequireAuth>
|
||||
<HomePage />
|
||||
{/* 只允许活跃一个tab标签 */}
|
||||
<RequireSingleTab>
|
||||
<HomePage />
|
||||
</RequireSingleTab>
|
||||
</RequireAuth>
|
||||
</LazyIt>
|
||||
}
|
||||
@@ -199,11 +202,11 @@ const PageRoutes = () => {
|
||||
};
|
||||
|
||||
export default function ReduxRoutes() {
|
||||
const { tabActive } = useTabBroadcast();
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Meta />
|
||||
{tabActive ? <PageRoutes /> : <InactiveScreen />}
|
||||
<PageRoutes />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useJoinPrivateChannelMutation, useLazyGetChannelQuery } from '../../app/services/channel';
|
||||
import StyledButton from '../../common/component/styled/Button';
|
||||
import { useCheckMagicTokenValidMutation } from '../../app/services/auth';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
|
||||
const InvitePrivate = () => {
|
||||
const { channel_id } = useParams();
|
||||
const server = useAppSelector(store => store.server);
|
||||
const navigateTo = useNavigate();
|
||||
const [joinChannel, { isLoading, data, isSuccess }] = useJoinPrivateChannelMutation();
|
||||
const [fetchChannelInfo, { data: channel, isSuccess: fetchChannelSuccess }] = useLazyGetChannelQuery();
|
||||
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
|
||||
useCheckMagicTokenValidMutation();
|
||||
let [searchParams] = useSearchParams(new URLSearchParams(location.search));
|
||||
const magic_token = searchParams.get("magic_token") ?? "";
|
||||
useEffect(() => {
|
||||
if (channel_id) {
|
||||
fetchChannelInfo(+channel_id);
|
||||
}
|
||||
}, [channel_id]);
|
||||
useEffect(() => {
|
||||
if (magic_token) {
|
||||
checkTokenInvalid(magic_token);
|
||||
}
|
||||
}, [magic_token]);
|
||||
useEffect(() => {
|
||||
if (data && isSuccess) {
|
||||
// joinChannel(data)
|
||||
navigateTo(`/chat/channel/${data.gid}`);
|
||||
}
|
||||
}, [isSuccess, data]);
|
||||
const handleJoin = async () => {
|
||||
const resp = await joinChannel({ magic_token });
|
||||
if ("error" in resp) {
|
||||
if (resp.error.originalStatus === 409) {
|
||||
// alert("The invite link is invalid or expired");
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!fetchChannelSuccess) return null;
|
||||
return (
|
||||
<div className="flex-center flex-col gap-4 h-screen overflow-x-hidden overflow-y-auto dark:bg-gray-700 dark:text-slate-100">
|
||||
<div className="flex flex-col gap-4 items-center py-8 px-10 rounded-lg shadow-md bg-slate-100/30 dark:bg-gray-800 text-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<img src={server.logo} className='w-20 h-20' alt="server logo" />
|
||||
<h2 className="text-2xl font-bold">
|
||||
{server.name}
|
||||
</h2>
|
||||
</div>
|
||||
<span>
|
||||
{checkingToken ? "Checking..." : isTokenValid ? <>You are invited to join private channel <strong className='text-primary-400'>#{channel?.name}</strong></> : "The invite link is invalid or expired"}
|
||||
</span>
|
||||
<StyledButton disabled={isLoading || checkingToken || !isTokenValid} onClick={handleJoin}>Join</StyledButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvitePrivate;
|
||||
@@ -88,14 +88,6 @@ const navs = [
|
||||
name: "version",
|
||||
component: <Version />
|
||||
},
|
||||
{
|
||||
name: "faq",
|
||||
link: "https://doc.voce.chat/faq",
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
link: `https://privoce.voce.chat/widget?from=${encodeURIComponent(location.host)}`,
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
import { FC, ChangeEvent } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import IconSearch from "../../assets/icons/search.svg";
|
||||
import IconAdd from "../../assets/icons/add.svg";
|
||||
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
|
||||
import Tooltip from "../../common/component/Tooltip";
|
||||
import { FC, ChangeEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { compareVersion } from "../../common/utils";
|
||||
import Tooltip from "../../common/component/Tooltip";
|
||||
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
|
||||
type Props = {
|
||||
input: string,
|
||||
openModal: () => void,
|
||||
updateInput: (input: string) => void
|
||||
};
|
||||
const Search: FC<Props> = ({ input, updateInput }) => {
|
||||
const Search: FC<Props> = ({ input, updateInput, openModal }) => {
|
||||
const serverVersion = useAppSelector(store => store.server.version);
|
||||
const { t } = useTranslation();
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
const enableContact = compareVersion(serverVersion, "0.3.7") >= 0;
|
||||
return (
|
||||
|
||||
<div className="hidden md:flex relative min-h-[56px] px-2 py-3 items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />
|
||||
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50" onChange={handleInput} />
|
||||
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50 outline-none" onChange={handleInput} />
|
||||
</div>
|
||||
<Tooltip tip={t("more")} placement="bottom">
|
||||
{enableContact ? <IconAdd onClick={openModal} role="button" className="dark:fill-gray-400" /> : <Tooltip tip={t("more")} placement="bottom">
|
||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||
<IconAdd role="button" className="dark:fill-gray-400" />
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
</Tooltip>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { NavLink, useParams, useLocation } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ViewportList } from 'react-viewport-list';
|
||||
@@ -12,6 +12,7 @@ import BlankPlaceholder from "../../common/component/BlankPlaceholder";
|
||||
import useFilteredUsers from "../../common/hook/useFilteredUsers";
|
||||
import clsx from "clsx";
|
||||
import GoBackNav from "../../common/component/GoBackNav";
|
||||
import SearchUser from "../../common/component/SearchUser";
|
||||
|
||||
function UsersPage() {
|
||||
const ref = useRef<HTMLDivElement | null>(
|
||||
@@ -19,6 +20,7 @@ function UsersPage() {
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const { pathname } = useLocation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const { input, updateInput, users } = useFilteredUsers();
|
||||
const { user_id } = useParams();
|
||||
// 记住路由
|
||||
@@ -28,7 +30,9 @@ function UsersPage() {
|
||||
dispatch(updateRememberedNavs({ key: "user", path: pathname }));
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalVisible(prev => !prev);
|
||||
};
|
||||
if (!users) return null;
|
||||
const isUserDetail = !!user_id;
|
||||
return (
|
||||
@@ -36,7 +40,8 @@ function UsersPage() {
|
||||
<div className={clsx("md:rounded-l-2xl bg-white dark:bg-gray-800 relative flex flex-col w-full md:w-auto md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]",
|
||||
isUserDetail && "hidden md:flex"
|
||||
)}>
|
||||
<Search input={input} updateInput={updateInput} />
|
||||
<Search input={input} updateInput={updateInput} openModal={toggleModal} />
|
||||
{modalVisible && <SearchUser closeModal={toggleModal} />}
|
||||
<div className="flex flex-col md:gap-1 px-2 pt-3 pb-20 md:py-3 overflow-scroll" ref={ref}>
|
||||
<ViewportList
|
||||
viewportRef={ref}
|
||||
|
||||
Reference in New Issue
Block a user