Virtualise server settings user lists. Closes #300

This commit is contained in:
Paul
2021-09-25 14:43:28 +01:00
parent 81379d6ec4
commit 920f78b650
8 changed files with 272 additions and 337 deletions

View File

@@ -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%;

View File

@@ -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>
);
});

View File

@@ -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>
);
});
};

View File

@@ -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>
);
});
};

View File

@@ -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 {