feat: search user by username
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"cancel": "Cancel",
|
||||
"dismiss": "Dismiss",
|
||||
"search": "Search",
|
||||
"search_user": "Search by username",
|
||||
"new_channel": "New Channel",
|
||||
"new_private_channel": "New Private Channel",
|
||||
"new_msg": "New Message",
|
||||
|
||||
@@ -2,11 +2,13 @@ import { FC, useEffect, useRef } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ViewportList } from "react-viewport-list";
|
||||
import Search from "@/routes/users/Search";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
|
||||
import { useUpdateUserMutation } from "@/app/services/user";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
import useFilteredUsers from "@/hooks/useFilteredUsers";
|
||||
import useUserOperation from "@/hooks/useUserOperation";
|
||||
import IconArrowDown from "@/assets/icons/arrow.down.mini.svg";
|
||||
import IconCheck from "@/assets/icons/check.sign.svg";
|
||||
@@ -21,13 +23,13 @@ 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) => {
|
||||
const { channels, loginUser } = useAppSelector((store) => {
|
||||
return {
|
||||
users: store.users,
|
||||
channels: store.channels,
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const { users, input, updateInput } = useFilteredUsers();
|
||||
const { copyEmail, removeFromChannel, removeUser } = useUserOperation({ cid });
|
||||
const [updateUser, { isSuccess: updateSuccess }] = useUpdateUserMutation();
|
||||
|
||||
@@ -52,119 +54,123 @@ const MemberList: FC<Props> = ({ cid }) => {
|
||||
};
|
||||
|
||||
const channel = cid ? channels.byId[cid] : null;
|
||||
const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids;
|
||||
const ids = users.map((u) => u.uid);
|
||||
const uids = channel ? (channel.is_public ? ids : channel.members) : ids;
|
||||
return (
|
||||
<ul
|
||||
className="flex flex-col gap-1 w-full md:w-[512px] mb-44 max-h-[800px] overflow-y-scroll"
|
||||
ref={ref}
|
||||
>
|
||||
<ViewportList initialPrerender={15} 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 dark:fill-white" />}
|
||||
</li>
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: !is_admin,
|
||||
uid,
|
||||
isAdmin: false
|
||||
})}
|
||||
>
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon dark:fill-white" />}
|
||||
</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" />
|
||||
<>
|
||||
<Search input={input} updateInput={updateInput} type="members" />
|
||||
<ul
|
||||
className="flex flex-col gap-1 w-full md:w-[512px] mb-44 max-h-[800px] overflow-y-scroll"
|
||||
ref={ref}
|
||||
>
|
||||
<ViewportList initialPrerender={15} viewportRef={ref} items={uids}>
|
||||
{(uid) => {
|
||||
const currUser = users.find((u) => u.uid == 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>
|
||||
</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>
|
||||
)}
|
||||
<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 dark:fill-white" />}
|
||||
</li>
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
ignore: !is_admin,
|
||||
uid,
|
||||
isAdmin: false
|
||||
})}
|
||||
>
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon dark:fill-white" />}
|
||||
</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">
|
||||
<IconMore role="button" className="dark:fill-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
</ViewportList>
|
||||
</ul>
|
||||
{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">
|
||||
<IconMore role="button" className="dark:fill-gray-400" />
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
</ViewportList>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default MemberList;
|
||||
|
||||
+29
-14
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useAppSelector } from "@/app/store";
|
||||
import AddEntriesMenu from "@/components/AddEntriesMenu";
|
||||
@@ -9,36 +10,50 @@ import IconAdd from "@/assets/icons/add.svg";
|
||||
import IconSearch from "@/assets/icons/search.svg";
|
||||
|
||||
type Props = {
|
||||
type?: "users" | "members";
|
||||
input: string;
|
||||
openModal: () => void;
|
||||
openModal?: () => void;
|
||||
updateInput: (input: string) => void;
|
||||
};
|
||||
const Search: FC<Props> = ({ input, updateInput, openModal }) => {
|
||||
const Search: FC<Props> = ({ input, updateInput, openModal, type = "users" }) => {
|
||||
const enableContact = useAppSelector((store) => store.server.contact_verification_enable);
|
||||
const { t } = useTranslation();
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
const isMembers = type === "members";
|
||||
return (
|
||||
<div className="hidden md:flex relative min-h-[56px] px-2 py-3 items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full hidden md:flex relative px-2 items-center justify-between gap-2",
|
||||
isMembers
|
||||
? "md:w-[512px] min-h-[40px] rounded-full bg-black/10 mb-4 focus-within:bg-transparent focus-within:ring-[1px] focus-within:ring-gray-300"
|
||||
: "shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500 min-h-[56px]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />
|
||||
<input
|
||||
value={input}
|
||||
placeholder={`${t("action.search")}...`}
|
||||
className="w-full text-sm bg-transparent dark:text-gray-50 outline-none"
|
||||
placeholder={`${t("action.search_user")}...`}
|
||||
className={clsx(
|
||||
"w-full text-sm dark:text-gray-50 outline-none bg-transparent"
|
||||
// isMembers ? "rounded-full bg-black/10" : "bg-transparent"
|
||||
)}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</div>
|
||||
{enableContact ? (
|
||||
<IconAdd onClick={openModal} role="button" className="dark:fill-gray-400" />
|
||||
) : (
|
||||
<Tooltip tip={t("more")} placement="bottom">
|
||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||
<IconAdd role="button" className="dark:fill-gray-400" />
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isMembers ? (
|
||||
enableContact ? (
|
||||
<IconAdd onClick={openModal} role="button" className="dark:fill-gray-400" />
|
||||
) : (
|
||||
<Tooltip tip={t("more")} placement="bottom">
|
||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||
<IconAdd role="button" className="dark:fill-gray-400" />
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user