From 7a1e6394831fb07e303c5ed0900dfe1ea4820de5 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 24 Apr 2024 12:12:47 +0200
Subject: [PATCH] [chore] Refactor settings panel routing (and other fixes)
(#2864)
---
.vscode/settings.json | 13 +-
web/source/index.js | 2 +-
web/source/package.json | 8 +-
.../admin/domain-permissions/index.tsx | 49 -----
.../settings/admin/emoji/category-select.jsx | 96 ---------
.../settings/admin/emoji/local/overview.js | 153 -------------
web/source/settings/admin/settings/rules.tsx | 174 ---------------
.../settings/components/account-list.tsx | 6 +-
.../settings/components/back-button.jsx | 12 +-
web/source/settings/index.js | 124 -----------
web/source/settings/index.tsx | 84 ++++++++
.../settings/lib/navigation/components.jsx | 201 ------------------
web/source/settings/lib/navigation/error.tsx | 98 +++++++++
web/source/settings/lib/navigation/index.js | 136 ------------
web/source/settings/lib/navigation/menu.tsx | 175 +++++++++++++++
web/source/settings/lib/navigation/util.ts | 45 +++-
web/source/settings/lib/query/admin/index.ts | 13 +-
web/source/settings/lib/types/custom-emoji.ts | 8 +
.../local/index.tsx => lib/types/rules.ts} | 24 +--
web/source/settings/style.css | 23 +-
.../admin/actions/keys/expireremote.tsx | 13 +-
.../{ => views}/admin/actions/keys/index.tsx | 0
.../admin/actions/media/cleanup.tsx | 12 +-
.../{ => views}/admin/actions/media/index.tsx | 0
.../views/admin/emoji/category-select.tsx | 134 ++++++++++++
.../admin/emoji/local/detail.tsx} | 54 +++--
.../admin/emoji/local/new-emoji.tsx | 20 +-
.../views/admin/emoji/local/overview.tsx | 173 +++++++++++++++
.../admin/emoji/local/use-shortcode.ts} | 12 +-
.../{ => views}/admin/emoji/remote/index.tsx | 26 +--
.../admin/emoji/remote/steal-this-look.tsx} | 26 +--
web/source/settings/views/admin/routes.tsx | 177 +++++++++++++++
.../{ => views}/admin/settings/index.tsx | 24 +--
.../settings/views/admin/settings/rules.tsx | 151 +++++++++++++
.../moderation}/accounts/detail/actions.tsx | 12 +-
.../accounts/detail/handlesignup.tsx | 18 +-
.../moderation}/accounts/detail/index.tsx | 52 ++---
.../moderation}/accounts/index.tsx | 16 +-
.../moderation}/accounts/pending/index.tsx | 4 +-
.../moderation}/accounts/search/index.tsx | 26 ++-
.../moderation}/domain-permissions/detail.tsx | 68 +++---
.../export-format-table.tsx} | 8 +-
.../moderation}/domain-permissions/form.tsx | 12 +-
.../domain-permissions/import-export.tsx | 26 ++-
.../domain-permissions/overview.tsx | 61 +++---
.../domain-permissions/process.tsx | 42 ++--
.../moderation}/reports/detail.tsx | 51 ++---
.../moderation/reports/overview.tsx} | 74 +++----
.../moderation}/reports/username.tsx | 0
.../settings/views/moderation/routes.tsx | 201 ++++++++++++++++++
.../settings/{ => views}/user/migration.tsx | 64 +++---
.../settings/{ => views}/user/profile.tsx | 22 +-
web/source/settings/views/user/routes.tsx | 80 +++++++
.../settings/{ => views}/user/settings.tsx | 52 +++--
web/source/yarn.lock | 78 +++++--
55 files changed, 1788 insertions(+), 1445 deletions(-)
delete mode 100644 web/source/settings/admin/domain-permissions/index.tsx
delete mode 100644 web/source/settings/admin/emoji/category-select.jsx
delete mode 100644 web/source/settings/admin/emoji/local/overview.js
delete mode 100644 web/source/settings/admin/settings/rules.tsx
delete mode 100644 web/source/settings/index.js
create mode 100644 web/source/settings/index.tsx
delete mode 100644 web/source/settings/lib/navigation/components.jsx
create mode 100644 web/source/settings/lib/navigation/error.tsx
delete mode 100644 web/source/settings/lib/navigation/index.js
create mode 100644 web/source/settings/lib/navigation/menu.tsx
rename web/source/settings/{admin/emoji/local/index.tsx => lib/types/rules.ts} (70%)
rename web/source/settings/{ => views}/admin/actions/keys/expireremote.tsx (85%)
rename web/source/settings/{ => views}/admin/actions/keys/index.tsx (100%)
rename web/source/settings/{ => views}/admin/actions/media/cleanup.tsx (84%)
rename web/source/settings/{ => views}/admin/actions/media/index.tsx (100%)
create mode 100644 web/source/settings/views/admin/emoji/category-select.tsx
rename web/source/settings/{admin/emoji/local/detail.js => views/admin/emoji/local/detail.tsx} (74%)
rename web/source/settings/{ => views}/admin/emoji/local/new-emoji.tsx (86%)
create mode 100644 web/source/settings/views/admin/emoji/local/overview.tsx
rename web/source/settings/{admin/emoji/local/use-shortcode.js => views/admin/emoji/local/use-shortcode.ts} (85%)
rename web/source/settings/{ => views}/admin/emoji/remote/index.tsx (65%)
rename web/source/settings/{admin/emoji/remote/parse-from-toot.tsx => views/admin/emoji/remote/steal-this-look.tsx} (87%)
create mode 100644 web/source/settings/views/admin/routes.tsx
rename web/source/settings/{ => views}/admin/settings/index.tsx (88%)
create mode 100644 web/source/settings/views/admin/settings/rules.tsx
rename web/source/settings/{admin => views/moderation}/accounts/detail/actions.tsx (86%)
rename web/source/settings/{admin => views/moderation}/accounts/detail/handlesignup.tsx (85%)
rename web/source/settings/{admin => views/moderation}/accounts/detail/index.tsx (78%)
rename web/source/settings/{admin => views/moderation}/accounts/index.tsx (79%)
rename web/source/settings/{admin => views/moderation}/accounts/pending/index.tsx (90%)
rename web/source/settings/{admin => views/moderation}/accounts/search/index.tsx (82%)
rename web/source/settings/{admin => views/moderation}/domain-permissions/detail.tsx (78%)
rename web/source/settings/{admin/domain-permissions/export-format-table.jsx => views/moderation/domain-permissions/export-format-table.tsx} (91%)
rename web/source/settings/{admin => views/moderation}/domain-permissions/form.tsx (92%)
rename web/source/settings/{admin => views/moderation}/domain-permissions/import-export.tsx (79%)
rename web/source/settings/{admin => views/moderation}/domain-permissions/overview.tsx (79%)
rename web/source/settings/{admin => views/moderation}/domain-permissions/process.tsx (91%)
rename web/source/settings/{admin => views/moderation}/reports/detail.tsx (87%)
rename web/source/settings/{admin/reports/index.tsx => views/moderation/reports/overview.tsx} (63%)
rename web/source/settings/{admin => views/moderation}/reports/username.tsx (100%)
create mode 100644 web/source/settings/views/moderation/routes.tsx
rename web/source/settings/{ => views}/user/migration.tsx (81%)
rename web/source/settings/{ => views}/user/profile.tsx (92%)
create mode 100644 web/source/settings/views/user/routes.tsx
rename web/source/settings/{ => views}/user/settings.tsx (76%)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b2adc1cb8..beb76548d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,14 @@
},
"eslint.workingDirectories": ["web/source"],
"eslint.lintTask.enable": true,
- "eslint.lintTask.options": "${workspaceFolder}/web/source"
-}
\ No newline at end of file
+ "eslint.lintTask.options": "${workspaceFolder}/web/source",
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ "typescript",
+ "typescriptreact"
+ ],
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ }
+}
diff --git a/web/source/index.js b/web/source/index.js
index dcdb701ee..d9ef70ff9 100644
--- a/web/source/index.js
+++ b/web/source/index.js
@@ -78,7 +78,7 @@ skulk({
// commonjs here, no need for the typescript preset.
["babelify", {
global: true,
- ignore: [/node_modules\/(?!nanoid)/],
+ ignore: [/node_modules\/(?!(nanoid)|(wouter))/],
}]
],
presets: [
diff --git a/web/source/package.json b/web/source/package.json
index 20f525228..d72cf7764 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -13,7 +13,6 @@
"dependencies": {
"@reduxjs/toolkit": "^1.8.6",
"ariakit": "^2.0.0-next.41",
- "bluebird": "^3.7.2",
"get-by-dot": "^1.0.2",
"is-valid-domain": "^0.1.6",
"js-file-download": "^0.4.12",
@@ -33,9 +32,7 @@
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"skulk": "^0.0.8-fix",
- "split-filter-n": "^1.1.3",
- "syncpipe": "^1.0.0",
- "wouter": "^2.8.0-alpha.2"
+ "wouter": "^3.1.0"
},
"devDependencies": {
"@babel/core": "^7.23.0",
@@ -45,14 +42,13 @@
"@browserify/envify": "^6.0.0",
"@browserify/uglifyify": "^6.0.0",
"@joepie91/eslint-config": "^1.1.1",
- "@types/bluebird": "^3.5.39",
"@types/is-valid-domain": "^0.0.2",
"@types/papaparse": "^5.3.9",
"@types/psl": "^1.1.1",
"@types/react-dom": "^18.2.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
- "autoprefixer": "^10.4.13",
+ "autoprefixer": "^10.4.19",
"babelify": "^10.0.0",
"css-extract": "^2.0.0",
"eslint": "^8.26.0",
diff --git a/web/source/settings/admin/domain-permissions/index.tsx b/web/source/settings/admin/domain-permissions/index.tsx
deleted file mode 100644
index 7d790cfc8..000000000
--- a/web/source/settings/admin/domain-permissions/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- 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 .
-*/
-
-import React from "react";
-import { Switch, Route } from "wouter";
-
-import DomainPermissionsOverview from "./overview";
-import { PermType } from "../../lib/types/domain-permission";
-import DomainPermDetail from "./detail";
-
-export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
- return (
-
-
- {params => (
-
- )}
-
-
- {params => (
-
- )}
-
-
- );
-}
diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx
deleted file mode 100644
index e5cf29939..000000000
--- a/web/source/settings/admin/emoji/category-select.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- 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 .
-*/
-
-const React = require("react");
-const splitFilterN = require("split-filter-n");
-const syncpipe = require('syncpipe');
-const { matchSorter } = require("match-sorter");
-
-const ComboBox = require("../../components/combo-box");
-const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji");
-
-function useEmojiByCategory(emoji) {
- // split all emoji over an object keyed by the category names (or Unsorted)
- return React.useMemo(() => splitFilterN(
- emoji,
- [],
- (entry) => entry.category ?? "Unsorted"
- ), [emoji]);
-}
-
-function CategorySelect({ field, children }) {
- const { value, setIsNew } = field;
-
- const {
- data: emoji = [],
- isLoading,
- isSuccess,
- error
- } = useListEmojiQuery({ filter: "domain:local" });
-
- const emojiByCategory = useEmojiByCategory(emoji);
-
- const categories = React.useMemo(() => new Set(Object.keys(emojiByCategory)), [emojiByCategory]);
-
- // data used by the ComboBox element to select an emoji category
- const categoryItems = React.useMemo(() => {
- return syncpipe(emojiByCategory, [
- (_) => Object.keys(_), // just emoji category names
- (_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm
- (_) => _.map((categoryName) => [ // map to input value, and selectable element with icon
- categoryName,
- <>
-
- {categoryName}
- >
- ])
- ]);
- }, [emojiByCategory, value]);
-
- React.useEffect(() => {
- if (value != undefined && isSuccess && value.trim().length > 0) {
- setIsNew(!categories.has(value.trim()));
- }
- }, [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 (
- <>
- { field.value = e.target.value; }} />;
- >
- );
- } else if (isLoading) {
- return ;
- }
-
- return (
-
- );
-}
-
-module.exports = {
- useEmojiByCategory,
- CategorySelect
-};
\ No newline at end of file
diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js
deleted file mode 100644
index 45bfd614d..000000000
--- a/web/source/settings/admin/emoji/local/overview.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- 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 .
-*/
-
-const React = require("react");
-const { Link } = require("wouter");
-const syncpipe = require("syncpipe");
-const { matchSorter } = require("match-sorter");
-
-const NewEmojiForm = require("./new-emoji").default;
-const { useTextInput } = require("../../../lib/form");
-
-const { useEmojiByCategory } = require("../category-select");
-const { useBaseUrl } = require("../../../lib/navigation/util");
-
-const Loading = require("../../../components/loading");
-const { Error } = require("../../../components/error");
-const { TextInput } = require("../../../components/form/inputs");
-const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
-
-module.exports = function EmojiOverview({ }) {
- const {
- data: emoji = [],
- isLoading,
- isError,
- error
- } = useListEmojiQuery({ filter: "domain:local" });
-
- let content = null;
-
- if (isLoading) {
- content = ;
- } else if (isError) {
- content = ;
- } else {
- content = (
- <>
-
-
- >
- );
- }
-
- return (
- <>
-
Local Custom Emoji
-
- 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 Remote Emoji page.
-
-
- Be warned! 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).
-
- );
-}
\ No newline at end of file
diff --git a/web/source/settings/admin/settings/rules.tsx b/web/source/settings/admin/settings/rules.tsx
deleted file mode 100644
index e5e4d17c5..000000000
--- a/web/source/settings/admin/settings/rules.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- 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 .
-*/
-
-import React from "react";
-import { Switch, Route, Link, Redirect, useRoute } from "wouter";
-
-import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../lib/query";
-import FormWithData from "../../lib/form/form-with-data";
-import { useBaseUrl } from "../../lib/navigation/util";
-
-import { useValue, useTextInput } from "../../lib/form";
-import useFormSubmit from "../../lib/form/submit";
-
-import { TextArea } from "../../components/form/inputs";
-import MutationButton from "../../components/form/mutation-button";
-import { Error } from "../../components/error";
-
-export default function InstanceRulesData({ baseUrl }) {
- return (
-
- );
-}
-
-function InstanceRules({ baseUrl, data: rules }) {
- return (
-
-
-
-
-
-
-
Instance Rules
-
-
- The rules for your instance are listed on the about page, and can be selected when submitting reports.
-
);
-}
\ No newline at end of file
+}
diff --git a/web/source/settings/components/back-button.jsx b/web/source/settings/components/back-button.jsx
index 05306e2ea..bf9038b2b 100644
--- a/web/source/settings/components/back-button.jsx
+++ b/web/source/settings/components/back-button.jsx
@@ -17,13 +17,11 @@
along with this program. If not, see .
*/
-const React = require("react");
-const { Link } = require("wouter");
+import React from "react";
+import { Link } from "wouter";
-module.exports = function BackButton({ to }) {
+export default function BackButton({ to }) {
return (
-
- < back
-
+ < back
);
-};
\ No newline at end of file
+}
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
deleted file mode 100644
index 6ab0ab6ec..000000000
--- a/web/source/settings/index.js
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- 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 .
-*/
-
-const React = require("react");
-const ReactDom = require("react-dom/client");
-const { Provider } = require("react-redux");
-const { PersistGate } = require("redux-persist/integration/react");
-
-const { store, persistor } = require("./redux/store");
-const { createNavigation, Menu, Item } = require("./lib/navigation");
-
-const { Authorization } = require("./components/authorization");
-const Loading = require("./components/loading");
-const UserLogoutCard = require("./components/user-logout-card");
-const { RoleContext } = require("./lib/navigation/util");
-
-const UserProfile = require("./user/profile").default;
-const UserSettings = require("./user/settings").default;
-const UserMigration = require("./user/migration").default;
-
-const Reports = require("./admin/reports").default;
-
-const Accounts = require("./admin/accounts").default;
-const AccountsPending = require("./admin/accounts/pending").default;
-
-const DomainPerms = require("./admin/domain-permissions").default;
-const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
-
-const AdminMedia = require("./admin/actions/media").default;
-const AdminKeys = require("./admin/actions/keys").default;
-
-const LocalEmoji = require("./admin/emoji/local").default;
-const RemoteEmoji = require("./admin/emoji/remote").default;
-
-const InstanceSettings = require("./admin/settings").default;
-const InstanceRules = require("./admin/settings/rules").default;
-
-require("./style.css");
-
-const { Sidebar, ViewRouter } = createNavigation("/settings", [
- Menu("User", [
- Item("Profile", { icon: "fa-user" }, UserProfile),
- Item("Settings", { icon: "fa-cogs" }, UserSettings),
- Item("Migration", { icon: "fa-exchange" }, UserMigration),
- ]),
- Menu("Moderation", {
- url: "admin",
- permissions: ["admin"]
- }, [
- Item("Reports", { icon: "fa-flag", wildcard: true }, Reports),
- Item("Accounts", { icon: "fa-users", wildcard: true }, [
- Item("Overview", { icon: "fa-list", url: "", wildcard: true }, Accounts),
- Item("Pending", { icon: "fa-question", url: "pending", wildcard: true }, AccountsPending),
- ]),
- Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
- Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
- Item("Allows", { icon: "fa-check", url: "allow", wildcard: true }, DomainPerms),
- Item("Import/Export", { icon: "fa-floppy-o", url: "import-export", wildcard: true }, DomainPermsImportExport),
- ]),
- ]),
- Menu("Administration", {
- url: "admin",
- defaultUrl: "/settings/admin/settings",
- permissions: ["admin"]
- }, [
- Menu("Actions", { icon: "fa-bolt" }, [
- Item("Media", { icon: "fa-photo" }, AdminMedia),
- Item("Keys", { icon: "fa-key-modern" }, AdminKeys),
- ]),
- Menu("Custom Emoji", { icon: "fa-smile-o" }, [
- Item("Local", { icon: "fa-home", wildcard: true }, LocalEmoji),
- Item("Remote", { icon: "fa-cloud" }, RemoteEmoji),
- ]),
- Menu("Settings", { icon: "fa-sliders" }, [
- Item("Settings", { icon: "fa-sliders", url: "" }, InstanceSettings),
- Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, InstanceRules),
- ]),
- ])
-]);
-
-function App({ account }) {
- const permissions = [account.role.name];
-
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-
-function Main() {
- return (
-
- } persistor={persistor}>
-
-
-
- );
-}
-
-const root = ReactDom.createRoot(document.getElementById("root"));
-root.render();
\ No newline at end of file
diff --git a/web/source/settings/index.tsx b/web/source/settings/index.tsx
new file mode 100644
index 000000000..d0af8524d
--- /dev/null
+++ b/web/source/settings/index.tsx
@@ -0,0 +1,84 @@
+/*
+ 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 .
+*/
+
+import React, { StrictMode } from "react";
+import "./style.css";
+
+import { createRoot } from "react-dom/client";
+import { Provider } from "react-redux";
+import { PersistGate } from "redux-persist/integration/react";
+import { store, persistor } from "./redux/store";
+import { Authorization } from "./components/authorization";
+import Loading from "./components/loading";
+import { Account } from "./lib/types/account";
+import { BaseUrlContext, RoleContext } from "./lib/navigation/util";
+import { SidebarMenu } from "./lib/navigation/menu";
+import { UserMenu, UserRouter } from "./views/user/routes";
+import { ModerationMenu, ModerationRouter } from "./views/moderation/routes";
+import { AdminMenu, AdminRouter } from "./views/admin/routes";
+import { Redirect, Route, Router } from "wouter";
+
+interface AppProps {
+ account: Account;
+}
+
+export function App({ account }: AppProps) {
+ const roles: string[] = [ account.role.name ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ Redirect to first part of UserRouter if
+ just the bare settings page is open, so
+ user isn't greeted with a blank page.
+ */}
+
+
+
+
+
+ );
+}
+
+function Main() {
+ return (
+
+ }
+ persistor={persistor}
+ >
+
+
+
+ );
+}
+
+const root = createRoot(document.getElementById("root") as HTMLElement);
+root.render();
diff --git a/web/source/settings/lib/navigation/components.jsx b/web/source/settings/lib/navigation/components.jsx
deleted file mode 100644
index 64ed160b6..000000000
--- a/web/source/settings/lib/navigation/components.jsx
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- 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 .
-*/
-
-const React = require("react");
-const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter");
-const syncpipe = require("syncpipe");
-
-const {
- RoleContext,
- useHasPermission,
- checkPermission,
- BaseUrlContext
-} = require("./util");
-
-const ActiveRouteCtx = React.createContext();
-function useActiveRoute() {
- return React.useContext(ActiveRouteCtx);
-}
-
-function Sidebar(menuTree, routing) {
- const components = menuTree.map((m) => m.MenuEntry);
-
- return function SidebarComponent() {
- const router = useRouter();
- const [location] = useLocation();
-
- let activeRoute = routing.find((l) => {
- let [match] = router.matcher(l.routingUrl, location);
- return match;
- })?.routingUrl;
-
- return (
-
- );
- };
-}
-
-function ViewRouter(routing, defaultRoute) {
- return function ViewRouterComponent() {
- const permissions = React.useContext(RoleContext);
-
- const filteredRoutes = React.useMemo(() => {
- return syncpipe(routing, [
- (_) => _.filter((route) => checkPermission(route.permissions, permissions)),
- (_) => _.map((route) => {
- return (
-
-
- {/* FIXME: implement reset */}
-
- {route.view}
-
-
-
- );
- })
- ]);
- }, [permissions]);
-
- return (
-
- {filteredRoutes}
-
-
- );
- };
-}
-
-function MenuComponent({ type, name, url, icon, permissions, links, level, children }) {
- const activeRoute = useActiveRoute();
-
- if (!useHasPermission(permissions)) {
- return null;
- }
-
- const classes = [type];
-
- if (level == 0) {
- classes.push("top-level");
- } else if (level == 1) {
- classes.push("expanding");
- } else {
- classes.push("nested");
- }
-
- const isActive = links.includes(activeRoute);
- if (isActive) {
- classes.push("active");
- }
-
- const className = classes.join(" ");
-
- return (
-
- );
-}
-
-module.exports = {
- Sidebar,
- ViewRouter,
- MenuComponent
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/navigation/error.tsx b/web/source/settings/lib/navigation/error.tsx
new file mode 100644
index 000000000..08edc83f0
--- /dev/null
+++ b/web/source/settings/lib/navigation/error.tsx
@@ -0,0 +1,98 @@
+/*
+ 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 .
+*/
+
+import React, { Component, ReactNode } from "react";
+
+
+interface ErrorBoundaryProps {
+ children?: ReactNode;
+}
+
+interface ErrorBoundaryState {
+ hadError?: boolean;
+ componentStack?;
+ error?;
+}
+
+class ErrorBoundary extends Component {
+ resetErrorBoundary: () => void;
+
+ constructor(props: ErrorBoundaryProps) {
+ super(props);
+ this.state = {};
+ this.resetErrorBoundary = () => {
+ this.setState({});
+ };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hadError: true, error };
+ }
+
+ componentDidCatch(_e, info) {
+ this.setState({
+ ...this.state,
+ componentStack: info.componentStack
+ });
+ }
+
+ render() {
+ if (this.state.hadError) {
+ return (
+
+ );
+ } else {
+ return this.props.children;
+ }
+ }
+}
+
+function ErrorFallback({ error, componentStack, resetErrorBoundary }) {
+ return (
+
+ );
+}
+
+export { ErrorBoundary };
diff --git a/web/source/settings/lib/navigation/index.js b/web/source/settings/lib/navigation/index.js
deleted file mode 100644
index 2e8f062f4..000000000
--- a/web/source/settings/lib/navigation/index.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- 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 .
-*/
-
-const React = require("react");
-const { nanoid } = require("nanoid");
-const { Redirect } = require("wouter");
-
-const { urlSafe } = require("./util");
-
-const {
- Sidebar,
- ViewRouter,
- MenuComponent
-} = require("./components");
-
-function createNavigation(rootUrl, menus) {
- const root = {
- url: rootUrl,
- links: [],
- };
-
- const routing = [];
-
- const menuTree = menus.map((creatorFunc) =>
- creatorFunc(root, routing)
- );
-
- return {
- Sidebar: Sidebar(menuTree, routing),
- ViewRouter: ViewRouter(routing, root.redirectUrl)
- };
-}
-
-function MenuEntry(name, opts, contents) {
- if (contents == undefined) { // opts argument is optional
- contents = opts;
- opts = {};
- }
-
- return function createMenuEntry(root, routing) {
- const type = Array.isArray(contents) ? "category" : "view";
-
- let urlParts = [root.url];
- if (opts.url != "") {
- urlParts.push(opts.url ?? urlSafe(name));
- }
-
- const url = urlParts.join("/");
- let routingUrl = url;
-
- if (opts.wildcard) {
- routingUrl += "/:wildcard*";
- }
-
- const entry = {
- name, type,
- url, routingUrl,
- key: nanoid(),
- permissions: opts.permissions ?? false,
- icon: opts.icon,
- links: [routingUrl],
- level: (root.level ?? -1) + 1,
- redirectUrl: opts.defaultUrl
- };
-
- if (type == "category") {
- let entries = contents.map((creatorFunc) => creatorFunc(entry, routing));
- let routes = [];
-
- entries.forEach((e) => {
- // move empty wildcard routes to end of category, to prevent overlap
- if (e.url == entry.url) {
- routes.unshift(e);
- } else {
- routes.push(e);
- }
- });
- routes.reverse();
-
- routing.push(...routes);
-
- if (opts.redirectUrl != entry.url) {
- routing.push({
- key: entry.key,
- url: entry.url,
- permissions: entry.permissions,
- routingUrl: entry.redirectUrl + "/:fallback*",
- view: React.createElement(Redirect, { to: entry.redirectUrl })
- });
- entry.url = entry.redirectUrl;
- }
-
- root.links.push(...entry.links);
-
- entry.MenuEntry = React.createElement(
- MenuComponent,
- entry,
- entries.map((e) => e.MenuEntry)
- );
- } else {
- entry.links.push(routingUrl);
- root.links.push(routingUrl);
-
- entry.view = React.createElement(contents, { baseUrl: url });
- entry.MenuEntry = React.createElement(MenuComponent, entry);
- }
-
- if (root.redirectUrl == undefined) {
- root.redirectUrl = entry.url;
- }
-
- return entry;
- };
-}
-
-module.exports = {
- createNavigation,
- Menu: MenuEntry,
- Item: MenuEntry
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/navigation/menu.tsx b/web/source/settings/lib/navigation/menu.tsx
new file mode 100644
index 000000000..514e3ea2f
--- /dev/null
+++ b/web/source/settings/lib/navigation/menu.tsx
@@ -0,0 +1,175 @@
+/*
+ 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 .
+*/
+
+import React, { PropsWithChildren } from "react";
+import { Link, useRoute } from "wouter";
+import {
+ BaseUrlContext,
+ MenuLevelContext,
+ useBaseUrl,
+ useHasPermission,
+ useMenuLevel,
+} from "./util";
+import UserLogoutCard from "../../components/user-logout-card";
+import { nanoid } from "nanoid";
+
+export interface MenuItemProps {
+ /**
+ * Name / title of this menu item.
+ */
+ name?: string;
+
+ /**
+ * Url path component for this menu item.
+ */
+ itemUrl: string;
+
+ /**
+ * If this menu item is a category containing
+ * children, which child should be selected by
+ * default when category title is clicked.
+ *
+ * Optional, use for categories only.
+ */
+ defaultChild?: string;
+
+ /**
+ * Permissions required to access this
+ * menu item (none, "moderator", "admin").
+ */
+ permissions?: string[];
+
+ /**
+ * Fork-awesome string to render
+ * icon for this menu item.
+ */
+ icon?: string;
+}
+
+export function MenuItem(props: PropsWithChildren) {
+ const {
+ name,
+ itemUrl,
+ defaultChild,
+ permissions,
+ icon,
+ children,
+ } = props;
+
+ // Derive where this item is
+ // in terms of URL routing.
+ const baseUrl = useBaseUrl();
+ const thisUrl = [ baseUrl, itemUrl ].join('/');
+
+ // Derive where this item is in
+ // terms of nesting within the menu.
+ const thisLevel = useMenuLevel();
+ const nextLevel = thisLevel+1;
+ const topLevel = thisLevel === 0;
+
+ // Check whether this item is currently active
+ // (ie., user has selected it in the menu).
+ //
+ // This uses a wildcard to mark both parent
+ // and relevant child as active.
+ //
+ // See:
+ // https://github.com/molefrog/wouter?tab=readme-ov-file#useroute-route-matching-and-parameters
+ const [isActive] = useRoute([ thisUrl, "*?" ].join("/"));
+
+ // Don't render item if logged-in user
+ // doesn't have permissions to use it.
+ if (!useHasPermission(permissions)) {
+ return null;
+ }
+
+ // Check whether this item has children.
+ const hasChildren = children !== undefined;
+ const childrenArray = hasChildren && Array.isArray(children);
+
+ // Class name of the item varies depending
+ // on where it is in the menu, and whether
+ // it has children beneath it or not.
+ const classNames: string[] = [];
+ if (topLevel) {
+ classNames.push("category", "top-level");
+ } else {
+ if (thisLevel === 1 && hasChildren) {
+ classNames.push("category", "expanding");
+ } else if (thisLevel === 1 && !hasChildren) {
+ classNames.push("view", "expanding");
+ } else if (thisLevel === 2) {
+ classNames.push("view", "nested");
+ }
+ }
+
+ if (isActive) {
+ classNames.push("active");
+ }
+
+ let content: React.JSX.Element | null;
+ if ((isActive || topLevel) && childrenArray) {
+ // Render children as a nested list.
+ content =
{children}
;
+ } else if (isActive && hasChildren) {
+ // Render child as solo element.
+ content = <>{children}>;
+ } else {
+ // Not active: hide children.
+ content = null;
+ }
+
+ // If a default child is defined, this item should point to that.
+ const href = defaultChild ? [ thisUrl, defaultChild ].join("/") : thisUrl;
+
+ return (
+