diff --git a/web/source/package.json b/web/source/package.json index a12d6fe77..8a561af42 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -34,6 +34,7 @@ "pretty-bytes": "^5.6.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-error-boundary": "^3.1.4", "reactify": "^1.1.1", "uglifyify": "^5.0.2", "wouter": "^2.8.0-alpha.2" diff --git a/web/source/settings-panel/admin/index.js b/web/source/settings-panel/admin/index.js new file mode 100644 index 000000000..551f71af2 --- /dev/null +++ b/web/source/settings-panel/admin/index.js @@ -0,0 +1,33 @@ +/* + 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 { Route, Switch } = require("wouter"); + +module.exports = function AdminPanel({oauth, routes}) { + return ( + <Switch> + {routes.map(([path, component]) => { + return <Route key={path} path={path} component={component}/>; + })} + </Switch> + ); +}; \ No newline at end of file diff --git a/web/source/settings-panel/components/error.js b/web/source/settings-panel/components/error.js new file mode 100644 index 000000000..e5e0ff139 --- /dev/null +++ b/web/source/settings-panel/components/error.js @@ -0,0 +1,45 @@ +/* + 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"); + +module.exports = function ErrorFallback({error, resetErrorBoundary}) { + return ( + <div className="error"> + <p> + {"An error occured, please report this on the "} + <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: + </p> + <pre> + {error.name}: {error.message} + </pre> + <pre> + {error.stack} + </pre> + <p> + <button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a> + </p> + </div> + ); +}; \ No newline at end of file diff --git a/web/source/lib/submit.js b/web/source/settings-panel/components/submit.js similarity index 100% rename from web/source/lib/submit.js rename to web/source/settings-panel/components/submit.js diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js index c2586d12e..a69819f9c 100644 --- a/web/source/settings-panel/index.js +++ b/web/source/settings-panel/index.js @@ -22,23 +22,35 @@ const Promise = require("bluebird"); const React = require("react"); const ReactDom = require("react-dom"); const { Link, Route, Switch, useRoute, Redirect } = require("wouter"); +const { ErrorBoundary } = require("react-error-boundary"); const Auth = require("./components/auth"); +const ErrorFallback = require("./components/error"); + const oauthLib = require("./lib/oauth"); require("./style.css"); +const UserPanel = require("./user"); +const AdminPanel = require("./admin"); + const nav = { - "User": [ - ["Profile", require("./user/profile.js")], - ["Settings", require("./user/settings.js")], - ["Customization", require("./user/customization.js")] - ], - "Admin": [ - ["Instance Settings", require("./admin/settings.js")], - ["Federation", require("./admin/federation.js")], - ["Customization", require("./admin/customization.js")] - ] + "User": { + Component: require("./user"), + entries: { + "Profile": require("./user/profile.js"), + "Settings": require("./user/settings.js"), + "Customization": require("./user/customization.js") + } + }, + "Admin": { + Component: require("./admin"), + entries: { + "Instance Settings": require("./admin/settings.js"), + "Federation": require("./admin/federation.js"), + "Customization": require("./admin/customization.js") + } + } }; function urlSafe(str) { @@ -49,31 +61,50 @@ function urlSafe(str) { const sidebar = []; const panelRouter = []; -Object.entries(nav).forEach(([category, entries]) => { - let base = `/settings/${urlSafe(category)}`; +// Generate component tree from `nav` object once, as it won't change +Object.entries(nav).forEach(([name, {Component, entries}]) => { + let base = `/settings/${urlSafe(name)}`; + + let links = []; + let routes = []; + + let firstRoute; + + Object.entries(entries).forEach(([name, component]) => { + let url = `${base}/${urlSafe(name)}`; + + if (firstRoute == undefined) { + firstRoute = `${base}/${urlSafe(name)}`; + } + + routes.push([url, component]); + + links.push( + <NavButton key={url} href={url} name={name} /> + ); + }); - // Category header goes to first page in category panelRouter.push( <Route key={base} path={base}> - <Redirect to={`${base}/${urlSafe(entries[0][0])}`}/> + <Redirect to={firstRoute}/> </Route> ); - let links = entries.map(([name, component]) => { - let url = `${base}/${urlSafe(name)}`; - - panelRouter.push( - <Route key={url} path={url} component={component}/> - ); - - return <NavButton key={url} href={url} name={name} />; - }); + let childrenPath = `${base}/:section`; + panelRouter.push( + <Route key={childrenPath} path={childrenPath}> + <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => {}}> + {/* FIXME: implement onReset */} + <Component routes={routes}/> + </ErrorBoundary> + </Route> + ); sidebar.push( - <React.Fragment key={category}> - <Link href={`${base}/${urlSafe(entries[0][0])}`}> + <React.Fragment key={name}> + <Link href={firstRoute}> <a> - <h2>{category}</h2> + <h2>{name}</h2> </a> </Link> <nav> diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css index 36c7708f6..051038c58 100644 --- a/web/source/settings-panel/style.css +++ b/web/source/settings-panel/style.css @@ -129,6 +129,13 @@ input, select, textarea { .error { font-weight: bold; + + pre { + background: $bg; + padding: 1rem; + overflow: auto; + margin: 0; + } } .hidden { diff --git a/web/source/settings-panel/user/index.js b/web/source/settings-panel/user/index.js new file mode 100644 index 000000000..dddd3118c --- /dev/null +++ b/web/source/settings-panel/user/index.js @@ -0,0 +1,51 @@ +/* + 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 { Route, Switch } = require("wouter"); + +module.exports = function UserPanel({oauth, routes}) { + // const [account, setAccount] = React.useState({}); + // const [errorMsg, setError] = React.useState(""); + // const [statusMsg, setStatus] = React.useState("Fetching user info"); + + // React.useEffect(() => { + // Promise.try(() => { + // return oauth.apiRequest("/api/v1/accounts/verify_credentials", "GET"); + // }).then((json) => { + // setAccount(json); + // }).catch((e) => { + // setError(e.message); + // setStatus(""); + // }); + // }, [oauth, setAccount, setError, setStatus]); + + // throw new Error("test"); + + return ( + <Switch> + {routes.map(([path, component]) => { + console.log(component); + return <Route key={path} path={path} component={component}/>; + })} + </Switch> + ); +}; \ No newline at end of file diff --git a/web/source/settings-panel/user/profile.js b/web/source/settings-panel/user/profile.js index 517f60cdc..0127ca08b 100644 --- a/web/source/settings-panel/user/profile.js +++ b/web/source/settings-panel/user/profile.js @@ -18,6 +18,121 @@ "use strict"; -module.exports = function UserProfile() { - return "user profile"; +const Promise = require("bluebird"); +const React = require("react"); +const { useErrorHandler } = require("react-error-boundary"); + +const Submit = require("../components/submit"); + +module.exports = function UserProfile({account, oauth}) { + const [errorMsg, setError] = React.useState(""); + const [statusMsg, setStatus] = React.useState(""); + + const [headerFile, setHeaderFile] = React.useState(undefined); + const [headerSrc, setHeaderSrc] = React.useState(""); + + const [avatarFile, setAvatarFile] = React.useState(undefined); + const [avatarSrc, setAvatarSrc] = React.useState(""); + + const [displayName, setDisplayName] = React.useState(""); + const [bio, setBio] = React.useState(""); + const [locked, setLocked] = React.useState(false); + + React.useEffect(() => { + setHeaderSrc(account.header); + setAvatarSrc(account.avatar); + + setDisplayName(account.display_name); + setBio(account.source ? account.source.note : ""); + setLocked(account.locked); + }, [account, setHeaderSrc, setAvatarSrc, setDisplayName, setBio, setLocked]); + + const headerOnChange = (e) => { + setHeaderFile(e.target.files[0]); + setHeaderSrc(URL.createObjectURL(e.target.files[0])); + }; + + const avatarOnChange = (e) => { + setAvatarFile(e.target.files[0]); + setAvatarSrc(URL.createObjectURL(e.target.files[0])); + }; + + const submit = (e) => { + e.preventDefault(); + + setStatus("PATCHing"); + setError(""); + return Promise.try(() => { + let formDataInfo = new FormData(); + + if (headerFile) { + formDataInfo.set("header", headerFile); + } + + if (avatarFile) { + formDataInfo.set("avatar", avatarFile); + } + + formDataInfo.set("display_name", displayName); + formDataInfo.set("note", bio); + formDataInfo.set("locked", locked); + + return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form"); + }).then((json) => { + setStatus("Saved!"); + + setHeaderSrc(json.header); + setAvatarSrc(json.avatar); + + setDisplayName(json.display_name); + setBio(json.source.note); + setLocked(json.locked); + }).catch((e) => { + setError(e.message); + setStatus(""); + }); + }; + + return ( + <section className="basic"> + <h1>@{account.username}'s Profile Info</h1> + <form> + <div className="labelinput"> + <label htmlFor="header">Header</label> + <div className="border"> + <img className="headerpreview" src={headerSrc} alt={headerSrc ? `header image for ${account.username}` : "None set"}/> + <div> + <label htmlFor="header" className="file-input button">Browse…</label> + <span>{headerFile ? headerFile.name : ""}</span> + </div> + </div> + <input className="hidden" id="header" type="file" accept="image/*" onChange={headerOnChange}/> + </div> + <div className="labelinput"> + <label htmlFor="avatar">Avatar</label> + <div className="border"> + <img className="avatarpreview" src={avatarSrc} alt={headerSrc ? `avatar image for ${account.username}` : "None set"}/> + <div> + <label htmlFor="avatar" className="file-input button">Browse…</label> + <span>{avatarFile ? avatarFile.name : ""}</span> + </div> + </div> + <input className="hidden" id="avatar" type="file" accept="image/*" onChange={avatarOnChange}/> + </div> + <div className="labelinput"> + <label htmlFor="displayname">Display Name</label> + <input id="displayname" type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="A GoToSocial user"/> + </div> + <div className="labelinput"> + <label htmlFor="bio">Bio</label> + <textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/> + </div> + <div className="labelcheckbox"> + <label htmlFor="locked">Manually approve follow requests</label> + <input id="locked" type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)}/> + </div> + <Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/> + </form> + </section> + ); }; \ No newline at end of file diff --git a/web/source/yarn.lock b/web/source/yarn.lock index 41a2e6398..d213230b8 100644 --- a/web/source/yarn.lock +++ b/web/source/yarn.lock @@ -920,6 +920,13 @@ "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-pure-annotations" "^7.18.6" +"@babel/runtime@^7.12.5": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" @@ -5078,6 +5085,13 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"