handmade-revolt/src/pages/settings/panes/MyBots.tsx

599 lines
23 KiB
TypeScript

// ! FIXME: this code is garbage, need to replace
import { Key, Clipboard, Globe, Plus } from "@styled-icons/boxicons-regular";
import { LockAlt, HelpCircle } from "@styled-icons/boxicons-solid";
import type { AxiosError } from "axios";
import { observer } from "mobx-react-lite";
import { API, User } from "revolt.js";
import styled from "styled-components/macro";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks";
import {
Button,
CategoryButton,
Checkbox,
InputBox,
Tip,
} from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { internalEmit } from "../../../lib/eventEmitter";
import { useTranslation } from "../../../lib/i18n";
import { stopPropagation } from "../../../lib/stopPropagation";
import AutoComplete, {
useAutoComplete,
} from "../../../components/common/AutoComplete";
import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon";
import { useClient } from "../../../controllers/client/ClientController";
import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads";
import { modalController } from "../../../controllers/modals/ModalController";
interface Data {
_id: string;
token: string;
username: string;
public: boolean;
interactions_url?: string;
}
interface Changes {
name?: string;
public?: boolean;
interactions_url?: string;
remove?: "InteractionsURL"[];
}
const BotBadge = styled.div`
display: inline-block;
flex-shrink: 0;
height: 1.3em;
padding: 0px 4px;
font-size: 0.7em;
user-select: none;
margin-inline-start: 2px;
text-transform: uppercase;
color: var(--accent-contrast);
background: var(--accent);
border-radius: calc(var(--border-radius) / 2);
`;
interface Props {
bot: API.Bot;
onDelete(): void;
onUpdate(changes: Changes): void;
}
function BotCard({ bot, onDelete, onUpdate }: Props) {
const client = useClient();
const translate = useTranslation();
const [user, setUser] = useState<User>(client.users.get(bot._id)!);
const [data, setData] = useState<Data>({
_id: bot._id,
token: bot.token,
username: user.username,
public: bot.public,
interactions_url: bot.interactions_url as any,
});
const [error, setError] = useState<string | JSX.Element>("");
const [saving, setSaving] = useState(false);
const [editMode, setEditMode] = useState(false);
const [usernameRef, setUsernameRef] = useState<HTMLInputElement | null>(
null,
);
const [interactionsRef, setInteractionsRef] =
useState<HTMLInputElement | null>(null);
const [profile, setProfile] = useState<undefined | API.UserProfile>(
undefined,
);
const refreshProfile = useCallback(() => {
client.api
.get(`/users/${bot._id as ""}/profile`, undefined, {
headers: { "x-bot-token": data.token },
})
.then((profile) => setProfile(profile ?? {}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, setProfile]);
useEffect(() => {
if (profile === undefined && editMode) refreshProfile();
}, [profile, editMode, refreshProfile]);
const [changed, setChanged] = useState(false);
function setContent(content?: string) {
setProfile({ ...profile, content });
if (!changed) setChanged(true);
}
async function save() {
const changes: Changes = {};
if (data.username !== user!.username) changes.name = data.username;
if (data.public !== bot.public) changes.public = data.public;
if (data.interactions_url === "") changes.remove = ["InteractionsURL"];
else if (data.interactions_url !== bot.interactions_url)
changes.interactions_url = data.interactions_url;
setSaving(true);
setError("");
try {
if (Object.keys(changes).length > 0)
await client.bots.edit(bot._id, changes);
if (changed) await editBotContent(profile?.content ?? undefined);
onUpdate(changes);
setChanged(false);
setEditMode(false);
} catch (e) {
const err = e as AxiosError;
if (err.isAxiosError && err.response?.data?.type) {
switch (err.response.data.type) {
case "UsernameTaken":
setError("That username is taken!");
break;
default:
setError(`Error: ${err.response.data.type}`);
break;
}
} else setError(err.toString());
}
setSaving(false);
}
async function editBotAvatar(avatar?: string) {
setSaving(true);
setError("");
await client.api.patch(
"/users/@me",
avatar ? { avatar } : { remove: ["Avatar"] },
{
headers: { "x-bot-token": data.token },
},
);
const res = await client.bots.fetch(bot._id);
if (!avatar) res.user.update({}, ["Avatar"]);
setUser(res.user);
setSaving(false);
}
async function editBotBackground(background?: string) {
setSaving(true);
setError("");
await client.api.patch(
"/users/@me",
background
? { profile: { background } }
: { remove: ["ProfileBackground"] },
{
headers: { "x-bot-token": data.token },
},
);
if (!background) setProfile({ ...profile, background: undefined });
else refreshProfile();
setSaving(false);
}
async function editBotContent(content?: string) {
setSaving(true);
setError("");
await client.api.patch(
"/users/@me",
content ? { profile: { content } } : { remove: ["ProfileContent"] },
{
headers: { "x-bot-token": data.token },
},
);
if (!content) setProfile({ ...profile, content: undefined });
else refreshProfile();
setSaving(false);
}
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete(setContent, {
users: { type: "all" },
});
return (
<div key={bot._id} className={styles.botCard}>
<div className={styles.infocontainer}>
<div className={styles.infoheader}>
<div className={styles.container}>
{!editMode ? (
<UserIcon
className={styles.avatar}
target={user}
size={42}
onClick={() =>
modalController.push({
type: "user_profile",
user_id: user._id,
})
}
/>
) : (
<FileUploader
width={42}
height={42}
style="icon"
fileType="avatars"
behaviour="upload"
maxFileSize={4_000_000}
onUpload={(avatar) => editBotAvatar(avatar)}
remove={() => editBotAvatar()}
defaultPreview={user.generateAvatarURL(
{ max_side: 256 },
true,
)}
previewURL={user.generateAvatarURL(
{ max_side: 256 },
true,
)}
/>
)}
{!editMode ? (
<div className={styles.userDetail}>
<div className={styles.userName}>
{user!.username}{" "}
<BotBadge>
<Text id="app.main.channel.bot" />
</BotBadge>
</div>
<div className={styles.userid}>
<Tooltip
content={
<Text id="app.settings.pages.bots.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip
content={
<Text id="app.special.copy" />
}>
<a
onClick={() =>
modalController.writeText(
user!._id,
)
}>
{user!._id}
</a>
</Tooltip>
</div>
</div>
) : (
<InputBox
style={{ width: "100%" }}
ref={setUsernameRef}
value={data.username}
disabled={saving}
onChange={(e) =>
setData({
...data,
username: e.currentTarget.value,
})
}
/>
)}
</div>
{!editMode && (
<Tooltip
content={
<Text
id={`app.settings.pages.bots.${
bot.public ? "public" : "private"
}_bot_tip`}
/>
}>
{bot.public ? (
<Globe size={24} />
) : (
<LockAlt size={24} />
)}
</Tooltip>
)}
</div>
<Button
disabled={saving}
onClick={() => {
if (editMode) {
setData({
_id: bot._id,
token: data.token,
username: user!.username,
public: bot.public,
interactions_url: bot.interactions_url as any,
});
usernameRef!.value = user!.username;
interactionsRef!.value = bot.interactions_url || "";
setError("");
setEditMode(false);
} else setEditMode(true);
}}
palette="secondary">
<Text
id={`app.special.modals.actions.${
editMode ? "cancel" : "edit"
}`}
/>
</Button>
</div>
{!editMode && (
<CategoryButton
account
icon={<Key size={24} />}
onClick={() => modalController.writeText(data.token)}
description={
<>
{"••••• "}
<a
onClick={(ev) =>
stopPropagation(
ev,
modalController.push({
type: "show_token",
token: data.token,
name: user!.username,
}),
)
}>
<Text id="app.special.modals.actions.reveal" />
</a>
</>
}
action={<Clipboard size={18} />}>
<Text id="app.settings.pages.bots.token" />
</CategoryButton>
)}
{editMode && (
<div className={styles.botSection}>
<CollapsibleSection
defaultValue={false}
id={`bot_profile_${bot._id}`}
summary={<Text id="app.settings.pages.bots.profile" />}>
<h3>
<Text id="app.settings.pages.profile.custom_background" />
</h3>
<FileUploader
height={92}
style="banner"
behaviour="upload"
fileType="backgrounds"
maxFileSize={6_000_000}
onUpload={(background) =>
editBotBackground(background)
}
remove={() => editBotBackground()}
previewURL={
profile?.background
? client.generateFileURL(
profile.background,
{ width: 1000 },
true,
)
: undefined
}
/>
<h3>
<Text id="app.settings.pages.profile.info" />
</h3>
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
maxRows={10}
minHeight={200}
maxLength={2000}
value={profile?.content ?? ""}
disabled={typeof profile === "undefined"}
onChange={(ev) => {
onChange(ev);
setContent(ev.currentTarget.value);
}}
placeholder={translate(
`app.settings.pages.profile.${
typeof profile === "undefined"
? "fetching"
: "placeholder"
}`,
)}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
/>
</CollapsibleSection>
<Checkbox
value={data.public}
disabled={saving}
title={<Text id="app.settings.pages.bots.public_bot" />}
description={
<Text id="app.settings.pages.bots.public_bot_desc" />
}
onChange={(v) => setData({ ...data, public: v })}
/>
<h3>
<Text id="app.settings.pages.bots.interactions_url" />
</h3>
<h5>
<Text id="app.settings.pages.bots.reserved" />
</h5>
<InputBox
palette="secondary"
ref={setInteractionsRef}
value={data.interactions_url}
disabled={saving}
onChange={(e) =>
setData({
...data,
interactions_url: e.currentTarget.value,
})
}
/>
</div>
)}
{error && (
<div className={styles.botSection}>
<Tip palette="error">{error}</Tip>
</div>
)}
<div className={styles.buttonRow}>
{editMode && (
<>
<Button onClick={save}>
<Text id="app.special.modals.actions.save" />
</Button>
<Button
palette="error"
onClick={async () => {
setSaving(true);
modalController.push({
type: "delete_bot",
target: bot._id,
name: user.username,
cb: onDelete,
});
}}>
<Text id="app.special.modals.actions.delete" />
</Button>
</>
)}
{!editMode && (
<>
<Button
onClick={() =>
modalController.writeText(
`${window.origin}/bot/${bot._id}`,
)
}>
<Text id="app.settings.pages.bots.copy_invite" />
</Button>
<Button
onClick={() =>
internalEmit(
"Intermediate",
"navigate",
`/bot/${bot._id}`,
)
}>
<Text id="app.settings.pages.bots.add" />
</Button>
<Button
palette="error"
onClick={ async () => {
modalController.push({
type: "reset_bot_token",
target: { name: user.username, id: bot._id },
callback: async () => {
const updatedBot = await client.bots.fetch(bot._id);
setData({
...data,
token: updatedBot.bot.token
})
}
})
}
}>
<Text id="app.settings.pages.bots.reset_token" />
</Button>
</>
)}
</div>
</div>
);
}
export const MyBots = observer(() => {
const client = useClient();
const [bots, setBots] = useState<API.Bot[] | undefined>(undefined);
useEffect(() => {
client.bots.fetchOwned().then(({ bots }) => setBots(bots));
// eslint-disable-next-line
}, []);
return (
<div className={styles.myBots}>
<CategoryButton
account
icon={<Plus size={24} />}
onClick={() =>
modalController.push({
type: "create_bot",
onCreate: (bot) => setBots([...(bots ?? []), bot]),
})
}
action="chevron">
<Text id="app.settings.pages.bots.create_bot" />
</CategoryButton>
<h5>
By creating a bot, you are agreeing to the {` `}
<a
href="https://revolt.chat/aup"
target="_blank"
rel="noreferrer">
Acceptable Usage Policy
</a>
.
</h5>
<hr />
<h3>
<Text id="app.settings.pages.bots.title" />
</h3>
<div className={styles.botList}>
{bots?.map((bot) => {
return (
<BotCard
key={bot._id}
bot={bot}
onDelete={() =>
setBots(bots.filter((x) => x._id !== bot._id))
}
onUpdate={(changes: Changes) =>
setBots(
bots.map((x) => {
if (x._id === bot._id) {
if (
"public" in changes &&
typeof changes.public ===
"boolean"
)
x.public = changes.public;
if ("interactions_url" in changes)
x.interactions_url =
changes.interactions_url;
if (
changes.remove?.includes(
"InteractionsURL",
)
)
x.interactions_url = undefined;
}
return x;
}),
)
}
/>
);
})}
</div>
</div>
);
});