diff --git a/app/components/Scrollable.js b/app/components/Scrollable.js index 179bee86..762301a6 100644 --- a/app/components/Scrollable.js +++ b/app/components/Scrollable.js @@ -1,28 +1,52 @@ // @flow -import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import styled from "styled-components"; +import useWindowSize from "hooks/useWindowSize"; -type Props = { +type Props = {| shadow?: boolean, -}; + topShadow?: boolean, + bottomShadow?: boolean, +|}; -@observer -class Scrollable extends React.Component { - @observable shadow: boolean = false; +function Scrollable({ shadow, topShadow, bottomShadow, ...rest }: Props) { + const ref = React.useRef(); + const [topShadowVisible, setTopShadow] = React.useState(false); + const [bottomShadowVisible, setBottomShadow] = React.useState(false); + const { height } = useWindowSize(); - handleScroll = (ev: SyntheticMouseEvent) => { - this.shadow = !!(this.props.shadow && ev.currentTarget.scrollTop > 0); - }; + const updateShadows = React.useCallback(() => { + const c = ref.current; + if (!c) return; - render() { - const { shadow, ...rest } = this.props; + const scrollTop = c.scrollTop; + const tsv = !!((shadow || topShadow) && scrollTop > 0); + if (tsv !== topShadowVisible) { + setTopShadow(tsv); + } - return ( - - ); - } + const wrapperHeight = c.scrollHeight - c.clientHeight; + const bsv = !!((shadow || bottomShadow) && wrapperHeight - scrollTop !== 0); + + if (bsv !== bottomShadowVisible) { + setBottomShadow(bsv); + } + }, [shadow, topShadow, bottomShadow, topShadowVisible, bottomShadowVisible]); + + React.useEffect(() => { + updateShadows(); + }, [height, updateShadows]); + + return ( + + ); } const Wrapper = styled.div` @@ -31,9 +55,20 @@ const Wrapper = styled.div` overflow-x: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch; - box-shadow: ${(props) => - props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"}; - transition: all 250ms ease-in-out; + box-shadow: ${(props) => { + if (props.$topShadowVisible && props.$bottomShadowVisible) { + return "0 1px inset rgba(0,0,0,.1), 0 -1px inset rgba(0,0,0,.1)"; + } + if (props.$topShadowVisible) { + return "0 1px inset rgba(0,0,0,.1)"; + } + if (props.$bottomShadowVisible) { + return "0 -1px inset rgba(0,0,0,.1)"; + } + + return "none"; + }}; + transition: all 100ms ease-in-out; `; -export default Scrollable; +export default observer(Scrollable); diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js index 44b3794c..61d06cdd 100644 --- a/app/components/Sidebar/Main.js +++ b/app/components/Sidebar/Main.js @@ -1,6 +1,5 @@ // @flow -import { observable } from "mobx"; -import { observer, inject } from "mobx-react"; +import { observer } from "mobx-react"; import { ArchiveIcon, HomeIcon, @@ -10,14 +9,11 @@ import { ShapesIcon, TrashIcon, PlusIcon, + SettingsIcon, } from "outline-icons"; import * as React from "react"; -import { withTranslation, type TFunction } from "react-i18next"; +import { useTranslation } from "react-i18next"; import styled from "styled-components"; - -import AuthStore from "stores/AuthStore"; -import DocumentsStore from "stores/DocumentsStore"; -import PoliciesStore from "stores/PoliciesStore"; import CollectionNew from "scenes/CollectionNew"; import Invite from "scenes/Invite"; import Flex from "components/Flex"; @@ -29,176 +25,184 @@ import Collections from "./components/Collections"; import HeaderBlock from "./components/HeaderBlock"; import Section from "./components/Section"; import SidebarLink from "./components/SidebarLink"; +import useStores from "hooks/useStores"; import AccountMenu from "menus/AccountMenu"; -type Props = { - auth: AuthStore, - documents: DocumentsStore, - policies: PoliciesStore, - t: TFunction, -}; +function MainSidebar() { + const { t } = useTranslation(); + const { policies, auth, documents } = useStores(); + const [inviteModalOpen, setInviteModalOpen] = React.useState(false); + const [ + createCollectionModalOpen, + setCreateCollectionModalOpen, + ] = React.useState(false); -@observer -class MainSidebar extends React.Component { - @observable inviteModalOpen = false; - @observable createCollectionModalOpen = false; + React.useEffect(() => { + documents.fetchDrafts(); + documents.fetchTemplates(); + }, [documents]); - componentDidMount() { - this.props.documents.fetchDrafts(); - this.props.documents.fetchTemplates(); - } + const handleCreateCollectionModalOpen = React.useCallback( + (ev: SyntheticEvent<>) => { + ev.preventDefault(); + setCreateCollectionModalOpen(true); + }, + [] + ); - handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => { + const handleCreateCollectionModalClose = React.useCallback( + (ev: SyntheticEvent<>) => { + ev.preventDefault(); + setCreateCollectionModalOpen(false); + }, + [] + ); + + const handleInviteModalOpen = React.useCallback((ev: SyntheticEvent<>) => { ev.preventDefault(); - this.createCollectionModalOpen = true; - }; + setInviteModalOpen(true); + }, []); - handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => { - this.createCollectionModalOpen = false; - }; - - handleInviteModalOpen = (ev: SyntheticEvent<>) => { + const handleInviteModalClose = React.useCallback((ev: SyntheticEvent<>) => { ev.preventDefault(); - this.inviteModalOpen = true; - }; + setInviteModalOpen(false); + }, []); - handleInviteModalClose = () => { - this.inviteModalOpen = false; - }; + const { user, team } = auth; + if (!user || !team) return null; - render() { - const { auth, documents, policies, t } = this.props; - const { user, team } = auth; - if (!user || !team) return null; + const can = policies.abilities(team.id); - const can = policies.abilities(team.id); - - return ( - - - {(props) => ( - + + {(props) => ( + + )} + + + +
+ } + exact={false} + label={t("Home")} /> - )} - - - -
+ } + label={t("Search")} + exact={false} + /> + } + exact={false} + label={t("Starred")} + /> + } + exact={false} + label={t("Templates")} + active={documents.active ? documents.active.template : undefined} + /> + } + label={ + + {t("Drafts")} + {documents.totalDrafts > 0 && ( + + )} + + } + active={ + documents.active + ? !documents.active.publishedAt && + !documents.active.isDeleted && + !documents.active.isTemplate + : undefined + } + /> +
+
+ +
+
+ +
+ } + exact={false} + label={t("Archive")} + active={ + documents.active + ? documents.active.isArchived && !documents.active.isDeleted + : undefined + } + /> + } + exact={false} + label={t("Trash")} + active={documents.active ? documents.active.isDeleted : undefined} + /> + } + exact={false} + label={t("Settings")} + /> + {can.invite && ( } - exact={false} - label={t("Home")} + to="/settings/people" + onClick={handleInviteModalOpen} + icon={} + label={t("Invite people…")} /> - } - label={t("Search")} - exact={false} - /> - } - exact={false} - label={t("Starred")} - /> - } - exact={false} - label={t("Templates")} - active={ - documents.active ? documents.active.template : undefined - } - /> - } - label={ - - {t("Drafts")} - {documents.totalDrafts > 0 && ( - - )} - - } - active={ - documents.active - ? !documents.active.publishedAt && - !documents.active.isDeleted && - !documents.active.isTemplate - : undefined - } - /> -
-
- -
-
- } - exact={false} - label={t("Archive")} - active={ - documents.active - ? documents.active.isArchived && !documents.active.isDeleted - : undefined - } - /> - } - exact={false} - label={t("Trash")} - active={ - documents.active ? documents.active.isDeleted : undefined - } - /> - {can.invite && ( - } - label={t("Invite people…")} - /> - )} -
- -
- - - - - - - - ); - } + )} +
+ +
+ + + + + + +
+ ); } +const Secondary = styled.div` + overflow-x: hidden; + flex-shrink: 0; +`; + const Drafts = styled(Flex)` height: 24px; `; -export default withTranslation()( - inject("documents", "policies", "auth")(MainSidebar) -); +export default observer(MainSidebar); diff --git a/app/components/Sidebar/Settings.js b/app/components/Sidebar/Settings.js index 0d0e7478..94f732e4 100644 --- a/app/components/Sidebar/Settings.js +++ b/app/components/Sidebar/Settings.js @@ -58,7 +58,7 @@ function SettingsSidebar() { /> - +
{t("Account")}
props.theme.sidebarMinWidth}px; + flex-shrink: 0; `; export default Section; diff --git a/app/hooks/useWindowSize.js b/app/hooks/useWindowSize.js new file mode 100644 index 00000000..8c2111b2 --- /dev/null +++ b/app/hooks/useWindowSize.js @@ -0,0 +1,31 @@ +// @flow +import { debounce } from "lodash"; +import * as React from "react"; + +export default function useWindowSize() { + const [windowSize, setWindowSize] = React.useState({ + width: undefined, + height: undefined, + }); + + React.useEffect(() => { + // Handler to call on window resize + const handleResize = debounce(() => { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }, 100); + + // Add event listener + window.addEventListener("resize", handleResize); + + // Call handler right away so state gets updated with initial window size + handleResize(); + + return () => window.removeEventListener("resize", handleResize); + }, []); + + return windowSize; +} diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index b0c41d23..3d4e8cbf 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -94,9 +94,11 @@ "Invite people": "Invite people", "Create a collection": "Create a collection", "Return to App": "Return to App", + "Account": "Account", "Profile": "Profile", "Notifications": "Notifications", "API Tokens": "API Tokens", + "Team": "Team", "Details": "Details", "Security": "Security", "People": "People", @@ -109,7 +111,6 @@ "System": "System", "Light": "Light", "Dark": "Dark", - "Account": "Account", "Settings": "Settings", "API documentation": "API documentation", "Changelog": "Changelog",