feat: sort and group members

This commit is contained in:
Tristan Yang
2023-09-26 12:16:01 +08:00
parent 11504023ec
commit 3045f3add3
9 changed files with 593 additions and 329 deletions
+9 -9
View File
@@ -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

+20
View File
@@ -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;
}
+11 -13
View File
@@ -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>
+13 -5
View File
@@ -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]);
+42 -6
View File
@@ -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
/>
+25 -6
View File
@@ -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>
);
}}