mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-25 17:53:15 +01:00
refactor custom-emoji, progress on federation bulk
This commit is contained in:
parent
59413c3482
commit
9b6a54032c
282
web/source/settings/admin/_federation.js
Normal file
282
web/source/settings/admin/_federation.js
Normal file
@ -0,0 +1,282 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const Redux = require("react-redux");
|
||||
const { Switch, Route, Link, Redirect, useRoute, useLocation } = require("wouter");
|
||||
const fileDownload = require("js-file-download");
|
||||
|
||||
const { formFields } = require("../components/form-fields");
|
||||
|
||||
const api = require("../lib/api");
|
||||
const adminActions = require("../redux/reducers/admin").actions;
|
||||
const submit = require("../lib/submit");
|
||||
const BackButton = require("../components/back-button");
|
||||
const Loading = require("../components/loading");
|
||||
const { matchSorter } = require("match-sorter");
|
||||
|
||||
const base = "/settings/admin/federation";
|
||||
|
||||
module.exports = function AdminSettings() {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!loadedBlockedInstances) {
|
||||
Promise.try(() => {
|
||||
return dispatch(api.admin.fetchDomainBlocks());
|
||||
});
|
||||
}
|
||||
}, [dispatch, loadedBlockedInstances]);
|
||||
|
||||
if (!loadedBlockedInstances) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Federation</h1>
|
||||
<div>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${base}/:domain`}>
|
||||
<InstancePageWrapped />
|
||||
</Route>
|
||||
<InstanceOverview />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
function InstanceOverview() {
|
||||
const [filter, setFilter] = React.useState("");
|
||||
const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances);
|
||||
const [_location, setLocation] = useLocation();
|
||||
|
||||
const filteredInstances = React.useMemo(() => {
|
||||
return matchSorter(Object.values(blockedInstances), filter, { keys: ["domain"] });
|
||||
}, [blockedInstances, filter]);
|
||||
|
||||
function filterFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLocation(`${base}/${filter}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Federation</h1>
|
||||
Here you can see an overview of blocked instances.
|
||||
|
||||
<div className="instance-list">
|
||||
<h2>Blocked instances</h2>
|
||||
<form action={`${base}/view`} className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||
<input name="domain" value={filter} onChange={(e) => setFilter(e.target.value)} />
|
||||
<Link to={`${base}/${filter}`}><a className="button">Add block</a></Link>
|
||||
</form>
|
||||
<div className="list scrolling">
|
||||
{filteredInstances.map((entry) => {
|
||||
return (
|
||||
<Link key={entry.domain} to={`${base}/${entry.domain}`}>
|
||||
<a className="entry nounderline">
|
||||
<span id="domain">
|
||||
{entry.domain}
|
||||
</span>
|
||||
<span id="date">
|
||||
{new Date(entry.created_at).toLocaleString()}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BulkBlocking />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock);
|
||||
function BulkBlocking() {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const { bulkBlock, blockedInstances } = Redux.useSelector(state => state.admin);
|
||||
|
||||
const [errorMsg, setError] = React.useState("");
|
||||
const [statusMsg, setStatus] = React.useState("");
|
||||
|
||||
function importBlocks() {
|
||||
setStatus("Processing");
|
||||
setError("");
|
||||
return Promise.try(() => {
|
||||
return dispatch(api.admin.bulkDomainBlock());
|
||||
}).then(({ success, invalidDomains }) => {
|
||||
return Promise.try(() => {
|
||||
return resetBulk();
|
||||
}).then(() => {
|
||||
dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")]));
|
||||
|
||||
let stat = "";
|
||||
if (success == 0) {
|
||||
return setError("No valid domains in import");
|
||||
} else if (success == 1) {
|
||||
stat = "Imported 1 domain";
|
||||
} else {
|
||||
stat = `Imported ${success} domains`;
|
||||
}
|
||||
|
||||
if (invalidDomains.length > 0) {
|
||||
if (invalidDomains.length == 1) {
|
||||
stat += ", input contained 1 invalid domain.";
|
||||
} else {
|
||||
stat += `, input contained ${invalidDomains.length} invalid domains.`;
|
||||
}
|
||||
} else {
|
||||
stat += "!";
|
||||
}
|
||||
|
||||
setStatus(stat);
|
||||
});
|
||||
}).catch((e) => {
|
||||
setError(e.message);
|
||||
setStatus("");
|
||||
});
|
||||
}
|
||||
|
||||
function exportBlocks() {
|
||||
return Promise.try(() => {
|
||||
setStatus("Exporting");
|
||||
setError("");
|
||||
let asJSON = bulkBlock.exportType.startsWith("json");
|
||||
let _asCSV = bulkBlock.exportType.startsWith("csv");
|
||||
|
||||
let exportList = Object.values(blockedInstances).map((entry) => {
|
||||
if (asJSON) {
|
||||
return {
|
||||
domain: entry.domain,
|
||||
public_comment: entry.public_comment
|
||||
};
|
||||
} else {
|
||||
return entry.domain;
|
||||
}
|
||||
});
|
||||
|
||||
if (bulkBlock.exportType == "json") {
|
||||
return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)]));
|
||||
} else if (bulkBlock.exportType == "json-download") {
|
||||
return fileDownload(JSON.stringify(exportList), "block-export.json");
|
||||
} else if (bulkBlock.exportType == "plain") {
|
||||
return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")]));
|
||||
}
|
||||
}).then(() => {
|
||||
setStatus("Exported!");
|
||||
}).catch((e) => {
|
||||
setError(e.message);
|
||||
setStatus("");
|
||||
});
|
||||
}
|
||||
|
||||
function resetBulk(e) {
|
||||
if (e != undefined) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return dispatch(adminActions.resetBulkBlockVal());
|
||||
}
|
||||
|
||||
function disableInfoFields(props = {}) {
|
||||
if (bulkBlock.list[0] == "[") {
|
||||
return {
|
||||
...props,
|
||||
disabled: true,
|
||||
placeHolder: "Domain list is a JSON import, input disabled"
|
||||
};
|
||||
} else {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bulk">
|
||||
<h2>Import / Export <a onClick={resetBulk}>reset</a></h2>
|
||||
<Bulk.TextArea
|
||||
id="list"
|
||||
name="Domains, one per line"
|
||||
placeHolder={`google.com\nfacebook.com`}
|
||||
/>
|
||||
|
||||
<Bulk.TextArea
|
||||
id="public_comment"
|
||||
name="Public comment"
|
||||
inputProps={disableInfoFields({ rows: 3 })}
|
||||
/>
|
||||
|
||||
<Bulk.TextArea
|
||||
id="private_comment"
|
||||
name="Private comment"
|
||||
inputProps={disableInfoFields({ rows: 3 })}
|
||||
/>
|
||||
|
||||
<Bulk.Checkbox
|
||||
id="obfuscate"
|
||||
name="Obfuscate domains? "
|
||||
inputProps={disableInfoFields()}
|
||||
/>
|
||||
|
||||
<div className="hidden">
|
||||
<Bulk.File
|
||||
id="json"
|
||||
fileType="application/json"
|
||||
withPreview={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="messagebutton">
|
||||
<div>
|
||||
<button type="submit" onClick={importBlocks}>Import</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" onClick={exportBlocks}>Export</button>
|
||||
|
||||
<Bulk.Select id="exportType" name="Export type" options={
|
||||
<>
|
||||
<option value="plain">One per line in text field</option>
|
||||
<option value="json">JSON in text field</option>
|
||||
<option value="json-download">JSON file download</option>
|
||||
<option disabled value="csv">CSV in text field (glitch-soc)</option>
|
||||
<option disabled value="csv-download">CSV file download (glitch-soc)</option>
|
||||
</>
|
||||
} />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
{errorMsg.length > 0 &&
|
||||
<div className="error accent">{errorMsg}</div>
|
||||
}
|
||||
{statusMsg.length > 0 &&
|
||||
<div className="accent">{statusMsg}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -36,13 +36,15 @@ function useEmojiByCategory(emoji) {
|
||||
), [emoji]);
|
||||
}
|
||||
|
||||
function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
|
||||
function CategorySelect({ field, children }) {
|
||||
const { value, setIsNew } = field;
|
||||
|
||||
const {
|
||||
data: emoji = [],
|
||||
isLoading,
|
||||
isSuccess,
|
||||
error
|
||||
} = query.useGetAllEmojiQuery({filter: "domain:local"});
|
||||
} = query.useGetAllEmojiQuery({ filter: "domain:local" });
|
||||
|
||||
const emojiByCategory = useEmojiByCategory(emoji);
|
||||
|
||||
@ -52,7 +54,7 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
|
||||
const categoryItems = React.useMemo(() => {
|
||||
return syncpipe(emojiByCategory, [
|
||||
(_) => Object.keys(_), // just emoji category names
|
||||
(_) => matchSorter(_, value, {threshold: matchSorter.rankings.NO_MATCH}), // sorted by complex algorithm
|
||||
(_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm
|
||||
(_) => _.map((categoryName) => [ // map to input value, and selectable element with icon
|
||||
categoryName,
|
||||
<>
|
||||
@ -67,24 +69,24 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
|
||||
if (value != undefined && isSuccess && value.trim().length > 0) {
|
||||
setIsNew(!categories.has(value.trim()));
|
||||
}
|
||||
}, [categories, value, setIsNew, isSuccess]);
|
||||
}, [categories, value, isSuccess, setIsNew]);
|
||||
|
||||
if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere
|
||||
return (
|
||||
<>
|
||||
<input type="text" placeholder="e.g., reactions" onChange={(e) => {categoryState.value = e.target.value;}}/>;
|
||||
<input type="text" placeholder="e.g., reactions" onChange={(e) => { field.value = e.target.value; }} />;
|
||||
</>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
return <input type="text" value="Loading categories..." disabled={true}/>;
|
||||
return <input type="text" value="Loading categories..." disabled={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ComboBox
|
||||
state={categoryState}
|
||||
field={field}
|
||||
items={categoryItems}
|
||||
label="Category"
|
||||
placeHolder="e.g., reactions"
|
||||
placeholder="e.g., reactions"
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
|
@ -19,15 +19,21 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const { useRoute, Link, Redirect } = require("wouter");
|
||||
|
||||
const { CategorySelect } = require("../category-select");
|
||||
const { useComboBoxInput, useFileInput } = require("../../../lib/form");
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
|
||||
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
|
||||
const { CategorySelect } = require("../category-select");
|
||||
|
||||
const useFormSubmit = require("../../../lib/form/submit");
|
||||
|
||||
const FakeToot = require("../../../components/fake-toot");
|
||||
const FormWithData = require("../../../lib/form/form-with-data");
|
||||
const Loading = require("../../../components/loading");
|
||||
const { FileInput } = require("../../../components/form/inputs");
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
const { Error } = require("../../../components/error");
|
||||
|
||||
const base = "/settings/custom-emoji/local";
|
||||
|
||||
@ -39,57 +45,41 @@ module.exports = function EmojiDetailRoute() {
|
||||
return (
|
||||
<div className="emoji-detail">
|
||||
<Link to={base}><a>< go back</a></Link>
|
||||
<EmojiDetailData emojiId={params.emojiId} />
|
||||
<FormWithData dataQuery={query.useGetEmojiQuery} arg={params.emojiId} DataForm={EmojiDetailForm} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function EmojiDetailData({ emojiId }) {
|
||||
const { currentData: emoji, isLoading, error } = query.useGetEmojiQuery(emojiId);
|
||||
function EmojiDetailForm({ data: emoji }) {
|
||||
const form = {
|
||||
id: useValue("id", emoji.id),
|
||||
category: useComboBoxInput("category", { defaultValue: emoji.category }),
|
||||
image: useFileInput("image", {
|
||||
withPreview: true,
|
||||
maxSize: 50 * 1024 // TODO: get from instance api
|
||||
})
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error accent">
|
||||
{error.status}: {error.data.error}
|
||||
</div>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
return (
|
||||
<div>
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <EmojiDetail emoji={emoji} />;
|
||||
}
|
||||
}
|
||||
|
||||
function EmojiDetail({ emoji }) {
|
||||
const [modifyEmoji, modifyResult] = query.useEditEmojiMutation();
|
||||
|
||||
const [isNewCategory, setIsNewCategory] = React.useState(false);
|
||||
|
||||
const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", { defaultValue: emoji.category });
|
||||
|
||||
const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
|
||||
withPreview: true,
|
||||
maxSize: 50 * 1024
|
||||
});
|
||||
|
||||
function modifyCategory() {
|
||||
modifyEmoji({ id: emoji.id, category: category.trim() });
|
||||
}
|
||||
|
||||
function modifyImage() {
|
||||
modifyEmoji({ id: emoji.id, image: image });
|
||||
}
|
||||
const [modifyEmoji, result] = useFormSubmit(form, query.useEditEmojiMutation());
|
||||
|
||||
// Automatic submitting of category change
|
||||
React.useEffect(() => {
|
||||
if (category != emoji.category && !categoryState.open && !isNewCategory && category?.trim().length > 0) {
|
||||
modifyEmoji({ id: emoji.id, category: category.trim() });
|
||||
if (
|
||||
form.category.hasChanged() &&
|
||||
!form.category.state.open &&
|
||||
!form.category.isNew) {
|
||||
modifyEmoji();
|
||||
}
|
||||
}, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]);
|
||||
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
|
||||
|
||||
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
||||
|
||||
if (deleteResult.isSuccess) {
|
||||
return <Redirect to={base} />;
|
||||
}
|
||||
|
||||
console.log(form.category);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -97,58 +87,62 @@ function EmojiDetail({ emoji }) {
|
||||
<img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode} />
|
||||
<div>
|
||||
<h2>{emoji.shortcode}</h2>
|
||||
<DeleteButton id={emoji.id} />
|
||||
<MutationButton
|
||||
label="Delete"
|
||||
type="button"
|
||||
onClick={() => deleteEmoji(emoji.id)}
|
||||
className="danger button-inline"
|
||||
showError={false}
|
||||
result={deleteResult}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="left-border">
|
||||
<h2>Modify this emoji {modifyResult.isLoading && "(processing..)"}</h2>
|
||||
|
||||
{modifyResult.error && <div className="error">
|
||||
{modifyResult.error.status}: {modifyResult.error.data.error}
|
||||
</div>}
|
||||
<form onSubmit={modifyEmoji} className="left-border">
|
||||
<h2>Modify this emoji {result.isLoading && <Loading />}</h2>
|
||||
|
||||
<div className="update-category">
|
||||
<CategorySelect
|
||||
value={category}
|
||||
categoryState={categoryState}
|
||||
setIsNew={setIsNewCategory}
|
||||
field={form.category}
|
||||
>
|
||||
<button style={{ visibility: (isNewCategory ? "initial" : "hidden") }} onClick={modifyCategory}>
|
||||
Create
|
||||
</button>
|
||||
<MutationButton
|
||||
name="create-category"
|
||||
label="Create"
|
||||
result={result}
|
||||
showError={false}
|
||||
style={{ visibility: (form.category.isNew ? "initial" : "hidden") }}
|
||||
/>
|
||||
</CategorySelect>
|
||||
</div>
|
||||
|
||||
<div className="update-image">
|
||||
<b>Image</b>
|
||||
<div className="form-field file">
|
||||
<label className="file-input button" htmlFor="image">
|
||||
Browse
|
||||
</label>
|
||||
{imageInfo}
|
||||
<input
|
||||
className="hidden"
|
||||
type="file"
|
||||
id="image"
|
||||
name="Image"
|
||||
accept="image/png,image/gif"
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
</div>
|
||||
<FileInput
|
||||
field={form.image}
|
||||
label="Image"
|
||||
accept="image/png,image/gif"
|
||||
/>
|
||||
|
||||
<button onClick={modifyImage} disabled={image == undefined}>Replace image</button>
|
||||
<MutationButton
|
||||
name="image"
|
||||
label="Replace image"
|
||||
showError={false}
|
||||
className="button-inline"
|
||||
result={result}
|
||||
/>
|
||||
|
||||
<FakeToot>
|
||||
Look at this new custom emoji <img
|
||||
className="emoji"
|
||||
src={imageURL ?? emoji.url}
|
||||
src={form.image.previewURL ?? emoji.url}
|
||||
title={`:${emoji.shortcode}:`}
|
||||
alt={emoji.shortcode}
|
||||
/> isn't it cool?
|
||||
</FakeToot>
|
||||
|
||||
{result.error && <Error error={result.error} />}
|
||||
{deleteResult.error && <Error error={deleteResult.error} />}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const {Switch, Route} = require("wouter");
|
||||
const { Switch, Route } = require("wouter");
|
||||
|
||||
const EmojiOverview = require("./overview");
|
||||
const EmojiDetail = require("./detail");
|
||||
@ -31,9 +31,9 @@ module.exports = function CustomEmoji() {
|
||||
<>
|
||||
<Switch>
|
||||
<Route path={`${base}/:emojiId`}>
|
||||
<EmojiDetail />
|
||||
<EmojiDetail baseUrl={base} />
|
||||
</Route>
|
||||
<EmojiOverview />
|
||||
<EmojiOverview baseUrl={base} />
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
|
@ -21,98 +21,65 @@
|
||||
const Promise = require('bluebird');
|
||||
const React = require("react");
|
||||
|
||||
const FakeToot = require("../../../components/fake-toot");
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
const query = require("../../../lib/query");
|
||||
|
||||
const {
|
||||
useTextInput,
|
||||
useFileInput,
|
||||
useComboBoxInput
|
||||
} = require("../../../lib/form");
|
||||
const useShortcode = require("./use-shortcode");
|
||||
|
||||
const useFormSubmit = require("../../../lib/form/submit");
|
||||
|
||||
const {
|
||||
TextInput, FileInput
|
||||
} = require("../../../components/form/inputs");
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
const { CategorySelect } = require('../category-select');
|
||||
const FakeToot = require("../../../components/fake-toot");
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
|
||||
const shortcodeRegex = /^[a-z0-9_]+$/;
|
||||
module.exports = function NewEmojiForm() {
|
||||
const shortcode = useShortcode();
|
||||
|
||||
module.exports = function NewEmojiForm({ emoji }) {
|
||||
const emojiCodes = React.useMemo(() => {
|
||||
return new Set(emoji.map((e) => e.shortcode));
|
||||
}, [emoji]);
|
||||
|
||||
const [addEmoji, result] = query.useAddEmojiMutation();
|
||||
|
||||
const [onFileChange, resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
|
||||
const image = useFileInput("image", {
|
||||
withPreview: true,
|
||||
maxSize: 50 * 1024
|
||||
maxSize: 50 * 1024 // TODO: get from instance api?
|
||||
});
|
||||
|
||||
const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {
|
||||
validator: function validateShortcode(code) {
|
||||
// technically invalid, but hacky fix to prevent validation error on page load
|
||||
if (shortcode == "") { return ""; }
|
||||
const category = useComboBoxInput("category");
|
||||
|
||||
if (emojiCodes.has(code)) {
|
||||
return "Shortcode already in use";
|
||||
}
|
||||
const [submitForm, result] = useFormSubmit({
|
||||
shortcode, image, category
|
||||
}, query.useAddEmojiMutation());
|
||||
|
||||
if (code.length < 2 || code.length > 30) {
|
||||
return "Shortcode must be between 2 and 30 characters";
|
||||
}
|
||||
// const [onFileChange, resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
|
||||
// withPreview: true,
|
||||
// maxSize: 50 * 1024
|
||||
// });
|
||||
|
||||
if (code.toLowerCase() != code) {
|
||||
return "Shortcode must be lowercase";
|
||||
}
|
||||
|
||||
if (!shortcodeRegex.test(code)) {
|
||||
return "Shortcode must only contain lowercase letters, numbers, and underscores";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const [categoryState, resetCategory, { category }] = useComboBoxInput("category");
|
||||
// const [categoryState, resetCategory, { category }] = useComboBoxInput("category");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shortcode.length == 0) {
|
||||
if (image != undefined) {
|
||||
let [name, _ext] = image.name.split(".");
|
||||
setShortcode(name);
|
||||
if (shortcode.value.length == 0) {
|
||||
if (image.value != undefined) {
|
||||
let [name, _ext] = image.value.name.split(".");
|
||||
shortcode.setter(name);
|
||||
}
|
||||
}
|
||||
// we explicitly don't want to add 'shortcode' as a dependency here
|
||||
// because we only want this to update 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]);
|
||||
}, [image.value]);
|
||||
|
||||
function uploadEmoji(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
let emojiOrShortcode = `:${shortcode.value}:`;
|
||||
|
||||
Promise.try(() => {
|
||||
return addEmoji({
|
||||
image,
|
||||
shortcode,
|
||||
category
|
||||
});
|
||||
}).then((res) => {
|
||||
if (res.error == undefined) {
|
||||
resetFile();
|
||||
resetShortcode();
|
||||
resetCategory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let emojiOrShortcode = `:${shortcode}:`;
|
||||
|
||||
if (imageURL != undefined) {
|
||||
if (image.previewValue != undefined) {
|
||||
emojiOrShortcode = <img
|
||||
className="emoji"
|
||||
src={imageURL}
|
||||
src={image.previewValue}
|
||||
title={`:${shortcode}:`}
|
||||
alt={shortcode}
|
||||
/>;
|
||||
@ -126,39 +93,19 @@ module.exports = function NewEmojiForm({ emoji }) {
|
||||
Look at this new custom emoji {emojiOrShortcode} isn't it cool?
|
||||
</FakeToot>
|
||||
|
||||
<form onSubmit={uploadEmoji} className="form-flex">
|
||||
<div className="form-field file">
|
||||
<label className="file-input button" htmlFor="image">
|
||||
Browse
|
||||
</label>
|
||||
{imageInfo}
|
||||
<input
|
||||
className="hidden"
|
||||
type="file"
|
||||
id="image"
|
||||
name="Image"
|
||||
accept="image/png,image/gif"
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={submitForm} className="form-flex">
|
||||
<FileInput
|
||||
field={image}
|
||||
accept="image/png,image/gif"
|
||||
/>
|
||||
|
||||
<div className="form-field text">
|
||||
<label htmlFor="shortcode">
|
||||
Shortcode, must be unique among the instance's local emoji
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shortcode"
|
||||
name="Shortcode"
|
||||
ref={shortcodeRef}
|
||||
onChange={onShortcodeChange}
|
||||
value={shortcode}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
field={shortcode}
|
||||
label="Shortcode, must be unique among the instance's local emoji"
|
||||
/>
|
||||
|
||||
<CategorySelect
|
||||
value={category}
|
||||
categoryState={categoryState}
|
||||
field={category}
|
||||
/>
|
||||
|
||||
<MutationButton label="Upload emoji" result={result} />
|
||||
|
@ -19,7 +19,7 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const {Link} = require("wouter");
|
||||
const { Link } = require("wouter");
|
||||
|
||||
const NewEmojiForm = require("./new-emoji");
|
||||
|
||||
@ -27,33 +27,31 @@ const query = require("../../../lib/query");
|
||||
const { useEmojiByCategory } = require("../category-select");
|
||||
const Loading = require("../../../components/loading");
|
||||
|
||||
const base = "/settings/custom-emoji/local";
|
||||
|
||||
module.exports = function EmojiOverview() {
|
||||
module.exports = function EmojiOverview({ baseUrl }) {
|
||||
const {
|
||||
data: emoji = [],
|
||||
isLoading,
|
||||
error
|
||||
} = query.useGetAllEmojiQuery({filter: "domain:local"});
|
||||
} = query.useGetAllEmojiQuery({ filter: "domain:local" });
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Custom Emoji (local)</h1>
|
||||
{error &&
|
||||
{error &&
|
||||
<div className="error accent">{error}</div>
|
||||
}
|
||||
{isLoading
|
||||
? <Loading/>
|
||||
? <Loading />
|
||||
: <>
|
||||
<EmojiList emoji={emoji}/>
|
||||
<NewEmojiForm emoji={emoji}/>
|
||||
<EmojiList emoji={emoji} baseUrl={baseUrl} />
|
||||
<NewEmojiForm emoji={emoji} />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function EmojiList({emoji}) {
|
||||
function EmojiList({ emoji, baseUrl }) {
|
||||
const emojiByCategory = useEmojiByCategory(emoji);
|
||||
|
||||
return (
|
||||
@ -62,24 +60,23 @@ function EmojiList({emoji}) {
|
||||
<div className="list emoji-list">
|
||||
{emoji.length == 0 && "No local emoji yet, add one below"}
|
||||
{Object.entries(emojiByCategory).map(([category, entries]) => {
|
||||
return <EmojiCategory key={category} category={category} entries={entries}/>;
|
||||
return <EmojiCategory key={category} category={category} entries={entries} baseUrl={baseUrl} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiCategory({category, entries}) {
|
||||
function EmojiCategory({ category, entries, baseUrl }) {
|
||||
return (
|
||||
<div className="entry">
|
||||
<b>{category}</b>
|
||||
<div className="emoji-group">
|
||||
{entries.map((e) => {
|
||||
return (
|
||||
<Link key={e.id} to={`${base}/${e.id}`}>
|
||||
{/* <Link key={e.static_url} to={`${base}`}> */}
|
||||
<Link key={e.id} to={`${baseUrl}/${e.id}`}>
|
||||
<a>
|
||||
<img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`}/>
|
||||
<img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`} />
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
61
web/source/settings/admin/emoji/local/use-shortcode.js
Normal file
61
web/source/settings/admin/emoji/local/use-shortcode.js
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
const { useTextInput } = require("../../../lib/form");
|
||||
|
||||
const shortcodeRegex = /^[a-z0-9_]+$/;
|
||||
|
||||
module.exports = function useShortcode() {
|
||||
const {
|
||||
data: emoji = []
|
||||
} = query.useGetAllEmojiQuery({ filter: "domain:local" });
|
||||
|
||||
const emojiCodes = React.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 (code.toLowerCase() != code) {
|
||||
return "Shortcode must be lowercase";
|
||||
}
|
||||
|
||||
if (!shortcodeRegex.test(code)) {
|
||||
return "Shortcode must only contain lowercase letters, numbers, and underscores";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
@ -18,10 +18,9 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const Redux = require("react-redux");
|
||||
const syncpipe = require("syncpipe");
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
|
||||
const {
|
||||
useTextInput,
|
||||
@ -34,44 +33,15 @@ const useFormSubmit = require("../../../lib/form/submit");
|
||||
const CheckList = require("../../../components/check-list");
|
||||
const { CategorySelect } = require('../category-select');
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
const Loading = require("../../../components/loading");
|
||||
const { TextInput } = require("../../../components/form/inputs");
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
const { Error } = require("../../../components/error");
|
||||
|
||||
module.exports = function ParseFromToot({ emojiCodes }) {
|
||||
const [searchStatus, { data, isLoading, isSuccess, error }] = query.useSearchStatusForEmojiMutation();
|
||||
const instanceDomain = Redux.useSelector((state) => (new URL(state.oauth.instance).host));
|
||||
const [searchStatus, result] = query.useSearchStatusForEmojiMutation();
|
||||
|
||||
const [onURLChange, _resetURL, { url }] = useTextInput("url");
|
||||
|
||||
const searchResult = React.useMemo(() => {
|
||||
if (!isSuccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.type == "none") {
|
||||
return "No results found";
|
||||
}
|
||||
|
||||
if (data.domain == instanceDomain) {
|
||||
return <b>This is a local user/toot, all referenced emoji are already on your instance</b>;
|
||||
}
|
||||
|
||||
if (data.list.length == 0) {
|
||||
return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyEmojiForm
|
||||
localEmojiCodes={emojiCodes}
|
||||
type={data.type}
|
||||
domain={data.domain}
|
||||
emojiList={data.list}
|
||||
/>
|
||||
);
|
||||
}, [isSuccess, data, instanceDomain, emojiCodes]);
|
||||
|
||||
function submitSearch(e) {
|
||||
e.preventDefault();
|
||||
if (url.trim().length != 0) {
|
||||
@ -95,101 +65,143 @@ module.exports = function ParseFromToot({ emojiCodes }) {
|
||||
onChange={onURLChange}
|
||||
value={url}
|
||||
/>
|
||||
<button className="button-inline" disabled={isLoading}>
|
||||
<button className="button-inline" disabled={result.isLoading}>
|
||||
<i className={[
|
||||
"fa fa-fw",
|
||||
(isLoading
|
||||
(result.isLoading
|
||||
? "fa-refresh fa-spin"
|
||||
: "fa-search")
|
||||
].join(" ")} aria-hidden="true" title="Search" />
|
||||
<span className="sr-only">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="error">{error.data.error}</div>}
|
||||
</div>
|
||||
</form>
|
||||
{searchResult}
|
||||
<SearchResult result={result} localEmojiCodes={emojiCodes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function CopyEmojiForm({ localEmojiCodes, type, domain, emojiList }) {
|
||||
const [patchRemoteEmojis, patchResult] = query.usePatchRemoteEmojisMutation();
|
||||
const [err, setError] = React.useState();
|
||||
function SearchResult({ result, localEmojiCodes }) {
|
||||
const { error, data, isSuccess, isError } = result;
|
||||
|
||||
const emojiCheckList = useCheckListInput("selectedEmoji", {
|
||||
entries: emojiList,
|
||||
uniqueKey: "shortcode"
|
||||
});
|
||||
if (!(isSuccess || isError)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [categoryState, resetCategory, { category }] = useComboBoxInput("category");
|
||||
if (error == "NONE_FOUND") {
|
||||
return "No results found";
|
||||
} else if (error == "LOCAL_INSTANCE") {
|
||||
return <b>This is a local user/toot, all referenced emoji are already on your instance</b>;
|
||||
} else if (error != undefined) {
|
||||
return <Error error={result.error} />;
|
||||
}
|
||||
|
||||
const buttonsInactive = emojiCheckList.someSelected
|
||||
if (data.list.length == 0) {
|
||||
return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyEmojiForm
|
||||
localEmojiCodes={localEmojiCodes}
|
||||
type={data.type}
|
||||
domain={data.domain}
|
||||
emojiList={data.list}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
|
||||
const form = {
|
||||
selectedEmoji: useCheckListInput("selectedEmoji", {
|
||||
entries: emojiList,
|
||||
uniqueKey: "shortcode"
|
||||
}),
|
||||
category: useComboBoxInput("category")
|
||||
};
|
||||
|
||||
const [formSubmit, result] = useFormSubmit(form, query.usePatchRemoteEmojisMutation(), { changedOnly: false });
|
||||
console.log("action:", result.action);
|
||||
|
||||
const buttonsInactive = form.selectedEmoji.someSelected
|
||||
? {}
|
||||
: {
|
||||
disabled: true,
|
||||
title: "No emoji selected, cannot perform any actions"
|
||||
};
|
||||
|
||||
function submit(action) {
|
||||
Promise.try(() => {
|
||||
setError(null);
|
||||
const selectedShortcodes = emojiCheckList.selectedValues.map(([shortcode, entry]) => {
|
||||
if (action == "copy" && !entry.valid) {
|
||||
throw `One or more selected emoji have non-unique shortcodes (${shortcode}), unselect them or pick a different local shortcode`;
|
||||
}
|
||||
return {
|
||||
shortcode,
|
||||
localShortcode: entry.shortcode
|
||||
};
|
||||
});
|
||||
// function submit(action) {
|
||||
// Promise.try(() => {
|
||||
// setError(null);
|
||||
// const selectedShortcodes = emojiCheckList.selectedValues.map(([shortcode, entry]) => {
|
||||
// if (action == "copy" && !entry.valid) {
|
||||
// throw `One or more selected emoji have non-unique shortcodes (${shortcode}), unselect them or pick a different local shortcode`;
|
||||
// }
|
||||
// return {
|
||||
// shortcode,
|
||||
// localShortcode: entry.shortcode
|
||||
// };
|
||||
// });
|
||||
|
||||
return patchRemoteEmojis({
|
||||
action,
|
||||
domain,
|
||||
list: selectedShortcodes,
|
||||
category
|
||||
}).unwrap();
|
||||
}).then(() => {
|
||||
emojiCheckList.reset();
|
||||
resetCategory();
|
||||
}).catch((e) => {
|
||||
if (Array.isArray(e)) {
|
||||
setError(e.map(([shortcode, msg]) => (
|
||||
<div key={shortcode}>
|
||||
{shortcode}: <span style={{ fontWeight: "initial" }}>{msg}</span>
|
||||
</div>
|
||||
)));
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
// return patchRemoteEmojis({
|
||||
// action,
|
||||
// domain,
|
||||
// list: selectedShortcodes,
|
||||
// category
|
||||
// }).unwrap();
|
||||
// }).then(() => {
|
||||
// emojiCheckList.reset();
|
||||
// resetCategory();
|
||||
// }).catch((e) => {
|
||||
// if (Array.isArray(e)) {
|
||||
// setError(e.map(([shortcode, msg]) => (
|
||||
// <div key={shortcode}>
|
||||
// {shortcode}: <span style={{ fontWeight: "initial" }}>{msg}</span>
|
||||
// </div>
|
||||
// )));
|
||||
// } else {
|
||||
// setError(e);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="parsed">
|
||||
<span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span>
|
||||
<CheckList
|
||||
field={emojiCheckList}
|
||||
Component={EmojiEntry}
|
||||
localEmojiCodes={localEmojiCodes}
|
||||
/>
|
||||
<form onSubmit={formSubmit}>
|
||||
<CheckList
|
||||
field={form.selectedEmoji}
|
||||
Component={EmojiEntry}
|
||||
localEmojiCodes={localEmojiCodes}
|
||||
/>
|
||||
|
||||
<CategorySelect
|
||||
value={category}
|
||||
categoryState={categoryState}
|
||||
/>
|
||||
<CategorySelect
|
||||
field={form.category}
|
||||
/>
|
||||
|
||||
<div className="action-buttons row">
|
||||
<MutationButton label="Copy to local emoji" type="button" result={patchResult} {...buttonsInactive} />
|
||||
<MutationButton label="Disable" type="button" result={patchResult} className="button danger" {...buttonsInactive} />
|
||||
</div>
|
||||
{err && <div className="error">
|
||||
{err}
|
||||
</div>}
|
||||
{patchResult.isSuccess && <div>
|
||||
Action applied to {patchResult.data.length} emoji
|
||||
</div>}
|
||||
<div className="action-buttons row">
|
||||
<MutationButton name="copy" label="Copy to local emoji" result={result} showError={false} {...buttonsInactive} />
|
||||
<MutationButton name="disable" label="Disable" result={result} className="button danger" showError={false} {...buttonsInactive} />
|
||||
</div>
|
||||
{result.error && (
|
||||
Array.isArray(result.error)
|
||||
? <ErrorList errors={result.error} />
|
||||
: <Error error={result.error} />
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorList({ errors }) {
|
||||
return (
|
||||
<div className="error">
|
||||
One or multiple emoji failed to process:
|
||||
{errors.map(([shortcode, err]) => (
|
||||
<div key={shortcode}>
|
||||
<b>{shortcode}:</b> {err}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
const React = require("react");
|
||||
|
||||
const query = require("../../lib/query");
|
||||
const processDomainList = require("../../lib/import-export");
|
||||
|
||||
const {
|
||||
useTextInput,
|
||||
@ -34,37 +35,71 @@ const {
|
||||
TextInput,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
FileInput
|
||||
FileInput,
|
||||
useCheckListInput
|
||||
} = require("../../components/form/inputs");
|
||||
|
||||
const FormWithData = require("../../lib/form/form-with-data");
|
||||
const CheckList = require("../../components/check-list");
|
||||
const MutationButton = require("../../components/form/mutation-button");
|
||||
|
||||
module.exports = function ImportExport() {
|
||||
const [parsedList, setParsedList] = React.useState();
|
||||
|
||||
const form = {
|
||||
domains: useTextInput("domains"),
|
||||
obfuscate: useBoolInput("obfuscate"),
|
||||
commentPrivate: useTextInput("private_comment"),
|
||||
commentPublic: useTextInput("public_comment"),
|
||||
// json: useFileInput("json")
|
||||
};
|
||||
|
||||
function submitImport(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Promise.try(() => {
|
||||
return processDomainList(form.domains.value);
|
||||
}).then((processed) => {
|
||||
setParsedList(processed);
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="import-export">
|
||||
<h2>Import / Export</h2>
|
||||
<FormWithData
|
||||
dataQuery={query.useInstanceBlocksQuery}
|
||||
DataForm={ImportExportForm}
|
||||
/>
|
||||
<div>
|
||||
{
|
||||
parsedList
|
||||
? <ImportExportList list={parsedList} />
|
||||
: <ImportExportForm form={form} submitImport={submitImport} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function ImportExportForm({ data: blockedInstances }) {
|
||||
const form = {
|
||||
list: useTextInput("list"),
|
||||
obfuscate: useBoolInput("obfuscate"),
|
||||
commentPrivate: useTextInput("private_comment"),
|
||||
commentPublic: useTextInput("public_comment"),
|
||||
json: useFileInput("json")
|
||||
};
|
||||
function ImportExportList({ list }) {
|
||||
const entryCheckList = useCheckListInput("selectedDomains", {
|
||||
entries: list,
|
||||
uniqueKey: "domain"
|
||||
});
|
||||
|
||||
return (
|
||||
<form>
|
||||
<CheckList
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportExportForm({ form, submitImport }) {
|
||||
return (
|
||||
<form onSubmit={submitImport}>
|
||||
<TextArea
|
||||
field={form.list}
|
||||
label="Domains, one per line"
|
||||
field={form.domains}
|
||||
label="Domains, one per line (plaintext) or JSON"
|
||||
placeholder={`google.com\nfacebook.com`}
|
||||
rows={8}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
@ -83,6 +118,10 @@ function ImportExportForm({ data: blockedInstances }) {
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domain in public lists"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<MutationButton label="Import" result={importResult} /> {/* default form action */}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -25,13 +25,14 @@ const baseUrl = `/settings/admin/federation`;
|
||||
|
||||
const InstanceOverview = require("./overview");
|
||||
const InstanceDetail = require("./detail");
|
||||
const InstanceImportExport = require("./import-export");
|
||||
|
||||
module.exports = function Federation({ }) {
|
||||
return (
|
||||
<Switch>
|
||||
{/* <Route path={`${baseUrl}/import-export`}>
|
||||
<Route path={`${baseUrl}/import-export`}>
|
||||
<InstanceImportExport />
|
||||
</Route> */}
|
||||
</Route>
|
||||
|
||||
<Route path={`${baseUrl}/:domain`}>
|
||||
<InstanceDetail baseUrl={baseUrl} />
|
||||
|
@ -72,7 +72,7 @@ module.exports = function InstanceOverview({ baseUrl }) {
|
||||
<span>
|
||||
{blockedInstances.length} blocked instance{blockedInstances.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered)`}
|
||||
</span>
|
||||
<div className="list">
|
||||
<div className="list scrolling">
|
||||
{filteredInstances.map((entry) => {
|
||||
return (
|
||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||
@ -90,8 +90,7 @@ module.exports = function InstanceOverview({ baseUrl }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImportExport />
|
||||
<Link to={`${baseUrl}/import-export`}><a>Or use the bulk import/export interface</a></Link>
|
||||
</>
|
||||
);
|
||||
};
|
@ -26,21 +26,21 @@ const {
|
||||
ComboboxPopover,
|
||||
} = require("ariakit/combobox");
|
||||
|
||||
module.exports = function ComboBox({state, items, label, placeHolder, children}) {
|
||||
module.exports = function ComboBox({ field, items, label, children, ...inputProps }) {
|
||||
return (
|
||||
<div className="form-field combobox-wrapper">
|
||||
<label>
|
||||
{label}
|
||||
<div className="row">
|
||||
<Combobox
|
||||
state={state}
|
||||
placeholder={placeHolder}
|
||||
state={field.state}
|
||||
className="combobox input"
|
||||
{...inputProps}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</label>
|
||||
<ComboboxPopover state={state} className="popover">
|
||||
<ComboboxPopover state={field.state} className="popover">
|
||||
{items.map(([key, value]) => (
|
||||
<ComboboxItem className="combobox-item" key={key} value={key}>
|
||||
{value}
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = function ErrorFallback({error, resetErrorBoundary}) {
|
||||
function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
return (
|
||||
<div className="error">
|
||||
<p>
|
||||
@ -28,7 +28,7 @@ module.exports = function ErrorFallback({error, resetErrorBoundary}) {
|
||||
<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a>
|
||||
{" or "}
|
||||
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
||||
<br/>Include the details below:
|
||||
<br />Include the details below:
|
||||
</p>
|
||||
<pre>
|
||||
{error.name}: {error.message}
|
||||
@ -41,4 +41,33 @@ module.exports = function ErrorFallback({error, resetErrorBoundary}) {
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function Error({ error }) {
|
||||
console.error("Rendering:", error);
|
||||
let message;
|
||||
|
||||
if (error.data != undefined) { // RTK Query error with data
|
||||
if (error.status) {
|
||||
message = (<>
|
||||
<b>{error.status}:</b> {error.data.error}
|
||||
</>);
|
||||
} 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 {
|
||||
message = error.message ?? error;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="error">
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { ErrorFallback, Error };
|
@ -60,7 +60,7 @@ function FileInput({ label, field, ...inputProps }) {
|
||||
return (
|
||||
<div className="form-field file">
|
||||
<label>
|
||||
{label}
|
||||
<div className="label">{label}</div>
|
||||
<div className="file-input button">Browse</div>
|
||||
{infoComponent}
|
||||
{/* <a onClick={removeFile("header")}>remove</a> */}
|
||||
|
@ -19,23 +19,30 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const { Error } = require("../error");
|
||||
|
||||
module.exports = function MutationButton({ label, result, disabled, ...inputProps }) {
|
||||
module.exports = function MutationButton({ label, result, disabled, showError = true, className = "", ...inputProps }) {
|
||||
let iconClass = "";
|
||||
const targetsThisButton = result.action == inputProps.name; // can also both be undefined, which is correct
|
||||
|
||||
if (result.isLoading) {
|
||||
iconClass = "fa-spin fa-refresh";
|
||||
} else if (result.isSuccess) {
|
||||
iconClass = "fa-check fadeout";
|
||||
/* FIXME? submitting an unchanged form will never transition to isLoading,
|
||||
and check icon will stay faded out because the css animation doesn't restart
|
||||
*/
|
||||
if (targetsThisButton) {
|
||||
if (result.isLoading) {
|
||||
iconClass = "fa-spin fa-refresh";
|
||||
} else if (result.isSuccess) {
|
||||
iconClass = "fa-check fadeout";
|
||||
}
|
||||
}
|
||||
|
||||
return (<div>
|
||||
{result.error &&
|
||||
<section className="error">{result.error.status}: {result.error.data.error}</section>
|
||||
{(showError && targetsThisButton && result.error) &&
|
||||
<Error error={result.error} />
|
||||
}
|
||||
<button type="submit" disabled={result.isLoading || disabled} {...inputProps}>
|
||||
<i className={`fa fa-fw with-text ${iconClass}`} aria-hidden="true"></i>
|
||||
{result.isLoading
|
||||
<button type="submit" className={"with-icon " + className} disabled={result.isLoading || disabled} {...inputProps}>
|
||||
<i className={`fa fa-fw ${iconClass}`} aria-hidden="true"></i>
|
||||
{(targetsThisButton && result.isLoading)
|
||||
? "Processing..."
|
||||
: label
|
||||
}
|
||||
|
@ -12,10 +12,10 @@
|
||||
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/>.
|
||||
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/>.
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
@ -24,11 +24,12 @@ const Redux = require("react-redux");
|
||||
|
||||
const { setInstance } = require("../redux/reducers/oauth").actions;
|
||||
const api = require("../lib/api");
|
||||
const { Error } = require("./error");
|
||||
|
||||
module.exports = function Login({error}) {
|
||||
module.exports = function Login({ error }) {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const [ instanceField, setInstanceField ] = React.useState("");
|
||||
const [ errorMsg, setErrorMsg ] = React.useState();
|
||||
const [instanceField, setInstanceField] = React.useState("");
|
||||
const [loginError, setLoginError] = React.useState();
|
||||
const instanceFieldRef = React.useRef("");
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -65,12 +66,7 @@ module.exports = function Login({error}) {
|
||||
}).then(() => {
|
||||
return dispatch(api.oauth.authorize()); // will send user off-page
|
||||
}).catch((e) => {
|
||||
setErrorMsg(
|
||||
<>
|
||||
<b>{e.type}</b>
|
||||
<span>{e.message}</span>
|
||||
</>
|
||||
);
|
||||
setLoginError(e);
|
||||
});
|
||||
}
|
||||
|
||||
@ -89,11 +85,9 @@ module.exports = function Login({error}) {
|
||||
{error}
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<label htmlFor="instance">Instance: </label>
|
||||
<input value={instanceField} onChange={updateInstanceField} id="instance"/>
|
||||
{errorMsg &&
|
||||
<div className="error">
|
||||
{errorMsg}
|
||||
</div>
|
||||
<input value={instanceField} onChange={updateInstanceField} id="instance" />
|
||||
{loginError &&
|
||||
<Error error={loginError} />
|
||||
}
|
||||
<button onClick={tryInstance}>Authenticate</button>
|
||||
</form>
|
||||
|
@ -33,6 +33,7 @@ const { AuthenticationError } = require("./lib/errors");
|
||||
|
||||
const Login = require("./components/login");
|
||||
const Loading = require("./components/loading");
|
||||
const { Error } = require("./components/error");
|
||||
|
||||
require("./style.css");
|
||||
|
||||
@ -103,12 +104,7 @@ function App() {
|
||||
|
||||
let ErrorElement = null;
|
||||
if (errorMsg != undefined) {
|
||||
ErrorElement = (
|
||||
<div className="error">
|
||||
<b>{errorMsg.type}</b>
|
||||
<span>{errorMsg.message}</span>
|
||||
</div>
|
||||
);
|
||||
ErrorElement = <Error error={errorMsg} />;
|
||||
}
|
||||
|
||||
const LogoutElement = (
|
||||
|
@ -75,7 +75,7 @@ module.exports = function ({ apiCall, getChanges }) {
|
||||
});
|
||||
|
||||
let defaultDate = new Date().toUTCString();
|
||||
|
||||
|
||||
if (list[0] == "[") {
|
||||
domains = JSON.parse(state.list);
|
||||
} else {
|
||||
@ -85,7 +85,7 @@ module.exports = function ({ apiCall, getChanges }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isValidDomain(line, {wildcard: true, allowUnicode: true})) {
|
||||
if (!isValidDomain(line, { wildcard: true, allowUnicode: true })) {
|
||||
invalidDomains.push(line);
|
||||
return null;
|
||||
}
|
||||
@ -103,7 +103,7 @@ module.exports = function ({ apiCall, getChanges }) {
|
||||
}
|
||||
|
||||
const update = {
|
||||
domains: new Blob([JSON.stringify(domains)], {type: "application/json"})
|
||||
domains: new Blob([JSON.stringify(domains)], { type: "application/json" })
|
||||
};
|
||||
|
||||
return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks?import=true", update, "form"));
|
||||
|
@ -18,9 +18,13 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const { useComboboxState } = require("ariakit/combobox");
|
||||
|
||||
module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {}) {
|
||||
const [isNew, setIsNew] = React.useState(false);
|
||||
|
||||
const state = useComboboxState({
|
||||
defaultValue,
|
||||
gutter: 0,
|
||||
@ -36,11 +40,17 @@ module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {}
|
||||
reset,
|
||||
{
|
||||
[name]: state.value,
|
||||
name
|
||||
name,
|
||||
[`${name}IsNew`]: isNew,
|
||||
[`set${Name}IsNew`]: setIsNew
|
||||
}
|
||||
], {
|
||||
name,
|
||||
state,
|
||||
value: state.value,
|
||||
hasChanged: () => state.value != defaultValue,
|
||||
isNew,
|
||||
setIsNew,
|
||||
reset
|
||||
});
|
||||
};
|
@ -6,12 +6,16 @@ const Loading = require("../../components/loading");
|
||||
|
||||
// Wrap Form component inside component that fires the RTK Query call,
|
||||
// so Form will only be rendered when data is available to generate form-fields for
|
||||
module.exports = function FormWithData({dataQuery, DataForm}) {
|
||||
const {data, isLoading} = dataQuery();
|
||||
module.exports = function FormWithData({ dataQuery, DataForm, arg }) {
|
||||
const { data, isLoading } = dataQuery(arg);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading/>;
|
||||
return (
|
||||
<div>
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <DataForm data={data}/>;
|
||||
return <DataForm data={data} />;
|
||||
}
|
||||
};
|
@ -33,6 +33,13 @@ module.exports = {
|
||||
useTextInput: makeHook(require("./text")),
|
||||
useFileInput: makeHook(require("./file")),
|
||||
useBoolInput: makeHook(require("./bool")),
|
||||
useComboBoxInput: makeHook(require("./combobox")),
|
||||
useCheckListInput: makeHook(require("./check-list"))
|
||||
useComboBoxInput: makeHook(require("./combo-box")),
|
||||
useCheckListInput: makeHook(require("./check-list")),
|
||||
useValue: function (name, value) {
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
hasChanged: () => true // always included
|
||||
};
|
||||
}
|
||||
};
|
@ -18,31 +18,57 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const syncpipe = require("syncpipe");
|
||||
|
||||
module.exports = function useFormSubmit(form, [mutationQuery, result], { changedOnly = true } = {}) {
|
||||
const [usedAction, setUsedAction] = React.useState();
|
||||
return [
|
||||
function submitForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let action;
|
||||
if (e?.preventDefault) {
|
||||
e.preventDefault();
|
||||
action = e.nativeEvent.submitter.name;
|
||||
} else {
|
||||
action = e;
|
||||
}
|
||||
setUsedAction(action);
|
||||
// transform the field definitions into an object with just their values
|
||||
let updatedFields = [];
|
||||
const mutationData = syncpipe(form, [
|
||||
(_) => Object.values(_),
|
||||
(_) => _.map((field) => {
|
||||
if (!changedOnly || field.hasChanged()) {
|
||||
if (field.selectedValues != undefined) {
|
||||
let selected = field.selectedValues();
|
||||
if (!changedOnly || selected.length > 0) {
|
||||
return [field.name, selected];
|
||||
}
|
||||
} else if (!changedOnly || field.hasChanged()) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, field.value];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
(_) => _.filter((value) => value != null),
|
||||
(_) => Object.fromEntries(_)
|
||||
]);
|
||||
|
||||
return mutationQuery(mutationData);
|
||||
mutationData.action = action;
|
||||
|
||||
return Promise.try(() => {
|
||||
return mutationQuery(mutationData);
|
||||
}).then((res) => {
|
||||
if (res.error == undefined) {
|
||||
updatedFields.forEach((field) => {
|
||||
field.reset();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
result
|
||||
{
|
||||
...result,
|
||||
action: usedAction
|
||||
}
|
||||
];
|
||||
};
|
@ -22,7 +22,7 @@ const React = require("react");
|
||||
const { Link, Route, Redirect } = require("wouter");
|
||||
const { ErrorBoundary } = require("react-error-boundary");
|
||||
|
||||
const ErrorFallback = require("../components/error");
|
||||
const { ErrorFallback } = require("../components/error");
|
||||
const NavButton = require("../components/nav-button");
|
||||
|
||||
function urlSafe(str) {
|
||||
|
65
web/source/settings/lib/import-export.js
Normal file
65
web/source/settings/lib/import-export.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const isValidDomain = require("is-valid-domain");
|
||||
|
||||
function parseDomainList(list) {
|
||||
if (list[0] == "[") {
|
||||
return JSON.parse(list);
|
||||
} else {
|
||||
return list.split("\n").map((line) => {
|
||||
let trimmed = line.trim();
|
||||
return trimmed.length > 0
|
||||
? { domain: trimmed }
|
||||
: null;
|
||||
}).filter((a) => a); // not `null`
|
||||
}
|
||||
}
|
||||
|
||||
function validateDomainList(list) {
|
||||
list.forEach((entry) => {
|
||||
entry.valid = isValidDomain(entry.domain, { wildcard: true, allowUnicode: true });
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function deduplicateDomainList(list) {
|
||||
let domains = new Set();
|
||||
return list.filter((entry) => {
|
||||
if (domains.has(entry.domain)) {
|
||||
return false;
|
||||
} else {
|
||||
domains.add(entry.domain);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function processDomainList(data) {
|
||||
return Promise.try(() => {
|
||||
return parseDomainList(data);
|
||||
}).then((parsed) => {
|
||||
return deduplicateDomainList(parsed);
|
||||
}).then((deduped) => {
|
||||
return validateDomainList(deduped);
|
||||
});
|
||||
};
|
24
web/source/settings/lib/query/admin/federation-bulk.js
Normal file
24
web/source/settings/lib/query/admin/federation-bulk.js
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = (build) => ({
|
||||
// importInstanceBlocks: build.mutation({
|
||||
// })
|
||||
});
|
@ -22,8 +22,8 @@ const {
|
||||
replaceCacheOnMutation,
|
||||
appendCacheOnMutation,
|
||||
spliceCacheOnMutation
|
||||
} = require("./lib");
|
||||
const base = require("./base");
|
||||
} = require("../lib");
|
||||
const base = require("../base");
|
||||
|
||||
const endpoints = (build) => ({
|
||||
updateInstance: build.mutation({
|
||||
@ -70,7 +70,8 @@ const endpoints = (build) => ({
|
||||
return draft.findIndex((block) => block.id == newData.id);
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
...require("./federation-bulk")(build)
|
||||
});
|
||||
|
||||
module.exports = base.injectEndpoints({ endpoints });
|
@ -37,12 +37,14 @@ const endpoints = (build) => ({
|
||||
? [...res.map((emoji) => ({ type: "Emojis", id: emoji.id })), { type: "Emojis", id: "LIST" }]
|
||||
: [{ type: "Emojis", id: "LIST" }]
|
||||
}),
|
||||
|
||||
getEmoji: build.query({
|
||||
query: (id) => ({
|
||||
url: `/api/v1/admin/custom_emojis/${id}`
|
||||
}),
|
||||
providesTags: (res, error, id) => [{ type: "Emojis", id }]
|
||||
}),
|
||||
|
||||
addEmoji: build.mutation({
|
||||
query: (form) => {
|
||||
return {
|
||||
@ -58,6 +60,7 @@ const endpoints = (build) => ({
|
||||
? [{ type: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }]
|
||||
: [{ type: "Emojis", id: "LIST" }]
|
||||
}),
|
||||
|
||||
editEmoji: build.mutation({
|
||||
query: ({ id, ...patch }) => {
|
||||
return {
|
||||
@ -75,6 +78,7 @@ const endpoints = (build) => ({
|
||||
? [{ type: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }]
|
||||
: [{ type: "Emojis", id: "LIST" }]
|
||||
}),
|
||||
|
||||
deleteEmoji: build.mutation({
|
||||
query: (id) => ({
|
||||
method: "DELETE",
|
||||
@ -82,75 +86,73 @@ const endpoints = (build) => ({
|
||||
}),
|
||||
invalidatesTags: (res, error, id) => [{ type: "Emojis", id }]
|
||||
}),
|
||||
|
||||
searchStatusForEmoji: build.mutation({
|
||||
query: (url) => ({
|
||||
method: "GET",
|
||||
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
|
||||
}),
|
||||
transformResponse: (res) => {
|
||||
/* Parses search response, prioritizing a toot result,
|
||||
and returns referenced custom emoji
|
||||
*/
|
||||
let type;
|
||||
queryFn: (url, api, _extraOpts, baseQuery) => {
|
||||
return Promise.try(() => {
|
||||
return baseQuery({
|
||||
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
|
||||
}).then(unwrapRes);
|
||||
}).then((searchRes) => {
|
||||
return emojiFromSearchResult(searchRes);
|
||||
}).then(({ type, domain, list }) => {
|
||||
const state = api.getState();
|
||||
if (domain == new URL(state.oauth.instance).host) {
|
||||
throw "LOCAL_INSTANCE";
|
||||
}
|
||||
|
||||
if (res.statuses.length > 0) {
|
||||
type = "statuses";
|
||||
} else if (res.accounts.length > 0) {
|
||||
type = "accounts";
|
||||
} else {
|
||||
return {
|
||||
type: "none"
|
||||
};
|
||||
}
|
||||
|
||||
let data = res[type][0];
|
||||
|
||||
return {
|
||||
type,
|
||||
domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
|
||||
list: data.emojis
|
||||
};
|
||||
}
|
||||
}),
|
||||
patchRemoteEmojis: build.mutation({
|
||||
queryFn: ({ action, domain, list, category }, api, _extraOpts, baseQuery) => {
|
||||
const data = [];
|
||||
const errors = [];
|
||||
|
||||
return Promise.each(list, (emoji) => {
|
||||
return Promise.try(() => {
|
||||
// search for every mentioned emoji with the admin api to get their ID
|
||||
return Promise.map(list, (emoji) => {
|
||||
return baseQuery({
|
||||
method: "GET",
|
||||
url: `/api/v1/admin/custom_emojis`,
|
||||
params: {
|
||||
filter: `domain:${domain},shortcode:${emoji.shortcode}`,
|
||||
limit: 1
|
||||
}
|
||||
}).then(unwrapRes);
|
||||
}).then(([lookup]) => {
|
||||
if (lookup == undefined) { throw "not found"; }
|
||||
}).then((unwrapRes)).then((list) => list[0]);
|
||||
}, { concurrency: 5 }).then((listWithIDs) => {
|
||||
return {
|
||||
data: {
|
||||
type,
|
||||
domain,
|
||||
list: listWithIDs
|
||||
}
|
||||
};
|
||||
});
|
||||
}).catch((e) => {
|
||||
return { error: e };
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
patchRemoteEmojis: build.mutation({
|
||||
queryFn: ({ action, ...formData }, _api, _extraOpts, baseQuery) => {
|
||||
const data = [];
|
||||
const errors = [];
|
||||
|
||||
return Promise.each(formData.selectedEmoji, (emoji) => {
|
||||
return Promise.try(() => {
|
||||
let body = {
|
||||
type: action
|
||||
};
|
||||
|
||||
if (action == "copy") {
|
||||
body.shortcode = emoji.localShortcode ?? emoji.shortcode;
|
||||
if (category.trim().length != 0) {
|
||||
body.category = category;
|
||||
body.shortcode = emoji.shortcode;
|
||||
if (formData.category.trim().length != 0) {
|
||||
body.category = formData.category;
|
||||
}
|
||||
}
|
||||
|
||||
return baseQuery({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/admin/custom_emojis/${lookup.id}`,
|
||||
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
|
||||
asForm: true,
|
||||
body: body
|
||||
}).then(unwrapRes);
|
||||
}).then((res) => {
|
||||
data.push([emoji.shortcode, res]);
|
||||
}).catch((e) => {
|
||||
console.error("emoji lookup for", emoji.shortcode, "failed:", e);
|
||||
console.error("emoji", action, "for", emoji.shortcode, "failed:", e);
|
||||
let msg = e.message ?? e;
|
||||
if (e.data.error) {
|
||||
msg = e.data.error;
|
||||
@ -171,4 +173,27 @@ const endpoints = (build) => ({
|
||||
})
|
||||
});
|
||||
|
||||
function emojiFromSearchResult(searchRes) {
|
||||
/* Parses the search response, prioritizing a toot result,
|
||||
and returns referenced custom emoji
|
||||
*/
|
||||
let type;
|
||||
|
||||
if (searchRes.statuses.length > 0) {
|
||||
type = "statuses";
|
||||
} else if (searchRes.accounts.length > 0) {
|
||||
type = "accounts";
|
||||
} else {
|
||||
throw "NONE_FOUND";
|
||||
}
|
||||
|
||||
let data = searchRes[type][0];
|
||||
|
||||
return {
|
||||
type,
|
||||
domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
|
||||
list: data.emojis
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = base.injectEndpoints({ endpoints });
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$fa-fw: 1.28571429em; /* Fork-Awesome 'fa-fw' fixed icon width */
|
||||
|
||||
body {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
@ -225,6 +227,7 @@ section.with-sidebar > div, section.with-sidebar > form {
|
||||
|
||||
.button-inline {
|
||||
width: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
@ -344,8 +347,12 @@ form {
|
||||
}
|
||||
|
||||
.form-field.file label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
.label {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
span.form-info {
|
||||
@ -598,6 +605,12 @@ span.form-info {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 8.5rem;
|
||||
width: 8.5rem;
|
||||
@ -608,15 +621,13 @@ span.form-info {
|
||||
}
|
||||
|
||||
.update-category {
|
||||
margin-bottom: 1rem;
|
||||
.combobox-wrapper button {
|
||||
font-size: 1rem;
|
||||
margin: 0.15rem 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: 0.4rem;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -681,8 +692,14 @@ span.form-info {
|
||||
}
|
||||
}
|
||||
|
||||
button .fa.with-text {
|
||||
margin-left: -1.28571429em;
|
||||
button.with-icon {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
padding-right: calc(0.5rem + $fa-fw);
|
||||
|
||||
.fa {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeout {
|
||||
|
Loading…
Reference in New Issue
Block a user