refactor: user list
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { useEffect, FC, useRef } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
import toast from "react-hot-toast";
|
||||
import { ViewportList } from 'react-viewport-list';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUpdateUserMutation } from "../../../app/services/user";
|
||||
import User from "../User";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import IconOwner from "../../../assets/icons/owner.svg";
|
||||
import IconArrowDown from "../../../assets/icons/arrow.down.mini.svg";
|
||||
import IconCheck from "../../../assets/icons/check.sign.svg";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
interface Props {
|
||||
cid?: number;
|
||||
}
|
||||
const MemberList: FC<Props> = ({ cid }) => {
|
||||
const ref = useRef<HTMLUListElement | null>(
|
||||
null,
|
||||
);
|
||||
const { t } = useTranslation("member");
|
||||
const { t: ct } = useTranslation();
|
||||
const { users, channels, loginUser } = useAppSelector((store) => {
|
||||
return {
|
||||
users: store.users,
|
||||
channels: store.channels,
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const { copyEmail, removeFromChannel, removeUser } = useUserOperation({ cid });
|
||||
const [updateUser, { isSuccess: updateSuccess }] = useUpdateUserMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSuccess) {
|
||||
toast.success(ct("tip.update"));
|
||||
}
|
||||
}, [updateSuccess]);
|
||||
|
||||
const handleToggleRole = ({
|
||||
ignore = false,
|
||||
uid,
|
||||
isAdmin = true
|
||||
}: {
|
||||
ignore: boolean;
|
||||
uid: number;
|
||||
isAdmin: boolean;
|
||||
}) => {
|
||||
hideAll();
|
||||
if (ignore) return;
|
||||
updateUser({ id: uid, is_admin: isAdmin });
|
||||
};
|
||||
|
||||
const channel = cid ? channels.byId[cid] : null;
|
||||
const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids;
|
||||
return <ul className="flex flex-col gap-1 w-full md:w-[512px] mb-44" ref={ref}>
|
||||
<ViewportList
|
||||
viewportRef={ref}
|
||||
items={uids}
|
||||
>
|
||||
{(uid) => {
|
||||
const currUser = users.byId[uid];
|
||||
if (!currUser) return null;
|
||||
const { name, email, is_admin } = currUser;
|
||||
const owner = channel && channel.owner == uid;
|
||||
const switchRoleVisible = loginUser?.is_admin && loginUser.uid !== uid && uid !== 1;
|
||||
const dotsVisible = email || loginUser?.is_admin;
|
||||
const canRemove = loginUser?.is_admin && loginUser?.uid != uid && uid !== 1;
|
||||
const canRemoveFromChannel =
|
||||
channel && channel.owner == loginUser?.uid && loginUser?.uid != uid;
|
||||
return (
|
||||
<li key={uid} className="w-full flex items-center justify-between px-3 py-2 rounded-md md:hover:bg-slate-50 md:dark:hover:bg-gray-800">
|
||||
<div className="flex gap-4">
|
||||
<User compact uid={uid} interactive={false} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm text-gray-600 dark:text-white flex items-center gap-1">
|
||||
{name} {owner && <IconOwner />}
|
||||
</span>
|
||||
<span className="hidden md:block text-xs text-gray-500 dark:text-slate-50">{email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-7">
|
||||
{switchRoleVisible ? <Tippy
|
||||
interactive
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
content={
|
||||
<ul className="context-menu">
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: is_admin,
|
||||
uid,
|
||||
isAdmin: true
|
||||
})}
|
||||
>
|
||||
{t("admin")}
|
||||
{is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: !is_admin,
|
||||
uid,
|
||||
isAdmin: false
|
||||
})}
|
||||
>
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="text-xs text-right text-gray-500 dark:text-slate-100 flex items-center gap-1 cursor-pointer">
|
||||
{is_admin ? t("admin") : t("user")}
|
||||
<IconArrowDown className="dark:fill-slate-50" />
|
||||
</span>
|
||||
</Tippy> :
|
||||
<span className="text-xs text-right text-gray-500 dark:text-slate-100 flex items-center gap-1">
|
||||
{is_admin ? t("admin") : t("user")}</span>}
|
||||
|
||||
{dotsVisible && (
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
content={
|
||||
<ul className="min-w-30 context-menu">
|
||||
{email && (
|
||||
<li className="item" onClick={copyEmail.bind(null, email)}>
|
||||
{ct("action.copy_email")}
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromChannel && (
|
||||
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
|
||||
{t("remove_from_channel")}
|
||||
</li>
|
||||
)}
|
||||
{canRemove && (
|
||||
<li className="item danger" onClick={removeUser.bind(null, uid)}>
|
||||
{ct("action.remove")}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<div className="relative w-6 h-6">
|
||||
<img className="cursor-pointer" src={moreIcon} alt="dots icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
</ViewportList>
|
||||
</ul>;
|
||||
};
|
||||
export default MemberList;
|
||||
@@ -1,56 +1,20 @@
|
||||
import { useEffect, FC } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUpdateUserMutation } from "../../../app/services/user";
|
||||
import User from "../User";
|
||||
import { FC } from "react";
|
||||
import InviteLink from "../InviteLink";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import IconOwner from "../../../assets/icons/owner.svg";
|
||||
import IconArrowDown from "../../../assets/icons/arrow.down.mini.svg";
|
||||
import IconCheck from "../../../assets/icons/check.sign.svg";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MemberList from "./MemberList";
|
||||
|
||||
interface Props {
|
||||
cid?: number;
|
||||
}
|
||||
const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
const { t } = useTranslation("member");
|
||||
const { t: ct } = useTranslation();
|
||||
const { users, channels, loginUser } = useAppSelector((store) => {
|
||||
const { loginUser } = useAppSelector((store) => {
|
||||
return {
|
||||
users: store.users,
|
||||
channels: store.channels,
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const { copyEmail, removeFromChannel, removeUser } = useUserOperation({ cid });
|
||||
const [updateUser, { isSuccess: updateSuccess }] = useUpdateUserMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSuccess) {
|
||||
toast.success(ct("tip.update"));
|
||||
}
|
||||
}, [updateSuccess]);
|
||||
|
||||
const handleToggleRole = ({
|
||||
ignore = false,
|
||||
uid,
|
||||
isAdmin = true
|
||||
}: {
|
||||
ignore: boolean;
|
||||
uid: number;
|
||||
isAdmin: boolean;
|
||||
}) => {
|
||||
hideAll();
|
||||
if (ignore) return;
|
||||
updateUser({ id: uid, is_admin: isAdmin });
|
||||
};
|
||||
|
||||
const channel = cid ? channels.byId[cid] : null;
|
||||
const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids;
|
||||
return (
|
||||
<section className="flex flex-col w-full">
|
||||
{loginUser?.is_admin && <InviteLink />}
|
||||
@@ -60,104 +24,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
{t("manage_tip")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-1 w-full md:w-[512px] mb-44">
|
||||
{uids.map((uid) => {
|
||||
const currUser = users.byId[uid];
|
||||
if (!currUser) return null;
|
||||
const { name, email, is_admin } = currUser;
|
||||
const owner = channel && channel.owner == uid;
|
||||
const switchRoleVisible = loginUser?.is_admin && loginUser.uid !== uid && uid !== 1;
|
||||
const dotsVisible = email || loginUser?.is_admin;
|
||||
const canRemove = loginUser?.is_admin && loginUser?.uid != uid && uid !== 1;
|
||||
const canRemoveFromChannel =
|
||||
channel && channel.owner == loginUser?.uid && loginUser?.uid != uid;
|
||||
return (
|
||||
<li key={uid} className="w-full flex items-center justify-between px-3 py-2 rounded-md md:hover:bg-slate-50 md:dark:hover:bg-gray-800">
|
||||
<div className="flex gap-4">
|
||||
<User compact uid={uid} interactive={false} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm text-gray-600 dark:text-white flex items-center gap-1">
|
||||
{name} {owner && <IconOwner />}
|
||||
</span>
|
||||
<span className="hidden md:block text-xs text-gray-500 dark:text-slate-50">{email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-7">
|
||||
{switchRoleVisible ? <Tippy
|
||||
interactive
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
content={
|
||||
<ul className="context-menu">
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: is_admin,
|
||||
uid,
|
||||
isAdmin: true
|
||||
})}
|
||||
>
|
||||
{t("admin")}
|
||||
{is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: !is_admin,
|
||||
uid,
|
||||
isAdmin: false
|
||||
})}
|
||||
>
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="text-xs text-right text-gray-500 dark:text-slate-100 flex items-center gap-1 cursor-pointer">
|
||||
{is_admin ? t("admin") : t("user")}
|
||||
<IconArrowDown className="dark:fill-slate-50" />
|
||||
</span>
|
||||
</Tippy> :
|
||||
<span className="text-xs text-right text-gray-500 dark:text-slate-100 flex items-center gap-1">
|
||||
{is_admin ? t("admin") : t("user")}</span>}
|
||||
|
||||
{dotsVisible && (
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
content={
|
||||
<ul className="min-w-30 context-menu">
|
||||
{email && (
|
||||
<li className="item" onClick={copyEmail.bind(null, email)}>
|
||||
{ct("action.copy_email")}
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromChannel && (
|
||||
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
|
||||
{t("remove_from_channel")}
|
||||
</li>
|
||||
)}
|
||||
{canRemove && (
|
||||
<li className="item danger" onClick={removeUser.bind(null, uid)}>
|
||||
{ct("action.remove")}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<div className="relative w-6 h-6">
|
||||
<img className="cursor-pointer" src={moreIcon} alt="dots icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<MemberList cid={cid} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { ViewportList } from 'react-viewport-list';
|
||||
import User from "../../../common/component/User";
|
||||
import IconAdd from "../../../assets/icons/add.svg";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
|
||||
|
||||
type Props = {
|
||||
membersVisible: boolean,
|
||||
uids: number[],
|
||||
addVisible: boolean,
|
||||
cid: number,
|
||||
ownerId: number
|
||||
}
|
||||
|
||||
const Members = ({ uids, addVisible, ownerId, cid, membersVisible }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const ref = useRef<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
|
||||
const toggleAddVisible = () => {
|
||||
setAddMemberModalVisible((prev) => !prev);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{addMemberModalVisible && <InviteModal cid={cid} closeModal={toggleAddVisible} />}
|
||||
<div ref={ref} className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${membersVisible ? "flex" : "hidden"}`}>
|
||||
{addVisible && (
|
||||
<div className="cursor-pointer flex items-center justify-start gap-1 select-none rounded-lg p-2.5 md:hover:bg-gray-500/10" onClick={toggleAddVisible}>
|
||||
<IconAdd className="w-6 h-6 dark:fill-slate-300" />
|
||||
<div className="font-semibold text-sm text-gray-600 dark:text-gray-50">{t("add_channel_members")}</div>
|
||||
</div>
|
||||
)}
|
||||
<ViewportList
|
||||
viewportRef={ref}
|
||||
items={uids}
|
||||
>
|
||||
{(uid: number) => {
|
||||
return (
|
||||
<User
|
||||
enableContextMenu={true}
|
||||
cid={cid}
|
||||
owner={ownerId == uid}
|
||||
key={uid}
|
||||
uid={uid}
|
||||
dm
|
||||
popover
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ViewportList>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Members;
|
||||
@@ -10,7 +10,6 @@ import { updateRememberedNavs } from "../../../app/slices/ui";
|
||||
import useMessageFeed from "../../../common/hook/useMessageFeed";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import Tooltip from "../../../common/component/Tooltip";
|
||||
import User from "../../../common/component/User";
|
||||
import Layout from "../Layout";
|
||||
import { renderMessageFragment } from "../utils";
|
||||
import EditIcon from "../../../assets/icons/edit.svg";
|
||||
@@ -18,12 +17,12 @@ import IconFav from "../../../assets/icons/bookmark.svg";
|
||||
import IconPeople from "../../../assets/icons/people.svg";
|
||||
import IconPin from "../../../assets/icons/pin.svg";
|
||||
|
||||
import IconAdd from "../../../assets/icons/add.svg";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
|
||||
import LoadMore from "../LoadMore";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import GoBackNav from "../../../common/component/GoBackNav";
|
||||
import Members from "./Members";
|
||||
type Props = {
|
||||
cid?: number;
|
||||
dropFiles?: File[];
|
||||
@@ -45,7 +44,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const updateReadDebounced = useDebounce(updateReadIndex, 300);
|
||||
const [membersVisible, setMembersVisible] = useState(true);
|
||||
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
|
||||
|
||||
const {
|
||||
selects,
|
||||
userIds,
|
||||
@@ -73,9 +72,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const toggleMembersVisible = () => {
|
||||
setMembersVisible((prev) => !prev);
|
||||
};
|
||||
const toggleAddVisible = () => {
|
||||
setAddMemberModalVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
if (!data) return null;
|
||||
const { name, description, is_public, members = [], owner } = data;
|
||||
const memberIds = is_public ? userIds : members.slice(0).sort((n) => (n == owner ? -1 : 0));
|
||||
@@ -86,7 +83,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const toolClass = `relative cursor-pointer`;
|
||||
return (
|
||||
<>
|
||||
{addMemberModalVisible && <InviteModal cid={cid} closeModal={toggleAddVisible} />}
|
||||
|
||||
<Layout
|
||||
to={cid}
|
||||
context="channel"
|
||||
@@ -143,27 +140,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
</header>
|
||||
}
|
||||
users={
|
||||
<div className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${membersVisible ? "flex" : "hidden"}`}>
|
||||
{addVisible && (
|
||||
<div className="cursor-pointer flex items-center justify-start gap-1 select-none rounded-lg p-2.5 md:hover:bg-gray-500/10" onClick={toggleAddVisible}>
|
||||
<IconAdd className="w-6 h-6 dark:fill-slate-300" />
|
||||
<div className="font-semibold text-sm text-gray-600 dark:text-gray-50">{t("add_channel_members")}</div>
|
||||
</div>
|
||||
)}
|
||||
{memberIds.map((uid: number) => {
|
||||
return (
|
||||
<User
|
||||
enableContextMenu={true}
|
||||
cid={cid}
|
||||
owner={owner == uid}
|
||||
key={uid}
|
||||
uid={uid}
|
||||
dm
|
||||
popover
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Members uids={memberIds} addVisible={addVisible} cid={cid} ownerId={owner} membersVisible={membersVisible} />
|
||||
}
|
||||
>
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { memo, useEffect } from "react";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { NavLink, useParams, useLocation } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ViewportList } from 'react-viewport-list';
|
||||
|
||||
import { updateRememberedNavs } from "../../app/slices/ui";
|
||||
import Search from "./Search";
|
||||
import User from "../../common/component/User";
|
||||
@@ -12,6 +14,9 @@ import clsx from "clsx";
|
||||
import GoBackNav from "../../common/component/GoBackNav";
|
||||
|
||||
function UsersPage() {
|
||||
const ref = useRef<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const { pathname } = useLocation();
|
||||
const { input, updateInput, users } = useFilteredUsers();
|
||||
@@ -32,16 +37,19 @@ function UsersPage() {
|
||||
isUserDetail && "hidden md:flex"
|
||||
)}>
|
||||
<Search input={input} updateInput={updateInput} />
|
||||
<div className="px-2 pt-3 pb-20 md:py-3 overflow-scroll">
|
||||
<nav className="flex flex-col md:gap-1">
|
||||
{users.map(({ uid }) => {
|
||||
<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) => {
|
||||
return (
|
||||
<NavLink key={uid} className={({ isActive }) => `rounded-md md:hover:bg-gray-500/10 ${isActive ? "bg-gray-500/10" : ""}`} to={`/users/${uid}`}>
|
||||
<User uid={uid} enableContextMenu={true} />
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
}}
|
||||
</ViewportList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(`md:rounded-r-2xl bg-white w-full flex justify-center items-start dark:bg-gray-700`,
|
||||
|
||||
Reference in New Issue
Block a user