components.jsx (4760B)
1 /* 2 GoToSocial 3 Copyright (C) GoToSocial Authors admin@gotosocial.org 4 SPDX-License-Identifier: AGPL-3.0-or-later 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 "use strict"; 21 22 const React = require("react"); 23 const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter"); 24 const syncpipe = require("syncpipe"); 25 26 const { 27 RoleContext, 28 useHasPermission, 29 checkPermission, 30 BaseUrlContext 31 } = require("./util"); 32 33 const ActiveRouteCtx = React.createContext(); 34 function useActiveRoute() { 35 return React.useContext(ActiveRouteCtx); 36 } 37 38 function Sidebar(menuTree, routing) { 39 const components = menuTree.map((m) => m.MenuEntry); 40 41 return function SidebarComponent() { 42 const router = useRouter(); 43 const [location] = useLocation(); 44 45 let activeRoute = routing.find((l) => { 46 let [match] = router.matcher(l.routingUrl, location); 47 return match; 48 })?.routingUrl; 49 50 return ( 51 <nav className="menu-tree"> 52 <ul className="top-level"> 53 <ActiveRouteCtx.Provider value={activeRoute}> 54 {components} 55 </ActiveRouteCtx.Provider> 56 </ul> 57 </nav> 58 ); 59 }; 60 } 61 62 function ViewRouter(routing, defaultRoute) { 63 return function ViewRouterComponent() { 64 const permissions = React.useContext(RoleContext); 65 66 const filteredRoutes = React.useMemo(() => { 67 return syncpipe(routing, [ 68 (_) => _.filter((route) => checkPermission(route.permissions, permissions)), 69 (_) => _.map((route) => { 70 return ( 71 <Route path={route.routingUrl} key={route.key}> 72 <ErrorBoundary> 73 {/* FIXME: implement reset */} 74 <BaseUrlContext.Provider value={route.url}> 75 {route.view} 76 </BaseUrlContext.Provider> 77 </ErrorBoundary> 78 </Route> 79 ); 80 }) 81 ]); 82 }, [permissions]); 83 84 return ( 85 <Switch> 86 {filteredRoutes} 87 <Redirect to={defaultRoute} /> 88 </Switch> 89 ); 90 }; 91 } 92 93 function MenuComponent({ type, name, url, icon, permissions, links, level, children }) { 94 const activeRoute = useActiveRoute(); 95 96 if (!useHasPermission(permissions)) { 97 return null; 98 } 99 100 const classes = [type]; 101 102 if (level == 0) { 103 classes.push("top-level"); 104 } else if (level == 1) { 105 classes.push("expanding"); 106 } else { 107 classes.push("nested"); 108 } 109 110 const isActive = links.includes(activeRoute); 111 if (isActive) { 112 classes.push("active"); 113 } 114 115 const className = classes.join(" "); 116 117 return ( 118 <li className={className}> 119 <Link href={url}> 120 <a tabIndex={level == 0 ? "-1" : null} className="title"> 121 {icon && <i className={`icon fa fa-fw ${icon}`} aria-hidden="true" />} 122 {name} 123 </a> 124 </Link> 125 {(type == "category" && (level == 0 || isActive) && children?.length > 0) && 126 <ul> 127 {children} 128 </ul> 129 } 130 </li> 131 ); 132 } 133 134 class ErrorBoundary extends React.Component { 135 136 constructor() { 137 super(); 138 this.state = {}; 139 140 this.resetErrorBoundary = () => { 141 this.setState({}); 142 }; 143 } 144 145 static getDerivedStateFromError(error) { 146 return { hadError: true, error }; 147 } 148 149 componentDidCatch(_e, info) { 150 this.setState({ 151 ...this.state, 152 componentStack: info.componentStack 153 }); 154 } 155 156 render() { 157 if (this.state.hadError) { 158 return ( 159 <ErrorFallback 160 error={this.state.error} 161 componentStack={this.state.componentStack} 162 resetErrorBoundary={this.resetErrorBoundary} 163 /> 164 ); 165 } else { 166 return this.props.children; 167 } 168 } 169 } 170 171 function ErrorFallback({ error, componentStack, resetErrorBoundary }) { 172 return ( 173 <div className="error"> 174 <p> 175 {"An error occured, please report this on the "} 176 <a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a> 177 {" or "} 178 <a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>. 179 <br />Include the details below: 180 </p> 181 <div className="details"> 182 <pre> 183 {error.name}: {error.message} 184 185 {componentStack && [ 186 "\n\nComponent trace:", 187 componentStack 188 ]} 189 {["\n\nError trace: ", error.stack]} 190 </pre> 191 </div> 192 <p> 193 <button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a> 194 </p> 195 </div> 196 ); 197 } 198 199 module.exports = { 200 Sidebar, 201 ViewRouter, 202 MenuComponent 203 };