feat: sort and group members
This commit is contained in:
@@ -37,15 +37,15 @@ const serverSlice = createSlice({
|
||||
},
|
||||
fillServer(state, action: PayloadAction<StoredServer>) {
|
||||
const {
|
||||
version = "",
|
||||
upgraded = false,
|
||||
version,
|
||||
upgraded,
|
||||
inviteLink = {
|
||||
link: "",
|
||||
expire: 0
|
||||
},
|
||||
logo = "", // todo: check missed logo property
|
||||
name = "",
|
||||
description = "",
|
||||
logo, // todo: check missed logo property
|
||||
name,
|
||||
description,
|
||||
show_user_online_status = true,
|
||||
webclient_auto_update = true,
|
||||
contact_verification_enable = false,
|
||||
@@ -53,11 +53,11 @@ const serverSlice = createSlice({
|
||||
loginConfig = state.loginConfig || null
|
||||
} = action.payload || {};
|
||||
return {
|
||||
version,
|
||||
upgraded,
|
||||
name,
|
||||
version: state.version || version,
|
||||
upgraded: state.upgraded || upgraded,
|
||||
name: state.name || name,
|
||||
logo: state.logo || logo,
|
||||
description,
|
||||
description: state.description || description,
|
||||
inviteLink,
|
||||
show_user_online_status,
|
||||
webclient_auto_update,
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -356,3 +356,23 @@ input[type="number"]::-webkit-outer-spin-button {
|
||||
.or:has(+ .socials:empty) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 成员分组 */
|
||||
|
||||
[data-group-title] {
|
||||
margin-top: 42px;
|
||||
position: relative;
|
||||
}
|
||||
[data-group-title]:before {
|
||||
pointer-events: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #667085;
|
||||
content: attr(data-group-title);
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: -34px;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import clsx from "clsx";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
import useContextMenu from "@/hooks/useContextMenu";
|
||||
import IconBot from "@/assets/icons/bot.svg";
|
||||
import IconOwner from "@/assets/icons/owner.svg";
|
||||
import IconAdmin from "@/assets/icons/owner.svg";
|
||||
import Avatar from "../Avatar";
|
||||
import Profile from "../Profile";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
interface Props {
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
uid: number;
|
||||
cid?: number;
|
||||
owner?: boolean;
|
||||
@@ -35,7 +35,8 @@ const User: FC<Props> = ({
|
||||
compact = false,
|
||||
avatarSize = 32,
|
||||
enableContextMenu = false,
|
||||
enableNavToSetting = false
|
||||
enableNavToSetting = false,
|
||||
...rest
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu();
|
||||
@@ -79,6 +80,7 @@ const User: FC<Props> = ({
|
||||
onClick={enableNavToSetting ? handleNavToSetting : undefined}
|
||||
onDoubleClick={dm ? handleDoubleClick : undefined}
|
||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer relative"
|
||||
@@ -92,20 +94,15 @@ const User: FC<Props> = ({
|
||||
name={curr.name}
|
||||
alt="avatar"
|
||||
/>
|
||||
{curr.is_bot ? (
|
||||
<IconBot
|
||||
className={clsx("absolute -bottom-[2.5px] -right-[2.5px]", "!w-[15px] !h-[15px]")}
|
||||
/>
|
||||
) : (
|
||||
statusElement
|
||||
)}
|
||||
{statusElement}
|
||||
</div>
|
||||
{!compact && (
|
||||
<span className={nameClass} title={curr?.name}>
|
||||
{curr?.name}
|
||||
</span>
|
||||
)}
|
||||
{owner && <IconOwner />}
|
||||
{!compact && curr.is_admin && <IconAdmin />}
|
||||
{!compact && curr.is_bot && <IconBot />}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
@@ -128,6 +125,7 @@ const User: FC<Props> = ({
|
||||
className={containerClass}
|
||||
onDoubleClick={dm ? handleDoubleClick : undefined}
|
||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer relative"
|
||||
@@ -148,8 +146,8 @@ const User: FC<Props> = ({
|
||||
{curr?.name}
|
||||
</span>
|
||||
)}
|
||||
{owner && <IconOwner />}
|
||||
{curr.is_bot && <IconBot className="!w-4 !h-4" />}
|
||||
{!compact && curr.is_admin && <IconAdmin />}
|
||||
{!compact && curr.is_bot && <IconBot className="!w-4 !h-4" />}
|
||||
</div>
|
||||
</Tippy>
|
||||
</ContextMenu>
|
||||
|
||||
@@ -18,15 +18,23 @@ export default function useFilteredUsers() {
|
||||
const users = useContactList ? originUsers.filter((u) => u.status == "added") : originUsers;
|
||||
useEffect(() => {
|
||||
if (!input) {
|
||||
setFilteredUsers(users);
|
||||
setFilteredUsers(
|
||||
users.sort((a, b) => {
|
||||
return Number(b.is_admin) - Number(a.is_admin) || Number(b.is_bot) - Number(a.is_bot);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
let str = ["", escapeRegExp(input.toLowerCase()), ""].join(".*");
|
||||
let reg = new RegExp(str);
|
||||
setFilteredUsers(
|
||||
users.filter((c) => {
|
||||
if (!c) return false;
|
||||
return reg.test(c.name.toLowerCase());
|
||||
})
|
||||
users
|
||||
.filter((c) => {
|
||||
if (!c) return false;
|
||||
return reg.test(c.name.toLowerCase());
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return Number(b.is_admin) - Number(a.is_admin) || Number(b.is_bot) - Number(a.is_bot);
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [input, users.length]);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ViewportList } from "react-viewport-list";
|
||||
|
||||
import InviteModal from "@/components/InviteModal";
|
||||
import User from "@/components/User";
|
||||
import IconAdd from "@/assets/icons/add.svg";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
import { shallowEqual } from "react-redux";
|
||||
import { StoredUser } from "@/app/slices/users";
|
||||
|
||||
type Props = {
|
||||
membersVisible: boolean;
|
||||
@@ -17,10 +20,27 @@ type Props = {
|
||||
const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [sortedUsers, setSortedUsers] = useState<StoredUser[]>([]);
|
||||
const users = useAppSelector((store) => Object.values(store.users.byId), shallowEqual);
|
||||
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
|
||||
const toggleAddVisible = () => {
|
||||
setAddMemberModalVisible((prev) => !prev);
|
||||
};
|
||||
console.log({ sortedUsers });
|
||||
useEffect(() => {
|
||||
setSortedUsers(
|
||||
users
|
||||
.filter((u) => uids.includes(u.uid))
|
||||
.sort((a, b) => {
|
||||
return Number(b.is_admin) - Number(a.is_admin) || Number(b.is_bot) - Number(a.is_bot);
|
||||
})
|
||||
);
|
||||
}, [uids, users]);
|
||||
|
||||
const adminCount = sortedUsers.filter(({ is_admin }) => is_admin).length;
|
||||
const botCount = sortedUsers.filter(({ is_bot }) => is_bot).length;
|
||||
const memberCount = sortedUsers.length - adminCount - botCount;
|
||||
const sortedUids = sortedUsers.map(({ uid }) => uid);
|
||||
return (
|
||||
<>
|
||||
{addMemberModalVisible && <InviteModal cid={cid} closeModal={toggleAddVisible} />}
|
||||
@@ -41,15 +61,31 @@ const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ViewportList initialPrerender={15} viewportRef={ref} items={uids}>
|
||||
{(uid: number) => {
|
||||
<ViewportList initialPrerender={15} viewportRef={ref} items={sortedUids}>
|
||||
{(id: number, idx) => {
|
||||
const curr = sortedUsers.find(({ uid }) => uid === id);
|
||||
const prevUid = sortedUids[idx - 1];
|
||||
const prev = sortedUsers.find(({ uid }) => uid === prevUid);
|
||||
if (!curr) return null;
|
||||
const { is_admin, is_bot } = curr;
|
||||
const role = is_admin ? "admin" : is_bot ? "bot" : "member";
|
||||
const groupTitle =
|
||||
role === "admin"
|
||||
? `admin - ${adminCount}`
|
||||
: role === "bot"
|
||||
? `bot - ${botCount}`
|
||||
: `member - ${memberCount}`;
|
||||
const prefixHeader =
|
||||
idx === 0 ? true : prev?.is_admin !== is_admin || prev?.is_bot !== is_bot;
|
||||
return (
|
||||
<User
|
||||
data-role={role}
|
||||
data-group-title={prefixHeader ? groupTitle : undefined}
|
||||
enableContextMenu={true}
|
||||
cid={cid}
|
||||
owner={ownerId == uid}
|
||||
key={uid}
|
||||
uid={uid}
|
||||
owner={ownerId == id}
|
||||
key={id}
|
||||
uid={id}
|
||||
dm
|
||||
popover
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ function UsersPage() {
|
||||
const dispatch = useDispatch();
|
||||
const { pathname } = useLocation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const { input, updateInput, users } = useFilteredUsers();
|
||||
const { input, updateInput, users, uids } = useFilteredUsers();
|
||||
const { user_id } = useParams();
|
||||
// 记住路由
|
||||
useEffect(() => {
|
||||
@@ -31,6 +31,9 @@ function UsersPage() {
|
||||
setModalVisible((prev) => !prev);
|
||||
};
|
||||
if (!users) return null;
|
||||
const adminCount = users.filter(({ is_admin }) => is_admin).length;
|
||||
const botCount = users.filter(({ is_bot }) => is_bot).length;
|
||||
const memberCount = users.length - adminCount - botCount;
|
||||
const isUserDetail = !!user_id;
|
||||
return (
|
||||
<div className={clsx("flex h-screen md:h-full md:pt-2 md:pb-2.5 md:pr-12")}>
|
||||
@@ -43,17 +46,33 @@ function UsersPage() {
|
||||
<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} items={users.map(({ uid }) => uid)}>
|
||||
{(uid) => {
|
||||
<ViewportList viewportRef={ref} items={uids}>
|
||||
{(id, idx) => {
|
||||
const curr = users.find(({ uid }) => uid === id);
|
||||
const prevUid = uids[idx - 1];
|
||||
const prev = users.find(({ uid }) => uid === prevUid);
|
||||
if (!curr) return null;
|
||||
const { is_admin, is_bot } = curr;
|
||||
const role = is_admin ? "admin" : is_bot ? "bot" : "member";
|
||||
const groupTitle =
|
||||
role === "admin"
|
||||
? `admin - ${adminCount}`
|
||||
: role === "bot"
|
||||
? `bot - ${botCount}`
|
||||
: `member - ${memberCount}`;
|
||||
const prefixHeader =
|
||||
idx === 0 ? true : prev?.is_admin !== is_admin || prev?.is_bot !== is_bot;
|
||||
return (
|
||||
<NavLink
|
||||
key={uid}
|
||||
data-role={role}
|
||||
data-group-title={prefixHeader ? groupTitle : undefined}
|
||||
key={id}
|
||||
className={({ isActive }) =>
|
||||
`rounded-md md:hover:bg-gray-500/10 ${isActive ? "bg-gray-500/10" : ""}`
|
||||
}
|
||||
to={`/users/${uid}`}
|
||||
to={`/users/${id}`}
|
||||
>
|
||||
<User uid={uid} enableContextMenu={true} />
|
||||
<User uid={id} enableContextMenu={true} />
|
||||
</NavLink>
|
||||
);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user