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
+21 -21
View File
@@ -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",
+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>
);
}}
+441 -260
View File
File diff suppressed because it is too large Load Diff