Compare commits

...

2 Commits

12 changed files with 214 additions and 134 deletions

2
go.mod
View File

@ -2,7 +2,7 @@ module github.com/superseriousbusiness/gotosocial
go 1.22
replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround
replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.9-concurrency-workaround
toolchain go1.22.2

6
go.sum
View File

@ -416,8 +416,6 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
@ -631,8 +629,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround h1:ESobxED9bfE0nOQP/WPv9+tMR8oZoDIWRKlNK2Vs4Ms=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.9-concurrency-workaround h1:gFAlklid3jyXIuZBy5Vy0dhG+F6YBgosRy4syT5CDsg=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.9-concurrency-workaround/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@ -228935,4 +228935,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor

View File

@ -228490,4 +228490,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor

View File

@ -701,7 +701,7 @@ type tx struct {
c *conn
}
func newTx(c *conn, opts driver.TxOptions) (*tx, error) {
func newTx(ctx context.Context, c *conn, opts driver.TxOptions) (*tx, error) {
r := &tx{c: c}
sql := "begin"
@ -709,7 +709,7 @@ func newTx(c *conn, opts driver.TxOptions) (*tx, error) {
sql = "begin " + c.beginMode
}
if err := r.exec(context.Background(), sql); err != nil {
if err := r.exec(ctx, sql); err != nil {
return nil, err
}
@ -1348,7 +1348,7 @@ func (c *conn) Begin() (dt driver.Tx, err error) {
}
func (c *conn) begin(ctx context.Context, opts driver.TxOptions) (t driver.Tx, err error) {
return newTx(c, opts)
return newTx(ctx, c, opts)
}
// Close invalidates and potentially stops any current prepared statements and

4
vendor/modules.txt vendored
View File

@ -1268,7 +1268,7 @@ modernc.org/mathutil
# modernc.org/memory v1.8.0
## explicit; go 1.18
modernc.org/memory
# modernc.org/sqlite v1.29.8 => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround
# modernc.org/sqlite v1.29.8 => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.9-concurrency-workaround
## explicit; go 1.20
modernc.org/sqlite
modernc.org/sqlite/lib
@ -1281,4 +1281,4 @@ modernc.org/token
# mvdan.cc/xurls/v2 v2.5.0
## explicit; go 1.19
mvdan.cc/xurls/v2
# modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround
# modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.9-concurrency-workaround

View File

@ -17,7 +17,9 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { SerializedError } from "@reduxjs/toolkit";
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import React, { ReactNode } from "react";
function ErrorFallback({ error, resetErrorBoundary }) {
return (
@ -44,39 +46,70 @@ function ErrorFallback({ error, resetErrorBoundary }) {
);
}
function Error({ error }) {
/* eslint-disable-next-line no-console */
console.error("Rendering error:", error);
let message;
interface GtsError {
/**
* Error message returned from the API.
*/
error: string;
if (error.data != undefined) { // RTK Query error with data
if (error.status) {
message = (<>
<b>{error.status}:</b> {error.data.error}
{error.data.error_description &&
<p>
{error.data.error_description}
</p>
}
</>);
} else {
message = error.data.error;
}
} else if (error.name != undefined || error.type != undefined) { // JS error
message = (<>
<b>{error.type && error.name}:</b> {error.message}
</>);
} else if (error.status && typeof error.error == "string") {
message = (<>
<b>{error.status}:</b> {error.error}
</>);
/**
* For OAuth errors: description of the error.
*/
error_description?: string;
}
interface ErrorProps {
error: FetchBaseQueryError | SerializedError | Error | undefined;
/**
* Optional function to clear the error.
* If provided, rendered error will have
* a "dismiss" button.
*/
reset?: () => void;
}
function Error({ error, reset }: ErrorProps) {
if (error === undefined) {
return null;
}
/* eslint-disable-next-line no-console */
console.error("caught error: ", error);
let message: ReactNode;
if ("status" in error) {
// RTK Query error with data.
const gtsError = error.data as GtsError;
const errMsg = gtsError.error_description ?? gtsError.error;
message = <>Code {error.status} {errMsg}</>;
} else {
message = error.message ?? error;
// SerializedError or Error.
const errMsg = error.message ?? JSON.stringify(error);
message = (
<>{error.name && `${error.name}: `}{errMsg}</>
);
}
let className = "error";
if (reset) {
className += " with-dismiss";
}
return (
<div className="error">
{message}
<div className={className}>
<span>{message}</span>
{ reset &&
<span
className="dismiss"
onClick={reset}
role="button"
tabIndex={0}
>
<span>Dismiss</span>
<i className="fa fa-fw fa-close" aria-hidden="true" />
</span>
}
</div>
);
}

View File

@ -29,6 +29,7 @@ import type {
RadioFormInputHook,
TextFormInputHook,
} from "../../lib/form/types";
import { nanoid } from "nanoid";
export interface TextInputProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
@ -92,22 +93,25 @@ export interface FileInputProps extends React.DetailedHTMLProps<
export function FileInput({ label, field, ...props }: FileInputProps) {
const { onChange, ref, infoComponent } = field;
const id = nanoid();
return (
<div className="form-field file">
<label>
<div className="label">{label}</div>
<div className="file-input button">Browse</div>
{infoComponent}
{/* <a onClick={removeFile("header")}>remove</a> */}
<input
type="file"
className="hidden"
onChange={onChange}
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
{...props}
/>
<label className="label-label" htmlFor={id}>
{label}
</label>
<label className="label-button" htmlFor={id}>
<div className="file-input button">Browse</div>
</label>
<input
id={id}
type="file"
className="hidden"
onChange={onChange}
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
{...props}
/>
{infoComponent}
</div>
);
}

View File

@ -51,9 +51,9 @@ export default function MutationButton({
}
return (
<div className={wrapperClassName}>
<div className={wrapperClassName ? wrapperClassName : "mutation-button"}>
{(showError && targetsThisButton && result.error) &&
<Error error={result.error} />
<Error error={result.error} reset={result.reset} />
}
<button
type="submit"

View File

@ -27,6 +27,7 @@ import type {
HookOpts,
FileFormInputHook,
} from "./types";
import { Error as ErrorC } from "../../components/error";
const _default = undefined;
export default function useFileInput(
@ -41,6 +42,15 @@ export default function useFileInput(
const [imageURL, setImageURL] = useState<string>();
const [info, setInfo] = useState<React.JSX.Element>();
function reset() {
if (imageURL) {
URL.revokeObjectURL(imageURL);
}
setImageURL(undefined);
setFile(undefined);
setInfo(undefined);
}
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const files = e.target.files;
if (!files) {
@ -59,25 +69,18 @@ export default function useFileInput(
setImageURL(URL.createObjectURL(file));
}
let size = prettierBytes(file.size);
const sizePrettier = prettierBytes(file.size);
if (maxSize && file.size > maxSize) {
size = <span className="error-text">{size}</span>;
const maxSizePrettier = prettierBytes(maxSize);
setInfo(
<ErrorC
error={new Error(`file size ${sizePrettier} is larger than max size ${maxSizePrettier}`)}
reset={(reset)}
/>
);
} else {
setInfo(<>{file.name} ({sizePrettier})</>);
}
setInfo(
<>
{file.name} ({size})
</>
);
}
function reset() {
if (imageURL) {
URL.revokeObjectURL(imageURL);
}
setImageURL(undefined);
setFile(undefined);
setInfo(undefined);
}
const infoComponent = (

View File

@ -257,33 +257,37 @@ input, select, textarea {
overflow: auto;
margin: 0;
}
&.with-dismiss {
display: flex;
gap: 1rem;
justify-content: space-between;
align-items: center;
align-items: center;
flex-wrap: wrap;
align-items: center;
flex-wrap: wrap;
.dismiss {
display: flex;
flex-shrink: 0;
align-items: center;
align-self: stretch;
gap: 0.25rem;
}
}
}
.mutation-button {
display: flex;
flex-direction: column;
gap: 1rem;
}
.hidden {
display: none;
}
.messagebutton, .messagebutton > div {
display: flex;
align-items: center;
flex-wrap: wrap;
div.padded {
margin-left: 1rem;
}
button, .button {
white-space: nowrap;
margin-right: 1rem;
}
}
.messagebutton > div {
button, .button {
margin-top: 1rem;
}
}
.notImplemented {
border: 2px solid rgb(70, 79, 88);
background: repeating-linear-gradient(
@ -500,12 +504,29 @@ form {
font-weight: bold;
}
.form-field.file label {
.form-field.file {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
grid-template-areas:
"label-label label-label"
"label-button file-info"
;
.label-label {
grid-area: label-label;
}
.label {
grid-column: 1 / span 2;
.label-button {
grid-area: label-button;
}
.form-info {
grid-area: file-info;
.error {
padding: 0.1rem;
line-height: 1.4rem;
}
}
}

View File

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo, useEffect } from "react";
import React, { useMemo, useEffect, ReactNode } from "react";
import { useFileInput, useComboBoxInput } from "../../../../lib/form";
import useShortcode from "./use-shortcode";
import useFormSubmit from "../../../../lib/form/submit";
@ -27,52 +27,74 @@ 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/gts-api";
import prettierBytes from "prettier-bytes";
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 prettierMaxSize = useMemo(() => {
return prettierBytes(emojiMaxSize);
}, [emojiMaxSize]);
const category = useComboBoxInput("category");
const form = {
shortcode: useShortcode(),
image: useFileInput("image", {
withPreview: true,
maxSize: emojiMaxSize
}),
category: useComboBoxInput("category"),
};
const [submitForm, result] = useFormSubmit({
shortcode, image, category
}, useAddEmojiMutation());
const [submitForm, result] = useFormSubmit(
form,
useAddEmojiMutation(),
{
changedOnly: false,
// On submission, reset form values
// no matter what the result was.
onFinish: (_res) => {
form.shortcode.reset();
form.image.reset();
form.category.reset();
}
},
);
useEffect(() => {
if (shortcode.value === undefined || shortcode.value.length == 0) {
if (image.value != undefined) {
let [name, _ext] = image.value.name.split(".");
shortcode.setter(name);
}
// If shortcode has not been entered yet, but an image file
// has been submitted, suggest a shortcode based on filename.
if (
(form.shortcode.value === undefined || form.shortcode.value.length === 0) &&
form.image.value !== undefined
) {
let [name, _ext] = form.image.value.name.split(".");
form.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]);
// 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
}, [form.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}:`;
let emojiOrShortcode: ReactNode;
if (form.image.previewValue !== undefined) {
emojiOrShortcode = (
<img
className="emoji"
src={form.image.previewValue}
title={`:${form.shortcode.value}:`}
alt={form.shortcode.value}
/>
);
} else if (form.shortcode.value !== undefined && form.shortcode.value.length > 0) {
emojiOrShortcode = `:${form.shortcode.value}:`;
} else {
emojiOrShortcode = `:your_emoji_here:`;
}
@ -87,22 +109,23 @@ export default function NewEmojiForm() {
<form onSubmit={submitForm} className="form-flex">
<FileInput
field={image}
field={form.image}
label={`Image file: png, gif, or static webp; max size ${prettierMaxSize}`}
accept="image/png,image/gif,image/webp"
/>
<TextInput
field={shortcode}
field={form.shortcode}
label="Shortcode, must be unique among the instance's local emoji"
{...{pattern: "^\\w{2,30}$"}}
/>
<CategorySelect
field={category}
children={[]}
field={form.category}
/>
<MutationButton
disabled={image.previewValue === undefined}
disabled={form.image.previewValue === undefined || form.shortcode.value?.length === 0}
label="Upload emoji"
result={result}
/>