forked from abner/for-legacy-web
Virtualise server settings user lists. Closes #300
This commit is contained in:
@@ -5,11 +5,12 @@ import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import UserIcon from "./UserIcon";
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
const BotBadge = styled.div`
|
||||
display: inline-block;
|
||||
@@ -29,15 +30,10 @@ const BotBadge = styled.div`
|
||||
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
||||
user?: User;
|
||||
prefixAt?: boolean;
|
||||
showServerIdentity?: boolean;
|
||||
}
|
||||
showServerIdentity?: boolean | "both";
|
||||
};
|
||||
export const Username = observer(
|
||||
({
|
||||
user,
|
||||
prefixAt,
|
||||
showServerIdentity,
|
||||
...otherProps
|
||||
}: UsernameProps) => {
|
||||
({ user, prefixAt, showServerIdentity, ...otherProps }: UsernameProps) => {
|
||||
let username = user?.username;
|
||||
let color;
|
||||
|
||||
@@ -52,7 +48,11 @@ export const Username = observer(
|
||||
|
||||
if (member) {
|
||||
if (member.nickname) {
|
||||
username = member.nickname;
|
||||
if (showServerIdentity === "both") {
|
||||
username = `${member.nickname} (${username})`;
|
||||
} else {
|
||||
username = member.nickname;
|
||||
}
|
||||
}
|
||||
|
||||
if (member.roles && member.roles.length > 0) {
|
||||
@@ -112,17 +112,12 @@ export default function UserShort({
|
||||
|
||||
const handleUserClick = (e: MouseEvent) => {
|
||||
if (e.shiftKey && user?._id) {
|
||||
e.preventDefault()
|
||||
internalEmit(
|
||||
"MessageBox",
|
||||
"append",
|
||||
`<@${user?._id}>`,
|
||||
"mention",
|
||||
);
|
||||
e.preventDefault();
|
||||
internalEmit("MessageBox", "append", `<@${user?._id}>`, "mention");
|
||||
} else {
|
||||
openProfile()
|
||||
openProfile();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -176,12 +176,12 @@
|
||||
.scrollbox {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.contentcontainer {
|
||||
display: flex;
|
||||
gap: 13px;
|
||||
height: fit-content;
|
||||
max-width: 740px;
|
||||
padding: 80px 32px;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { XCircle } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Ban } from "revolt-api/types/Servers";
|
||||
import { User } from "revolt-api/types/Users";
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
import { Server } from "revolt.js/dist/maps/Servers";
|
||||
|
||||
@@ -11,12 +14,45 @@ import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import IconButton from "../../../components/ui/IconButton";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
interface InnerProps {
|
||||
ban: Ban;
|
||||
users: Pick<User, "username" | "avatar" | "_id">[];
|
||||
server: Server;
|
||||
removeSelf: () => void;
|
||||
}
|
||||
|
||||
const Inner = observer(({ ban, users, server, removeSelf }: InnerProps) => {
|
||||
const [deleting, setDelete] = useState(false);
|
||||
const user = users.find((x) => x._id === ban._id.user);
|
||||
|
||||
return (
|
||||
<div className={styles.ban} data-deleting={deleting}>
|
||||
<span>
|
||||
<UserIcon attachment={user?.avatar ?? undefined} size={24} />{" "}
|
||||
{user?.username}
|
||||
</span>
|
||||
<div className={styles.reason}>
|
||||
{ban.reason ?? (
|
||||
<Text id="app.settings.server_pages.bans.no_reason" />
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDelete(true);
|
||||
server.unbanUser(ban._id.user).then(removeSelf);
|
||||
}}
|
||||
disabled={deleting}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
export const Bans = observer(({ server }: Props) => {
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
const [data, setData] = useState<
|
||||
Route<"GET", "/servers/id/bans">["response"] | undefined
|
||||
>(undefined);
|
||||
@@ -39,42 +75,31 @@ export const Bans = observer(({ server }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
{typeof data === "undefined" && <Preloader type="ring" />}
|
||||
{data?.bans.map((x) => {
|
||||
const user = data.users.find((y) => y._id === x._id.user);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={x._id.user}
|
||||
className={styles.ban}
|
||||
data-deleting={deleting.indexOf(x._id.user) > -1}>
|
||||
<span>
|
||||
<UserIcon attachment={user?.avatar} size={24} />
|
||||
{user?.username}
|
||||
</span>
|
||||
<div className={styles.reason}>
|
||||
{x.reason ?? (
|
||||
<Text id="app.settings.server_pages.bans.no_reason" />
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, x._id.user]);
|
||||
|
||||
await server.unbanUser(x._id.user);
|
||||
|
||||
setData({
|
||||
...data,
|
||||
bans: data.bans.filter(
|
||||
(y) => y._id.user !== x._id.user,
|
||||
),
|
||||
});
|
||||
}}
|
||||
disabled={deleting.indexOf(x._id.user) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{data && (
|
||||
<div className={styles.virtual}>
|
||||
<Virtuoso
|
||||
totalCount={data.bans.length}
|
||||
itemContent={(index) => (
|
||||
<Inner
|
||||
key={data.bans[index]._id.user}
|
||||
server={server}
|
||||
users={data.users}
|
||||
ban={data.bans[index]}
|
||||
removeSelf={() => {
|
||||
setData({
|
||||
bans: data.bans.filter(
|
||||
(y) =>
|
||||
y._id.user !==
|
||||
data.bans[index]._id.user,
|
||||
),
|
||||
users: data.users,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,35 +1,61 @@
|
||||
import { XCircle } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ServerInvite } from "revolt-api/types/Invites";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Invite, ServerInvite } from "revolt-api/types/Invites";
|
||||
import { Server } from "revolt.js/dist/maps/Servers";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
import { getChannelName } from "../../../context/revoltjs/util";
|
||||
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import { Username } from "../../../components/common/user/UserShort";
|
||||
import IconButton from "../../../components/ui/IconButton";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
interface InnerProps {
|
||||
invite: Invite;
|
||||
server: Server;
|
||||
removeSelf: () => void;
|
||||
}
|
||||
|
||||
const Inner = observer(({ invite, server, removeSelf }: InnerProps) => {
|
||||
const [deleting, setDelete] = useState(false);
|
||||
|
||||
const user = server.client.users.get(invite.creator);
|
||||
const channel = server.client.channels.get(invite.channel);
|
||||
|
||||
return (
|
||||
<div className={styles.invite} data-deleting={deleting}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={user} size={24} />{" "}
|
||||
<Username user={user} showServerIdentity="both" />
|
||||
</span>
|
||||
<span>{channel ? getChannelName(channel, true) : "#??"}</span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDelete(true);
|
||||
server.client.deleteInvite(invite._id).then(removeSelf);
|
||||
}}
|
||||
disabled={deleting}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
export const Invites = observer(({ server }: Props) => {
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
export const Invites = ({ server }: Props) => {
|
||||
const [invites, setInvites] = useState<ServerInvite[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const client = useClient();
|
||||
const users = invites?.map((invite) => client.users.get(invite.creator));
|
||||
const channels = invites?.map((invite) =>
|
||||
client.channels.get(invite.channel),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
server.fetchInvites().then(setInvites);
|
||||
}, [server, setInvites]);
|
||||
@@ -51,45 +77,27 @@ export const Invites = observer(({ server }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
{typeof invites === "undefined" && <Preloader type="ring" />}
|
||||
{invites?.map((invite, index) => {
|
||||
const creator = users![index];
|
||||
const channel = channels![index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={invite._id}
|
||||
className={styles.invite}
|
||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={creator} size={24} />{" "}
|
||||
{creator?.username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{channel && creator
|
||||
? getChannelName(channel, true)
|
||||
: "#??"}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, invite._id]);
|
||||
|
||||
await client.deleteInvite(invite._id);
|
||||
|
||||
setInvites(
|
||||
invites?.filter(
|
||||
(x) => x._id !== invite._id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={deleting.indexOf(invite._id) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{invites && (
|
||||
<div className={styles.virtual}>
|
||||
<Virtuoso
|
||||
totalCount={invites.length}
|
||||
itemContent={(index) => (
|
||||
<Inner
|
||||
key={invites[index]._id}
|
||||
invite={invites[index]}
|
||||
server={server}
|
||||
removeSelf={() =>
|
||||
setInvites(
|
||||
invites.filter(
|
||||
(x) => x._id !== invites[index]._id,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,139 +1,135 @@
|
||||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||
import { isEqual } from "lodash";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Member } from "revolt.js/dist/maps/Members";
|
||||
import { Server } from "revolt.js/dist/maps/Servers";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import { Username } from "../../../components/common/user/UserShort";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import IconButton from "../../../components/ui/IconButton";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
|
||||
interface InnerProps {
|
||||
member: Member;
|
||||
}
|
||||
|
||||
const Inner = observer(({ member }: InnerProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [roles, setRoles] = useState<string[]>(member.roles ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
setRoles(member.roles ?? []);
|
||||
}, [member.roles]);
|
||||
|
||||
const server_roles = member.server?.roles ?? {};
|
||||
const user = member.user;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={styles.member}
|
||||
data-open={open}
|
||||
onClick={() => setOpen(!open)}>
|
||||
<span>
|
||||
<UserIcon target={user} size={24} />{" "}
|
||||
<Username user={member.user} showServerIdentity="both" />
|
||||
</span>
|
||||
<IconButton className={styles.chevron}>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{open && (
|
||||
<div className={styles.memberView}>
|
||||
<Overline type="subtle">Roles</Overline>
|
||||
{Object.keys(server_roles).map((key) => {
|
||||
const role = server_roles[key];
|
||||
return (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={roles.includes(key) ?? false}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
setRoles([...roles, key]);
|
||||
} else {
|
||||
setRoles(
|
||||
roles.filter((x) => x !== key),
|
||||
);
|
||||
}
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: role.colour,
|
||||
}}>
|
||||
{role.name}
|
||||
</span>
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
compact
|
||||
disabled={isEqual(member.roles ?? [], roles)}
|
||||
onClick={() =>
|
||||
member.edit({
|
||||
roles,
|
||||
})
|
||||
}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
export const Members = observer(({ server }: Props) => {
|
||||
const [selected, setSelected] = useState<undefined | string>();
|
||||
const [data, setData] = useState<
|
||||
{ members: Member[]; users: User[] } | undefined
|
||||
>(undefined);
|
||||
export const Members = ({ server }: Props) => {
|
||||
const [data, setData] = useState<Member[] | undefined>(undefined);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
server.fetchMembers().then(setData);
|
||||
server
|
||||
.fetchMembers()
|
||||
.then((data) => data.members)
|
||||
.then(setData);
|
||||
}, [server, setData]);
|
||||
|
||||
const [roles, setRoles] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
setRoles(
|
||||
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
|
||||
);
|
||||
}
|
||||
}, [setRoles, selected, data]);
|
||||
const members = useMemo(
|
||||
() =>
|
||||
query
|
||||
? data?.filter((x) => x.user?.username.includes(query))
|
||||
: data,
|
||||
[data, query],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.userList}>
|
||||
<div className={styles.subtitle}>
|
||||
{data?.members.length ?? 0} Members
|
||||
</div>
|
||||
{data &&
|
||||
data.members.length > 0 &&
|
||||
data.members
|
||||
.map((member) => {
|
||||
return {
|
||||
member,
|
||||
user: data.users.find(
|
||||
(x) => x._id === member._id.user,
|
||||
),
|
||||
};
|
||||
})
|
||||
.map(({ member, user }) => (
|
||||
// @ts-expect-error brokey
|
||||
// eslint-disable-next-line react/jsx-no-undef
|
||||
<Fragment key={member._id.user}>
|
||||
<div
|
||||
className={styles.member}
|
||||
data-open={selected === member._id.user}
|
||||
onClick={() =>
|
||||
setSelected(
|
||||
selected === member._id.user
|
||||
? undefined
|
||||
: member._id.user,
|
||||
)
|
||||
}>
|
||||
<span>
|
||||
<UserIcon target={user} size={24} />{" "}
|
||||
{user?.username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
)}
|
||||
</span>
|
||||
<IconButton className={styles.chevron}>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{selected === member._id.user && (
|
||||
<div className={styles.memberView}>
|
||||
<Overline type="subtle">Roles</Overline>
|
||||
{Object.keys(server.roles ?? {}).map(
|
||||
(key) => {
|
||||
const role = server.roles![key];
|
||||
return (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={
|
||||
roles.includes(key) ??
|
||||
false
|
||||
}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
setRoles([
|
||||
...roles,
|
||||
key,
|
||||
]);
|
||||
} else {
|
||||
setRoles(
|
||||
roles.filter(
|
||||
(x) =>
|
||||
x !==
|
||||
key,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: role.colour,
|
||||
}}>
|
||||
{role.name}
|
||||
</span>
|
||||
</Checkbox>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<Button
|
||||
compact
|
||||
disabled={isEqual(
|
||||
member.roles ?? [],
|
||||
roles,
|
||||
)}
|
||||
onClick={() =>
|
||||
member.edit({
|
||||
roles,
|
||||
})
|
||||
}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
<InputBox
|
||||
placeholder="Search for a specific user..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||
contrast
|
||||
/>
|
||||
<div className={styles.subtitle}>{data?.length ?? 0} Members</div>
|
||||
{members && (
|
||||
<div className={styles.virtual}>
|
||||
<Virtuoso
|
||||
totalCount={members.length}
|
||||
itemContent={(index) => (
|
||||
<Inner member={members[index]} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
.userList {
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -53,6 +54,10 @@
|
||||
span,
|
||||
code {
|
||||
flex: 1;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -85,9 +90,12 @@
|
||||
|
||||
.memberView {
|
||||
padding: 10px;
|
||||
margin: 0 10px;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.virtual {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.roles {
|
||||
|
||||
Reference in New Issue
Block a user