[chore] Refactor settings panel routing (and other fixes) (#2864)

This commit is contained in:
tobi 2024-04-24 12:12:47 +02:00 committed by GitHub
commit 7a1e639483
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1788 additions and 1445 deletions

View file

@ -0,0 +1,142 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect } from "react";
import { Redirect, useParams } from "wouter";
import { useComboBoxInput, useFileInput, useValue } from "../../../../lib/form";
import useFormSubmit from "../../../../lib/form/submit";
import { useBaseUrl } from "../../../../lib/navigation/util";
import FakeToot from "../../../../components/fake-toot";
import FormWithData from "../../../../lib/form/form-with-data";
import Loading from "../../../../components/loading";
import { FileInput } from "../../../../components/form/inputs";
import MutationButton from "../../../../components/form/mutation-button";
import { Error } from "../../../../components/error";
import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
import { CategorySelect } from "../category-select";
import BackButton from "../../../../components/back-button";
export default function EmojiDetail() {
const baseUrl = useBaseUrl();
const params = useParams();
return (
<div className="emoji-detail">
<BackButton to={`~${baseUrl}/local`} />
<FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
</div>
);
}
function EmojiDetailForm({ data: emoji }) {
const baseUrl = useBaseUrl();
const form = {
id: useValue("id", emoji.id),
category: useComboBoxInput("category", { source: emoji }),
image: useFileInput("image", {
withPreview: true,
maxSize: 50 * 1024 // TODO: get from instance api
})
};
const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation());
// Automatic submitting of category change
useEffect(() => {
if (
form.category.hasChanged() &&
!form.category.state.open &&
!form.category.isNew) {
modifyEmoji();
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
if (deleteResult.isSuccess) {
return <Redirect to={`~${baseUrl}/local`} />;
}
return (
<>
<div className="emoji-header">
<img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode} />
<div>
<h2>{emoji.shortcode}</h2>
<MutationButton
label="Delete"
type="button"
onClick={() => deleteEmoji(emoji.id)}
className="danger"
showError={false}
result={deleteResult}
disabled={false}
/>
</div>
</div>
<form onSubmit={modifyEmoji} className="left-border">
<h2>Modify this emoji {result.isLoading && <Loading />}</h2>
<div className="update-category">
<CategorySelect
field={form.category}
>
<MutationButton
name="create-category"
label="Create"
result={result}
showError={false}
style={{ visibility: (form.category.isNew ? "initial" : "hidden") }}
disabled={!form.category.value}
/>
</CategorySelect>
</div>
<div className="update-image">
<FileInput
field={form.image}
label="Image"
accept="image/png,image/gif"
/>
<MutationButton
name="image"
label="Replace image"
showError={false}
result={result}
disabled={!form.image.value}
/>
<FakeToot>
Look at this new custom emoji <img
className="emoji"
src={form.image.previewValue ?? emoji.url}
title={`:${emoji.shortcode}:`}
alt={emoji.shortcode}
/> isn&apos;t it cool?
</FakeToot>
{result.error && <Error error={result.error} />}
{deleteResult.error && <Error error={deleteResult.error} />}
</div>
</form>
</>
);
}

View file

@ -0,0 +1,112 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo, useEffect } from "react";
import { useFileInput, useComboBoxInput } from "../../../../lib/form";
import useShortcode from "./use-shortcode";
import useFormSubmit from "../../../../lib/form/submit";
import { TextInput, FileInput } from "../../../../components/form/inputs";
import { CategorySelect } from '../category-select';
import FakeToot from "../../../../components/fake-toot";
import MutationButton from "../../../../components/form/mutation-button";
import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
import { useInstanceV1Query } from "../../../../lib/query";
export default function NewEmojiForm() {
const shortcode = useShortcode();
const { data: instance } = useInstanceV1Query();
const emojiMaxSize = useMemo(() => {
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
}, [instance]);
const image = useFileInput("image", {
withPreview: true,
maxSize: emojiMaxSize
});
const category = useComboBoxInput("category");
const [submitForm, result] = useFormSubmit({
shortcode, image, category
}, useAddEmojiMutation());
useEffect(() => {
if (shortcode.value === undefined || shortcode.value.length == 0) {
if (image.value != undefined) {
let [name, _ext] = image.value.name.split(".");
shortcode.setter(name);
}
}
/* We explicitly don't want to have 'shortcode' as a dependency here
because we only want to change the shortcode to the filename if the field is empty
at the moment the file is selected, not some time after when the field is emptied
*/
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [image.value]);
let emojiOrShortcode;
if (image.previewValue != undefined) {
emojiOrShortcode = <img
className="emoji"
src={image.previewValue}
title={`:${shortcode.value}:`}
alt={shortcode.value}
/>;
} else if (shortcode.value !== undefined && shortcode.value.length > 0) {
emojiOrShortcode = `:${shortcode.value}:`;
} else {
emojiOrShortcode = `:your_emoji_here:`;
}
return (
<div>
<h2>Add new custom emoji</h2>
<FakeToot>
Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool?
</FakeToot>
<form onSubmit={submitForm} className="form-flex">
<FileInput
field={image}
accept="image/png,image/gif,image/webp"
/>
<TextInput
field={shortcode}
label="Shortcode, must be unique among the instance's local emoji"
/>
<CategorySelect
field={category}
children={[]}
/>
<MutationButton
disabled={image.previewValue === undefined}
label="Upload emoji"
result={result}
/>
</form>
</div>
);
}

View file

@ -0,0 +1,173 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo, useState } from "react";
import { Link } from "wouter";
import { matchSorter } from "match-sorter";
import NewEmojiForm from "./new-emoji";
import { useTextInput } from "../../../../lib/form";
import { useEmojiByCategory } from "../category-select";
import Loading from "../../../../components/loading";
import { Error } from "../../../../components/error";
import { TextInput } from "../../../../components/form/inputs";
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
import { CustomEmoji } from "../../../../lib/types/custom-emoji";
export function EmojiOverview() {
const { data: emoji = [], isLoading, isError, error } = useListEmojiQuery({ filter: "domain:local" });
let content: React.JSX.Element;
if (isLoading) {
content = <Loading />;
} else if (isError) {
content = <Error error={error} />;
} else {
content = (
<>
<EmojiList emoji={emoji} />
<NewEmojiForm />
</>
);
}
return (
<>
<h1>Local Custom Emoji</h1>
<p>
To use custom emoji in your toots they have to be 'local' to the instance.
You can either upload them here directly, or copy from those already
present on other (known) instances through the <Link to={`/remote`}>Remote Emoji</Link> page.
</p>
<p>
<strong>Be warned!</strong> If you upload more than about 300-400 custom emojis in
total on your instance, this may lead to rate-limiting issues for users and clients
if they try to load all the emoji images at once (which is what many clients do).
</p>
{content}
</>
);
}
interface EmojiListParams {
emoji: CustomEmoji[];
}
function EmojiList({ emoji }: EmojiListParams) {
const filterField = useTextInput("filter");
const filter = filterField.value ?? "";
const emojiByCategory = useEmojiByCategory(emoji);
// Filter emoji based on shortcode match
// with user input, hiding empty categories.
const { filteredEmojis, filteredCount } = useMemo(() => {
// Amount of emojis removed by the filter.
// Start with the length of the array since
// that's the max that can be filtered out.
let filteredCount = emoji.length;
// Results of the filtering.
const filteredEmojis: [string, CustomEmoji[]][] = [];
// Filter from emojis in this category.
emojiByCategory.forEach((entries, category) => {
const filteredEntries = matchSorter(entries, filter, {
keys: ["shortcode"]
});
if (filteredEntries.length == 0) {
// Nothing left in this category, don't
// bother adding it to filteredEmojis.
return;
}
filteredCount -= filteredEntries.length;
filteredEmojis.push([category, filteredEntries]);
});
return { filteredEmojis, filteredCount };
}, [filter, emojiByCategory, emoji.length]);
return (
<>
<h2>Overview</h2>
{emoji.length > 0
? <span>{emoji.length} custom emoji {filteredCount > 0 && `(${filteredCount} filtered)`}</span>
: <span>No custom emoji yet, you can add one below.</span>
}
<div className="list emoji-list">
<div className="header">
<TextInput
field={filterField}
name="emoji-shortcode"
placeholder="Search"
/>
</div>
<div className="entries scrolling">
{filteredEmojis.length > 0
? (
<div className="entries scrolling">
{filteredEmojis.map(([category, emojis]) => {
return <EmojiCategory key={category} category={category} emojis={emojis} />;
})}
</div>
)
: <div className="entry">No local emoji matched your filter.</div>
}
</div>
</div>
</>
);
}
interface EmojiCategoryProps {
category: string;
emojis: CustomEmoji[];
}
function EmojiCategory({ category, emojis }: EmojiCategoryProps) {
return (
<div className="entry">
<b>{category}</b>
<div className="emoji-group">
{emojis.map((emoji) => {
return (
<Link key={emoji.id} to={`/local/${emoji.id}`} >
<EmojiPreview emoji={emoji} />
</Link>
);
})}
</div>
</div>
);
}
function EmojiPreview({ emoji }) {
const [ animate, setAnimate ] = useState(false);
return (
<img
onMouseEnter={() => { setAnimate(true); }}
onMouseLeave={() => { setAnimate(false); }}
src={animate ? emoji.url : emoji.static_url}
alt={emoji.shortcode}
title={emoji.shortcode}
loading="lazy"
/>
);
}

View file

@ -0,0 +1,56 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useMemo } from "react";
import { useTextInput } from "../../../../lib/form";
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
const shortcodeRegex = /^\w{2,30}$/;
export default function useShortcode() {
const { data: emoji = [] } = useListEmojiQuery({
filter: "domain:local"
});
const emojiCodes = useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
}, [emoji]);
return useTextInput("shortcode", {
validator: function validateShortcode(code) {
// technically invalid, but hacky fix to prevent validation error on page load
if (code == "") { return ""; }
if (emojiCodes.has(code)) {
return "Shortcode already in use";
}
if (code.length < 2 || code.length > 30) {
return "Shortcode must be between 2 and 30 characters";
}
if (!shortcodeRegex.test(code)) {
return "Shortcode must only contain letters, numbers, and underscores";
}
return "";
}
});
}