Path: blob/master/src/packages/frontend/app/page.tsx
1496 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6This defines the entire **desktop** Cocalc page layout and brings in7everything on *desktop*, once the user has signed in.8*/910declare var DEBUG: boolean;1112import { Spin } from "antd";13import { useIntl } from "react-intl";1415import { Avatar } from "@cocalc/frontend/account/avatar/avatar";16import { alert_message } from "@cocalc/frontend/alerts";17import { Button } from "@cocalc/frontend/antd-bootstrap";18import {19CSS,20React,21useActions,22useEffect,23useState,24useTypedRedux,25} from "@cocalc/frontend/app-framework";26import { ClientContext } from "@cocalc/frontend/client/context";27import { Icon, IconName } from "@cocalc/frontend/components/icon";28import Next from "@cocalc/frontend/components/next";29import { FileUsePage } from "@cocalc/frontend/file-use/page";30import { labels } from "@cocalc/frontend/i18n";31import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";32import BalanceButton from "@cocalc/frontend/purchases/balance-button";33import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";34import openSupportTab from "@cocalc/frontend/support/open";35import { webapp_client } from "@cocalc/frontend/webapp-client";36import { COLORS } from "@cocalc/util/theme";37import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";38import { ActiveContent } from "./active-content";39import { ConnectionIndicator } from "./connection-indicator";40import { ConnectionInfo } from "./connection-info";41import { useAppContext } from "./context";42import { FullscreenButton } from "./fullscreen-button";43import { I18NBanner, useShowI18NBanner } from "./i18n-banner";44import InsecureTestModeBanner from "./insecure-test-mode-banner";45import { AppLogo } from "./logo";46import { NavTab } from "./nav-tab";47import { Notification } from "./notifications";48import PopconfirmModal from "./popconfirm-modal";49import SettingsModal from "./settings-modal";50import { HIDE_LABEL_THRESHOLD, NAV_CLASS } from "./top-nav-consts";51import { VerifyEmail } from "./verify-email-banner";52import VersionWarning from "./version-warning";53import { CookieWarning, LocalStorageWarning } from "./warnings";5455// ipad and ios have a weird trick where they make the screen56// actually smaller than 100vh and have it be scrollable, even57// when overflow:hidden, which causes massive UI pain to cocalc.58// so in that case we make the page_height less. Without this59// one little tricky, cocalc is very, very frustrating to use60// on mobile safari. See the million discussions over the years:61// https://liuhao.im/english/2015/05/29/ios-safari-window-height.html62// ...63// https://lukechannings.com/blog/2021-06-09-does-safari-15-fix-the-vh-bug/64const PAGE_HEIGHT: string =65IS_MOBILE || IS_SAFARI66? `calc(100vh - env(safe-area-inset-bottom) - ${IS_IOS ? 80 : 20}px)`67: "100vh";6869const PAGE_STYLE: CSS = {70display: "flex",71flexDirection: "column",72height: PAGE_HEIGHT, // see note73width: "100vw",74overflow: "hidden",75background: "white",76} as const;7778export const Page: React.FC = () => {79const page_actions = useActions("page");8081const { pageStyle } = useAppContext();82const { isNarrow, fileUseStyle, topBarStyle, projectsNavStyle } = pageStyle;8384const intl = useIntl();8586const open_projects = useTypedRedux("projects", "open_projects");87const [show_label, set_show_label] = useState<boolean>(true);88useEffect(() => {89const next = open_projects.size <= HIDE_LABEL_THRESHOLD;90if (next != show_label) {91set_show_label(next);92}93}, [open_projects]);9495useEffect(() => {96return () => {97page_actions.clear_all_handlers();98};99}, []);100101const [showSignInTab, setShowSignInTab] = useState<boolean>(false);102useEffect(() => {103setTimeout(() => setShowSignInTab(true), 3000);104}, []);105106const active_top_tab = useTypedRedux("page", "active_top_tab");107const show_mentions = active_top_tab === "notifications";108const show_connection = useTypedRedux("page", "show_connection");109const show_file_use = useTypedRedux("page", "show_file_use");110const fullscreen = useTypedRedux("page", "fullscreen");111const local_storage_warning = useTypedRedux("page", "local_storage_warning");112const cookie_warning = useTypedRedux("page", "cookie_warning");113114const accountIsReady = useTypedRedux("account", "is_ready");115const account_id = useTypedRedux("account", "account_id");116const is_logged_in = useTypedRedux("account", "is_logged_in");117const is_anonymous = useTypedRedux("account", "is_anonymous");118const when_account_created = useTypedRedux("account", "created");119const groups = useTypedRedux("account", "groups");120const show_i18n = useShowI18NBanner();121122const is_commercial = useTypedRedux("customize", "is_commercial");123const insecure_test_mode = useTypedRedux("customize", "insecure_test_mode");124125function account_tab_icon(): IconName | React.JSX.Element {126if (is_anonymous) {127return <></>;128} else if (account_id) {129return (130<Avatar131size={20}132account_id={account_id}133no_tooltip={true}134no_loading={true}135/>136);137} else {138return "cog";139}140}141142function render_account_tab(): React.JSX.Element {143if (!accountIsReady) {144return (145<div>146<Spin delay={1000} />147</div>148);149}150const icon = account_tab_icon();151let label, style;152if (is_anonymous) {153let mesg;154style = { fontWeight: "bold", opacity: 0 };155if (156when_account_created &&157Date.now() - when_account_created.valueOf() >= 1000 * 60 * 60158) {159mesg = "Sign Up NOW to avoid losing all of your work!";160style.width = "400px";161} else {162mesg = "Sign Up!";163}164label = (165<Button id="anonymous-sign-up" bsStyle="success" style={style}>166{mesg}167</Button>168);169style = { marginTop: "-1px" }; // compensate for using a button170/* We only actually show the button if it is still there a few171seconds later. This avoids flickering it for a moment during172normal sign in. This feels like a hack, but was super173quick to implement.174*/175setTimeout(() => $("#anonymous-sign-up").css("opacity", 1), 3000);176} else {177label = undefined;178style = undefined;179}180181return (182<NavTab183name="account"184label={label}185style={style}186label_class={NAV_CLASS}187icon={icon}188active_top_tab={active_top_tab}189hide_label={!show_label}190tooltip={intl.formatMessage(labels.account)}191/>192);193}194195function render_balance() {196if (!is_commercial) return;197return <BalanceButton minimal topBar />;198}199200function render_admin_tab(): React.JSX.Element | undefined {201if (is_logged_in && groups?.includes("admin")) {202return (203<NavTab204name="admin"205label_class={NAV_CLASS}206icon={"users"}207active_top_tab={active_top_tab}208hide_label={!show_label}209/>210);211}212}213214function render_sign_in_tab(): React.JSX.Element | null {215if (is_logged_in || !showSignInTab) return null;216217return (218<Next219sameTab220href="/auth/sign-in"221style={{222backgroundColor: COLORS.TOP_BAR.SIGN_IN_BG,223fontSize: "16pt",224color: "black",225padding: "5px 15px",226}}227>228<Icon name="sign-in" />{" "}229{intl.formatMessage({230id: "page.sign_in.label",231defaultMessage: "Sign in",232})}233</Next>234);235}236237function render_support(): React.JSX.Element | undefined {238if (!is_commercial) {239return;240}241// Note: that styled span around the label is just242// because I'm too lazy to fix this properly, since243// it's all ancient react bootstrap stuff that will244// get rewritten.245return (246<NavTab247name={undefined} // does not open a tab, just a popup248active_top_tab={active_top_tab} // it's never supposed to be active!249label={intl.formatMessage({250id: "page.help.label",251defaultMessage: "Help",252})}253label_class={NAV_CLASS}254icon={"medkit"}255on_click={openSupportTab}256hide_label={!show_label}257/>258);259}260261function render_bell(): React.JSX.Element | undefined {262if (!is_logged_in || is_anonymous) return;263return (264<Notification type="bell" active={show_file_use} pageStyle={pageStyle} />265);266}267268function render_notification(): React.JSX.Element | undefined {269if (!is_logged_in || is_anonymous) return;270return (271<Notification272type="notifications"273active={show_mentions}274pageStyle={pageStyle}275/>276);277}278279function render_fullscreen(): React.JSX.Element | undefined {280if (isNarrow || is_anonymous) return;281282return <FullscreenButton pageStyle={pageStyle} />;283}284285function render_right_nav(): React.JSX.Element {286return (287<div288className="smc-right-tabs-fixed"289style={{290display: "flex",291flex: "0 0 auto",292height: `${pageStyle.height}px`,293margin: "0",294overflowY: "hidden",295alignItems: "center",296}}297>298{render_admin_tab()}299{render_sign_in_tab()}300{render_support()}301{is_logged_in ? render_account_tab() : undefined}302{render_balance()}303{render_notification()}304{render_bell()}305{!is_anonymous && (306<ConnectionIndicator307height={pageStyle.height}308pageStyle={pageStyle}309/>310)}311{render_fullscreen()}312</div>313);314}315316function render_project_nav_button(): React.JSX.Element {317return (318<NavTab319style={{320height: `${pageStyle.height}px`,321margin: "0",322overflow: "hidden",323}}324name={"projects"}325active_top_tab={active_top_tab}326tooltip={intl.formatMessage({327id: "page.project_nav.tooltip",328defaultMessage: "Show all the projects on which you collaborate.",329})}330icon="edit"331label={intl.formatMessage(labels.projects)}332/>333);334}335336// register a default drag and drop handler, that prevents337// accidental file drops338// TEST: make sure that usual drag'n'drop activities339// like rearranging tabs and reordering tasks work340function drop(e) {341if (DEBUG) {342e.persist();343}344//console.log "react desktop_app.drop", e345e.preventDefault();346e.stopPropagation();347if (e.dataTransfer.files.length > 0) {348alert_message({349type: "info",350title: "File Drop Rejected",351message:352'To upload a file, drop it onto a file you are editing, the file explorer listing or the "Drop files to upload" area in the +New page.',353});354}355}356357// Children must define their own padding from navbar and screen borders358// Note that the parent is a flex container359const body = (360<div361style={PAGE_STYLE}362onDragOver={(e) => e.preventDefault()}363onDrop={drop}364>365{insecure_test_mode && <InsecureTestModeBanner />}366{show_file_use && (367<div style={fileUseStyle} className="smc-vfill">368<FileUsePage />369</div>370)}371{show_connection && <ConnectionInfo />}372<VersionWarning />373{cookie_warning && <CookieWarning />}374{local_storage_warning && <LocalStorageWarning />}375{show_i18n && <I18NBanner />}376<VerifyEmail />377{!fullscreen && (378<nav className="smc-top-bar" style={topBarStyle}>379<AppLogo size={pageStyle.height} />380{is_logged_in && render_project_nav_button()}381{!isNarrow ? (382<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />383) : (384// we need an expandable placeholder, otherwise the right-nav-buttons won't align to the right385<div style={{ flex: "1 1 auto" }} />386)}387{render_right_nav()}388</nav>389)}390{fullscreen && render_fullscreen()}391{isNarrow && (392<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />393)}394<ActiveContent />395<PayAsYouGoModal />396<PopconfirmModal />397<SettingsModal />398</div>399);400return (401<ClientContext.Provider value={{ client: webapp_client }}>402{body}403</ClientContext.Provider>404);405};406407408