diff --git a/app/components/Authenticated.tsx b/app/components/Authenticated.tsx index 924e5da8d..02eec34ea 100644 --- a/app/components/Authenticated.tsx +++ b/app/components/Authenticated.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Redirect } from "react-router-dom"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import { changeLanguage } from "~/utils/language"; import LoadingIndicator from "./LoadingIndicator"; @@ -13,10 +14,11 @@ type Props = { const Authenticated = ({ children }: Props) => { const { auth } = useStores(); const { i18n } = useTranslation(); - const language = auth.user?.language; + const user = useCurrentUser({ rejectOnEmpty: false }); + const language = user?.language; - // Watching for language changes here as this is the earliest point we have - // the user available and means we can start loading translations faster + // Watching for language changes here as this is the earliest point we might have the user + // available and means we can start loading translations faster React.useEffect(() => { void changeLanguage(language, i18n); }, [i18n, language]); diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx index cad6c54ac..107b645f1 100644 --- a/app/components/AuthenticatedLayout.tsx +++ b/app/components/AuthenticatedLayout.tsx @@ -12,6 +12,7 @@ import Sidebar from "~/components/Sidebar"; import SidebarRight from "~/components/Sidebar/Right"; import SettingsSidebar from "~/components/Sidebar/Settings"; import type { Editor as TEditor } from "~/editor"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import history from "~/utils/history"; @@ -45,7 +46,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { const { ui, auth } = useStores(); const location = useLocation(); const can = usePolicy(ui.activeCollectionId); - const { user, team } = auth; + const team = useCurrentTeam(); const documentContext = useLocalStore(() => ({ editor: null, setEditor: (editor: TEditor) => { @@ -76,16 +77,14 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { return ; } - const showSidebar = auth.authenticated && user && team; - - const sidebar = showSidebar ? ( + const sidebar = ( - ) : undefined; + ); const showHistory = !!matchPath(location.pathname, { path: matchDocumentHistory, @@ -98,7 +97,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { !showHistory && ui.activeDocumentId && ui.commentsExpanded.includes(ui.activeDocumentId) && - team?.getPreference(TeamPreference.Commenting); + team.getPreference(TeamPreference.Commenting); const sidebarRight = ( { return ( - + diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index 2304dda10..4c0dafa58 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -21,6 +21,7 @@ import ClickablePadding from "~/components/ClickablePadding"; import ErrorBoundary from "~/components/ErrorBoundary"; import HoverPreview from "~/components/HoverPreview"; import type { Props as EditorProps, Editor as SharedEditor } from "~/editor"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useDictionary from "~/hooks/useDictionary"; import useEmbeds from "~/hooks/useEmbeds"; import useStores from "~/hooks/useStores"; @@ -65,12 +66,12 @@ function Editor(props: Props, ref: React.RefObject | null) { } = props; const userLocale = useUserLocale(); const locale = dateLocale(userLocale); - const { auth, comments, documents } = useStores(); + const { comments, documents } = useStores(); const dictionary = useDictionary(); const embeds = useEmbeds(!shareId); const history = useHistory(); const localRef = React.useRef(); - const preferences = auth.user?.preferences; + const preferences = useCurrentUser({ rejectOnEmpty: false })?.preferences; const previousHeadings = React.useRef(null); const [activeLinkElement, setActiveLink] = React.useState(null); diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index 7efaf46ec..faa63a4af 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -5,6 +5,7 @@ import styled from "styled-components"; import { NavigationNode } from "@shared/types"; import Scrollable from "~/components/Scrollable"; import SearchPopover from "~/components/SearchPopover"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import history from "~/utils/history"; import { homePath, sharedDocumentPath } from "~/utils/routeHelpers"; @@ -22,7 +23,8 @@ type Props = { function SharedSidebar({ rootNode, shareId }: Props) { const team = useTeamContext(); - const { ui, documents, auth } = useStores(); + const user = useCurrentUser({ rejectOnEmpty: false }); + const { ui, documents } = useStores(); const { t } = useTranslation(); return ( @@ -33,7 +35,7 @@ function SharedSidebar({ rootNode, shareId }: Props) { image={} onClick={() => history.push( - auth.user ? homePath() : sharedDocumentPath(shareId, rootNode.url) + user ? homePath() : sharedDocumentPath(shareId, rootNode.url) ) } /> diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 60227e694..78db3a391 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -6,6 +6,7 @@ import styled, { css, useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { depths, s } from "@shared/styles"; import Flex from "~/components/Flex"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useMenuContext from "~/hooks/useMenuContext"; import usePrevious from "~/hooks/usePrevious"; import useStores from "~/hooks/useStores"; @@ -33,11 +34,11 @@ const Sidebar = React.forwardRef(function _Sidebar( ) { const [isCollapsing, setCollapsing] = React.useState(false); const theme = useTheme(); - const { ui, auth } = useStores(); + const { ui } = useStores(); const location = useLocation(); const previousLocation = usePrevious(location); const { isMenuOpen } = useMenuContext(); - const { user } = auth; + const user = useCurrentUser({ rejectOnEmpty: false }); const width = ui.sidebarWidth; const collapsed = ui.sidebarIsClosed && !isMenuOpen; const maxWidth = theme.sidebarMaxWidth; diff --git a/app/editor/components/MentionMenu.tsx b/app/editor/components/MentionMenu.tsx index ed9ed384c..af0edc10f 100644 --- a/app/editor/components/MentionMenu.tsx +++ b/app/editor/components/MentionMenu.tsx @@ -10,6 +10,7 @@ import User from "~/models/User"; import Avatar from "~/components/Avatar"; import { AvatarSize } from "~/components/Avatar/Avatar"; import Flex from "~/components/Flex"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; import MentionMenuItem from "./MentionMenuItem"; @@ -39,8 +40,9 @@ function MentionMenu({ search, isActive, ...rest }: Props) { const [loaded, setLoaded] = React.useState(false); const [items, setItems] = React.useState([]); const { t } = useTranslation(); - const { users, auth } = useStores(); + const { users } = useStores(); const location = useLocation(); + const user = useCurrentUser({ rejectOnEmpty: false }); const documentId = parseDocumentSlug(location.pathname); const { data, loading, request } = useRequest( React.useCallback( @@ -69,7 +71,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) { id: v4(), type: MentionType.User, modelId: user.id, - actorId: auth.user?.id, + actorId: user?.id, label: user.name, }, })); @@ -77,7 +79,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) { setItems(items); setLoaded(true); } - }, [auth.user?.id, loading, data]); + }, [user?.id, loading, data]); // Prevent showing the menu until we have data otherwise it will be positioned // incorrectly due to the height being unknown. diff --git a/app/hooks/useCurrentTeam.ts b/app/hooks/useCurrentTeam.ts index db5c386e7..d64579ada 100644 --- a/app/hooks/useCurrentTeam.ts +++ b/app/hooks/useCurrentTeam.ts @@ -1,8 +1,23 @@ import invariant from "invariant"; +import Team from "~/models/Team"; import useStores from "./useStores"; -export default function useCurrentTeam() { +/** + * Returns the current team, or undefined if there is no current team and `rejectOnEmpty` is set to + * false. + * + * @param options.rejectOnEmpty - If true, throws an error if there is no current team. Defaults to true. + */ +function useCurrentTeam(options: { rejectOnEmpty: false }): Team | undefined; +function useCurrentTeam(options?: { rejectOnEmpty: true }): Team; +function useCurrentTeam({ + rejectOnEmpty = true, +}: { rejectOnEmpty?: boolean } = {}) { const { auth } = useStores(); - invariant(auth.team, "team required"); - return auth.team; + if (rejectOnEmpty) { + invariant(auth.team, "team required"); + } + return auth.team || undefined; } + +export default useCurrentTeam; diff --git a/app/hooks/useCurrentUser.ts b/app/hooks/useCurrentUser.ts index 53fb54c8a..267b39dce 100644 --- a/app/hooks/useCurrentUser.ts +++ b/app/hooks/useCurrentUser.ts @@ -1,8 +1,23 @@ import invariant from "invariant"; +import User from "~/models/User"; import useStores from "./useStores"; -export default function useCurrentUser() { +/** + * Returns the current user, or undefined if there is no current user and `rejectOnEmpty` is set to + * false. + * + * @param options.rejectOnEmpty - If true, throws an error if there is no current user. Defaults to true. + */ +function useCurrentUser(options: { rejectOnEmpty: false }): User | undefined; +function useCurrentUser(options?: { rejectOnEmpty: true }): User; +function useCurrentUser({ + rejectOnEmpty = true, +}: { rejectOnEmpty?: boolean } = {}) { const { auth } = useStores(); - invariant(auth.user, "user required"); - return auth.user; + if (rejectOnEmpty) { + invariant(auth.user, "user required"); + } + return auth.user || undefined; } + +export default useCurrentUser; diff --git a/app/hooks/useUserLocale.ts b/app/hooks/useUserLocale.ts index f8628d9fd..7a981589b 100644 --- a/app/hooks/useUserLocale.ts +++ b/app/hooks/useUserLocale.ts @@ -1,4 +1,4 @@ -import useStores from "./useStores"; +import useCurrentUser from "./useCurrentUser"; /** * Returns the user's locale, or undefined if the user is not logged in. @@ -7,12 +7,12 @@ import useStores from "./useStores"; * @returns The user's locale, or undefined if the user is not logged in */ export default function useUserLocale(languageCode?: boolean) { - const { auth } = useStores(); + const user = useCurrentUser({ rejectOnEmpty: false }); - if (!auth.user?.language) { + if (!user?.language) { return undefined; } - const { language } = auth.user; + const { language } = user; return languageCode ? language.split("_")[0] : language; } diff --git a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx index b3dd13706..8a89957de 100644 --- a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx +++ b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx @@ -17,6 +17,7 @@ import Modal from "~/components/Modal"; import PaginatedList from "~/components/PaginatedList"; import Text from "~/components/Text"; import useBoolean from "~/hooks/useBoolean"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; type Props = { @@ -29,11 +30,11 @@ function AddGroupsToCollection(props: Props) { const [newGroupModalOpen, handleNewGroupModalOpen, handleNewGroupModalClose] = useBoolean(false); const [query, setQuery] = React.useState(""); - - const { auth, collectionGroupMemberships, groups, policies } = useStores(); - const { fetchPage: fetchGroups } = groups; - + const team = useCurrentTeam(); + const { collectionGroupMemberships, groups, policies } = useStores(); const { t } = useTranslation(); + const { fetchPage: fetchGroups } = groups; + const can = policies.abilities(team.id); const debouncedFetch = React.useMemo( () => debounce((query) => fetchGroups({ query }), 250), @@ -65,13 +66,6 @@ function AddGroupsToCollection(props: Props) { } }; - const { user, team } = auth; - if (!user || !team) { - return null; - } - - const can = policies.abilities(team.id); - return ( {can.createGroup ? ( diff --git a/app/scenes/Document/Shared.tsx b/app/scenes/Document/Shared.tsx index dd4d2aa3e..756b9eab7 100644 --- a/app/scenes/Document/Shared.tsx +++ b/app/scenes/Document/Shared.tsx @@ -18,6 +18,7 @@ import { TeamContext } from "~/components/TeamContext"; import Text from "~/components/Text"; import env from "~/env"; import useBuildTheme from "~/hooks/useBuildTheme"; +import useCurrentUser from "~/hooks/useCurrentUser"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import { AuthorizationError, OfflineError } from "~/utils/errors"; @@ -83,8 +84,9 @@ function useDocumentId(documentSlug: string, response?: Response) { } function SharedDocumentScene(props: Props) { - const { ui, auth } = useStores(); + const { ui } = useStores(); const location = useLocation(); + const user = useCurrentUser({ rejectOnEmpty: false }); const searchParams = React.useMemo( () => new URLSearchParams(location.search), [location.search] @@ -104,10 +106,10 @@ function SharedDocumentScene(props: Props) { const theme = useBuildTheme(response?.team?.customTheme, themeOverride); React.useEffect(() => { - if (!auth.user) { + if (!user) { void changeLanguage(detectLanguage(), i18n); } - }, [auth, i18n]); + }, [user, i18n]); // ensure the wider page color always matches the theme React.useEffect(() => { diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 891c4b49f..d583df41d 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -7,6 +7,8 @@ import Document from "~/models/Document"; import Revision from "~/models/Revision"; import Error404 from "~/scenes/Error404"; import ErrorOffline from "~/scenes/ErrorOffline"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; +import useCurrentUser from "~/hooks/useCurrentUser"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import Logger from "~/utils/Logger"; @@ -16,12 +18,16 @@ import { matchDocumentEdit, settingsPath } from "~/utils/routeHelpers"; import Loading from "./Loading"; type Params = { + /** The document urlId + slugified title */ documentSlug: string; + /** A specific revision id to load. */ revisionId?: string; + /** The share ID to use to load data. */ shareId?: string; }; type LocationState = { + /** The document title, if preloaded */ title?: string; restore?: boolean; revisionId?: string; @@ -41,17 +47,10 @@ type Props = RouteComponentProps & { }; function DataLoader({ match, children }: Props) { - const { - ui, - views, - shares, - comments, - documents, - auth, - revisions, - subscriptions, - } = useStores(); - const { team } = auth; + const { ui, views, shares, comments, documents, revisions, subscriptions } = + useStores(); + const team = useCurrentTeam(); + const user = useCurrentUser(); const [error, setError] = React.useState(null); const { revisionId, shareId, documentSlug } = match.params; @@ -73,7 +72,7 @@ function DataLoader({ match, children }: Props) { : undefined; const isEditRoute = match.path === matchDocumentEdit || match.path.startsWith(settingsPath()); - const isEditing = isEditRoute || !auth.user?.separateEditMode; + const isEditing = isEditRoute || !user?.separateEditMode; const can = usePolicy(document?.id); const location = useLocation(); @@ -180,7 +179,7 @@ function DataLoader({ match, children }: Props) { // Prevents unauthorized request to load share information for the document // when viewing a public share link if (can.read) { - if (team?.getPreference(TeamPreference.Commenting)) { + if (team.getPreference(TeamPreference.Commenting)) { void comments.fetchDocumentComments(document.id, { limit: 100, }); @@ -199,7 +198,7 @@ function DataLoader({ match, children }: Props) { return error instanceof OfflineError ? : ; } - if (!document || !team || (revisionId && !revision)) { + if (!document || (revisionId && !revision)) { return ( <> diff --git a/app/scenes/Document/components/DocumentMeta.tsx b/app/scenes/Document/components/DocumentMeta.tsx index 941f2adb6..d87ea0a60 100644 --- a/app/scenes/Document/components/DocumentMeta.tsx +++ b/app/scenes/Document/components/DocumentMeta.tsx @@ -10,6 +10,7 @@ import Document from "~/models/Document"; import Revision from "~/models/Revision"; import DocumentMeta from "~/components/DocumentMeta"; import Fade from "~/components/Fade"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import { documentPath, documentInsightsPath } from "~/utils/routeHelpers"; @@ -29,10 +30,10 @@ function TitleDocumentMeta({ revision, ...rest }: Props) { - const { auth, views, comments, ui } = useStores(); + const { views, comments, ui } = useStores(); const { t } = useTranslation(); - const { team } = auth; const match = useRouteMatch(); + const team = useCurrentTeam(); const documentViews = useObserver(() => views.inDocument(document.id)); const totalViewers = documentViews.length; const onlyYou = totalViewers === 1 && documentViews[0].userId; @@ -45,7 +46,7 @@ function TitleDocumentMeta({ return ( - {team?.getPreference(TeamPreference.Commenting) && ( + {team.getPreference(TeamPreference.Commenting) && ( <>  •  ) { const { t } = useTranslation(); const match = useRouteMatch(); const focusedComment = useFocusedComment(); - const { ui, comments, auth } = useStores(); - const { user, team } = auth; + const { ui, comments } = useStores(); + const user = useCurrentUser({ rejectOnEmpty: false }); + const team = useCurrentTeam({ rejectOnEmpty: false }); const history = useHistory(); const { document, diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index 243ed512c..67019526a 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -29,6 +29,8 @@ import { publishDocument } from "~/actions/definitions/documents"; import { navigateToTemplateSettings } from "~/actions/definitions/navigation"; import { restoreRevision } from "~/actions/definitions/revisions"; import useActionContext from "~/hooks/useActionContext"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useMobile from "~/hooks/useMobile"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; @@ -84,10 +86,11 @@ function DocumentHeader({ headings, }: Props) { const { t } = useTranslation(); - const { ui, auth } = useStores(); + const { ui } = useStores(); const theme = useTheme(); + const team = useCurrentTeam({ rejectOnEmpty: false }); + const user = useCurrentUser({ rejectOnEmpty: false }); const { resolvedTheme } = ui; - const { team, user } = auth; const isMobile = useMobile(); const isRevision = !!revision; const isEditingFocus = useEditingFocus(); diff --git a/app/scenes/GroupMembers/AddPeopleToGroup.tsx b/app/scenes/GroupMembers/AddPeopleToGroup.tsx index 15d558391..f8472743b 100644 --- a/app/scenes/GroupMembers/AddPeopleToGroup.tsx +++ b/app/scenes/GroupMembers/AddPeopleToGroup.tsx @@ -16,6 +16,7 @@ import Modal from "~/components/Modal"; import PaginatedList from "~/components/PaginatedList"; import Text from "~/components/Text"; import useBoolean from "~/hooks/useBoolean"; +import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import GroupMemberListItem from "./components/GroupMemberListItem"; @@ -27,7 +28,8 @@ type Props = { function AddPeopleToGroup(props: Props) { const { group } = props; - const { users, auth, groupMemberships } = useStores(); + const { users, groupMemberships } = useStores(); + const team = useCurrentTeam(); const { t } = useTranslation(); const [query, setQuery] = React.useState(""); @@ -69,11 +71,6 @@ function AddPeopleToGroup(props: Props) { } }; - const { user, team } = auth; - if (!user || !team) { - return null; - } - return ( diff --git a/app/scenes/Login/index.tsx b/app/scenes/Login/index.tsx index de4245890..57f82919c 100644 --- a/app/scenes/Login/index.tsx +++ b/app/scenes/Login/index.tsx @@ -22,6 +22,7 @@ import PageTitle from "~/components/PageTitle"; import TeamLogo from "~/components/TeamLogo"; import Text from "~/components/Text"; import env from "~/env"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useLastVisitedPath from "~/hooks/useLastVisitedPath"; import useQuery from "~/hooks/useQuery"; import useStores from "~/hooks/useStores"; @@ -43,12 +44,13 @@ function Login({ children }: Props) { const notice = query.get("notice"); const { t } = useTranslation(); + const user = useCurrentUser({ rejectOnEmpty: false }); const { auth } = useStores(); const { config } = auth; const [error, setError] = React.useState(null); const [emailLinkSentTo, setEmailLinkSentTo] = React.useState(""); const isCreate = location.pathname === "/create"; - const rememberLastPath = !!auth.user?.getPreference( + const rememberLastPath = !!user?.getPreference( UserPreference.RememberLastPath ); const [lastVisitedPath] = useLastVisitedPath();