stoat-for-desktop/components/modal/modals/AddBot.tsx

223 lines
5.9 KiB
TypeScript

import { createFormControl, createFormGroup } from "solid-forms";
import { Show, createMemo, createSignal } from "solid-js";
import { Trans, useLingui } from "@lingui-solid/solid/macro";
import { Server } from "stoat.js";
import { cva } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { decodeTime } from "ulid";
import { useClient } from "@revolt/client";
import { Markdown } from "@revolt/markdown";
import {
Avatar,
Column,
Dialog,
DialogProps,
Form2,
Ripple,
Text,
TextField,
Time,
} from "@revolt/ui";
import { useModals } from "..";
import { Modals } from "../types";
/**
* Modal to join a server
*/
export function AddBotModal(props: DialogProps & Modals & { type: "add_bot" }) {
const { t } = useLingui();
const client = useClient();
const { showError } = useModals();
const [filter, setFilter] = createSignal("");
const filterLowercase = createMemo(() => filter().toLowerCase());
const availableGroups = createMemo(() => {
const instance = client();
return [
...instance.servers
.filter((server) => server.havePermission("ManageServer"))
// this won't always work, but we might as well try:
.filter((server) => !server.getMember(props.invite.id)),
...instance.channels
.filter((channel) => channel.type === "Group")
.filter((channel) => !channel.recipientIds.has(props.invite.id)),
]
.filter((group) => group.name.toLowerCase().includes(filterLowercase()))
.toSorted((a, b) => a.name.localeCompare(b.name))
.map((item) => ({ item, value: item.id }));
});
const group = createFormGroup({
id: createFormControl([] as string[], {
required: true,
}),
});
async function onSubmit() {
try {
const entry = availableGroups().find(
(entry) => entry.value === group.controls.id.value[0],
)!;
if (entry.item instanceof Server) {
await props.invite.addToServer(entry.item);
} else {
await props.invite.addToGroup(entry.item);
}
props.onClose();
} catch (error) {
showError(error);
}
}
const submit = Form2.useSubmitHandler(group, onSubmit);
return (
<Dialog
show={props.show}
onClose={props.onClose}
actions={[
{ text: <Trans>Cancel</Trans> },
{
text: <Trans>Add</Trans>,
onClick: () => {
onSubmit(); // doesn't go through submitHandler!
// much like other modals don't either
return false;
},
isDisabled: !Form2.canSubmit(group),
},
]}
isDisabled={group.isPending}
>
<form onSubmit={submit}>
<Column>
<Column align>
<Avatar
size={64}
src={props.invite.avatar?.originalUrl} // todo: correct URL
fallback={props.invite.username}
/>
<Text class="title">{props.invite.username}</Text>
<Text class="label">
<Trans>
Registered since{" "}
<Time format="calendar" value={decodeTime(props.invite.id)} />
</Trans>
</Text>
</Column>
<Show when={props.invite.description}>
<div use:scrollable={{ class: description() }}>
<Markdown content={props.invite.description} />
<ProvidedBy>
<Text class="label" size="small">
<Trans>Description provided by {props.invite.username}</Trans>
</Text>
<CoverText>
<div />
</CoverText>
</ProvidedBy>
</div>
</Show>
<TextField
value={filter()}
variant="filled"
placeholder={t`Search for groups...`}
onKeyUp={(e) => setFilter(e.currentTarget.value)}
/>
<Form2.VirtualSelect
control={group.controls.id}
items={availableGroups()}
selectHeight="240px"
>
{(item, selected) => (
<Item selected={selected}>
<Ripple />
<Avatar
src={item.animatedIconURL}
fallback={item.name}
size={24}
shape={item instanceof Server ? "circle" : "rounded-square"}
/>{" "}
<span>{item.name}</span>
</Item>
)}
</Form2.VirtualSelect>
<Column gap="sm" align>
<Text class="label" size="small">
Bots are not verified by Stoat.
</Text>
<Text class="label" size="small">
The bot will not be granted any permissions.
</Text>
</Column>
</Column>
</form>
</Dialog>
);
}
const description = cva({
base: {
position: "relative",
maxHeight: "120px",
padding: "var(--gap-md)",
borderRadius: "var(--borderRadius-lg)",
color: "var(--md-sys-color-on-secondary-container)",
background: "var(--md-sys-color-secondary-container)",
},
});
const ProvidedBy = styled("div", {
base: {
bottom: 0,
position: "sticky",
background: "var(--md-sys-color-secondary-container)",
},
});
const CoverText = styled("div", {
base: {
position: "relative",
"& *": {
top: 0,
width: "100%",
position: "absolute",
height: "var(--gap-md)",
background: "var(--md-sys-color-secondary-container)",
},
},
});
const Item = styled("div", {
base: {
height: "40px",
display: "flex",
position: "relative",
alignItems: "center",
gap: "var(--gap-md)",
padding: "var(--gap-md)",
borderRadius: "var(--borderRadius-sm)",
},
variants: {
selected: {
true: {
color: "var(--md-sys-color-on-primary)",
background: "var(--md-sys-color-primary)",
},
},
},
});