feat: sort and group members
This commit is contained in:
+21
-21
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "vocechat-web",
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.8",
|
||||
"homepage": "https://voce.chat",
|
||||
"dependencies": {
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@metamask/onboarding": "^1.0.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||
"@react-oauth/google": "^0.11.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@reduxjs/toolkit": "^1.9.6",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@toast-ui/editor": "^3.2.2",
|
||||
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
||||
"@toast-ui/react-editor": "^3.2.3",
|
||||
"@udecode/plate": "16.8",
|
||||
"@udecode/plate": "16.9.1",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"agora-rtc-sdk-ng": "^4.18.2",
|
||||
"agora-rtc-sdk-ng": "^4.19.0",
|
||||
"broadcast-channel": "^5.3.0",
|
||||
"browserslist": "^4.21.10",
|
||||
"browserslist": "^4.21.11",
|
||||
"clsx": "^2.0.0",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"css-loader": "^6.8.1",
|
||||
"css-minimizer-webpack-plugin": "^5.0.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"dayjs": "^1.11.10",
|
||||
"emoji-mart": "5.5.2",
|
||||
"eslint": "^8.48.0",
|
||||
"firebase": "^10.3.1",
|
||||
"framer-motion": "^10.16.3",
|
||||
"eslint": "^8.50.0",
|
||||
"firebase": "^10.4.0",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fs-extra": "^11.1.1",
|
||||
"heic2any": "^0.0.4",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"i18next": "^23.4.6",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"i18next-http-backend": "^2.2.2",
|
||||
"linkify-plugin-mention": "^4.1.1",
|
||||
@@ -53,12 +53,12 @@
|
||||
"react-i18next": "^13.2.2",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-refresh": "0.14.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-use-wizard": "^2.2.3",
|
||||
"react-viewport-list": "^7.1.1",
|
||||
"react-virtuoso": "^4.5.0",
|
||||
"react-virtuoso": "^4.6.0",
|
||||
"rooks": "^7.14.1",
|
||||
"slate": "^0.94.1",
|
||||
"slate-history": "^0.93.0",
|
||||
@@ -69,7 +69,7 @@
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"tippy.js": "^6.3.7",
|
||||
"typescript": "^5.2.2",
|
||||
"wavesurfer.js": "^7.2.1",
|
||||
"wavesurfer.js": "^7.3.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
@@ -105,20 +105,20 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.15",
|
||||
"@babel/core": "^7.23.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",
|
||||
"@types/emoji-mart": "^3.0.9",
|
||||
"@types/emoji-mart": "^3.0.10",
|
||||
"@types/masonry-layout": "^4.2.5",
|
||||
"@types/node": "^20.5.9",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/node": "^20.7.0",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"@welldone-software/why-did-you-render": "^7.0.1",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-named-asset-import": "^0.3.8",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
@@ -130,7 +130,7 @@
|
||||
"gh-pages": "^6.0.0",
|
||||
"md5-file": "^5.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss": "^8.4.29",
|
||||
"postcss": "^8.4.30",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.0.3",
|
||||
|
||||
@@ -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