Path: blob/master/src/packages/frontend/app/page.tsx
5878 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 type { IconName } from "@cocalc/frontend/components/icon";1314import { Spin } from "antd";15import { useIntl } from "react-intl";1617import { Avatar } from "@cocalc/frontend/account/avatar/avatar";18import { alert_message } from "@cocalc/frontend/alerts";19import { Button } from "@cocalc/frontend/antd-bootstrap";20import {21CSS,22React,23useActions,24useEffect,25useState,26useTypedRedux,27} from "@cocalc/frontend/app-framework";28import { ClientContext } from "@cocalc/frontend/client/context";29import { Icon } from "@cocalc/frontend/components/icon";30import Next from "@cocalc/frontend/components/next";31import { FileUsePage } from "@cocalc/frontend/file-use/page";32import { labels } from "@cocalc/frontend/i18n";33import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";34import BalanceButton from "@cocalc/frontend/purchases/balance-button";35import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";36import openSupportTab from "@cocalc/frontend/support/open";37import { webapp_client } from "@cocalc/frontend/webapp-client";38import { COLORS } from "@cocalc/util/theme";39import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";40import { ActiveContent } from "./active-content";41import { ConnectionIndicator } from "./connection-indicator";42import { ConnectionInfo } from "./connection-info";43import { useAppContext } from "./context";44import { FullscreenButton } from "./fullscreen-button";45import { I18NBanner, useShowI18NBanner } from "./i18n-banner";46import InsecureTestModeBanner from "./insecure-test-mode-banner";47import { AppLogo } from "./logo";48import { NavTab } from "./nav-tab";49import { Notification } from "./notifications";50import PopconfirmModal from "./popconfirm-modal";51import SettingsModal from "./settings-modal";52import { HIDE_LABEL_THRESHOLD, NAV_CLASS } from "./top-nav-consts";53import { VerifyEmail } from "./verify-email-banner";54import VersionWarning from "./version-warning";55import { CookieWarning, LocalStorageWarning } from "./warnings";5657// ipad and ios have a weird trick where they make the screen58// actually smaller than 100vh and have it be scrollable, even59// when overflow:hidden, which causes massive UI pain to cocalc.60// so in that case we make the page_height less. Without this61// one little tricky, cocalc is very, very frustrating to use62// on mobile safari. See the million discussions over the years:63// https://liuhao.im/english/2015/05/29/ios-safari-window-height.html64// ...65// https://lukechannings.com/blog/2021-06-09-does-safari-15-fix-the-vh-bug/66const PAGE_HEIGHT: string =67IS_MOBILE || IS_SAFARI68? `calc(100vh - env(safe-area-inset-bottom) - ${IS_IOS ? 80 : 20}px)`69: "100vh";7071const PAGE_STYLE: CSS = {72display: "flex",73flexDirection: "column",74height: PAGE_HEIGHT, // see note75width: "100vw",76overflow: "hidden",77background: "white",78} as const;7980export const Page: React.FC = () => {81const page_actions = useActions("page");8283const { pageStyle } = useAppContext();84const { isNarrow, fileUseStyle, topBarStyle, projectsNavStyle } = pageStyle;8586const intl = useIntl();8788const open_projects = useTypedRedux("projects", "open_projects");89const [show_label, set_show_label] = useState<boolean>(true);90useEffect(() => {91const next = open_projects.size <= HIDE_LABEL_THRESHOLD;92if (next != show_label) {93set_show_label(next);94}95}, [open_projects]);9697useEffect(() => {98return () => {99page_actions.clear_all_handlers();100};101}, []);102103const [showSignInTab, setShowSignInTab] = useState<boolean>(false);104useEffect(() => {105setTimeout(() => setShowSignInTab(true), 3000);106}, []);107108const active_top_tab = useTypedRedux("page", "active_top_tab");109const show_mentions = active_top_tab === "notifications";110const show_connection = useTypedRedux("page", "show_connection");111const show_file_use = useTypedRedux("page", "show_file_use");112const fullscreen = useTypedRedux("page", "fullscreen");113const local_storage_warning = useTypedRedux("page", "local_storage_warning");114const cookie_warning = useTypedRedux("page", "cookie_warning");115116const accountIsReady = useTypedRedux("account", "is_ready");117const account_id = useTypedRedux("account", "account_id");118const is_logged_in = useTypedRedux("account", "is_logged_in");119const is_anonymous = useTypedRedux("account", "is_anonymous");120const ephemeral = useTypedRedux("account", "ephemeral");121const when_account_created = useTypedRedux("account", "created");122const groups = useTypedRedux("account", "groups");123const show_i18n = useShowI18NBanner();124125const is_commercial = useTypedRedux("customize", "is_commercial");126const insecure_test_mode = useTypedRedux("customize", "insecure_test_mode");127128function account_tab_icon(): IconName | React.JSX.Element {129if (is_anonymous) {130return <></>;131} else if (account_id) {132return (133<Avatar134size={20}135account_id={account_id}136no_tooltip={true}137no_loading={true}138/>139);140} else {141return "cog";142}143}144145function render_account_tab(): React.JSX.Element {146if (!accountIsReady) {147return (148<div>149<Spin delay={1000} />150</div>151);152}153const icon = account_tab_icon();154let label, style;155if (is_anonymous && !ephemeral) {156let mesg;157style = { fontWeight: "bold", opacity: 0 };158if (159when_account_created &&160Date.now() - when_account_created.valueOf() >= 1000 * 60 * 60161) {162mesg = "Sign Up NOW to avoid losing all of your work!";163style.width = "400px";164} else {165mesg = "Sign Up!";166}167label = (168<Button id="anonymous-sign-up" bsStyle="success" style={style}>169{mesg}170</Button>171);172style = { marginTop: "-1px" }; // compensate for using a button173/* We only actually show the button if it is still there a few174seconds later. This avoids flickering it for a moment during175normal sign in. This feels like a hack, but was super176quick to implement.177*/178setTimeout(() => $("#anonymous-sign-up").css("opacity", 1), 3000);179} else {180label = undefined;181style = undefined;182}183184return (185<NavTab186name="account"187label={label}188style={style}189label_class={NAV_CLASS}190icon={icon}191active_top_tab={active_top_tab}192hide_label={!show_label}193tooltip={intl.formatMessage(labels.account)}194/>195);196}197198function render_balance() {199if (!is_commercial) return;200return <BalanceButton minimal topBar />;201}202203function render_admin_tab(): React.JSX.Element | undefined {204if (is_logged_in && groups?.includes("admin")) {205return (206<NavTab207name="admin"208label_class={NAV_CLASS}209icon={"users"}210active_top_tab={active_top_tab}211hide_label={!show_label}212/>213);214}215}216217function render_sign_in_tab(): React.JSX.Element | null {218if (is_logged_in || !showSignInTab) return null;219220return (221<Next222sameTab223href="/auth/sign-in"224style={{225backgroundColor: COLORS.TOP_BAR.SIGN_IN_BG,226fontSize: "16pt",227color: "black",228padding: "5px 15px",229}}230>231<Icon name="sign-in" />{" "}232{intl.formatMessage({233id: "page.sign_in.label",234defaultMessage: "Sign in",235})}236</Next>237);238}239240function render_support(): React.JSX.Element | undefined {241if (!is_commercial) {242return;243}244// Note: that styled span around the label is just245// because I'm too lazy to fix this properly, since246// it's all ancient react bootstrap stuff that will247// get rewritten.248return (249<NavTab250name={undefined} // does not open a tab, just a popup251active_top_tab={active_top_tab} // it's never supposed to be active!252label={intl.formatMessage({253id: "page.help.label",254defaultMessage: "Help",255})}256label_class={NAV_CLASS}257icon={"medkit"}258on_click={openSupportTab}259hide_label={!show_label}260/>261);262}263264function render_bell(): React.JSX.Element | undefined {265if (!is_logged_in || is_anonymous) return;266return (267<Notification type="bell" active={show_file_use} pageStyle={pageStyle} />268);269}270271function render_notification(): React.JSX.Element | undefined {272if (!is_logged_in || is_anonymous) return;273return (274<Notification275type="notifications"276active={show_mentions}277pageStyle={pageStyle}278/>279);280}281282function render_fullscreen(): React.JSX.Element | undefined {283if (isNarrow || is_anonymous) return;284285return <FullscreenButton pageStyle={pageStyle} />;286}287288function render_right_nav(): React.JSX.Element {289return (290<div291className="smc-right-tabs-fixed"292style={{293display: "flex",294flex: "0 0 auto",295height: `${pageStyle.height}px`,296margin: "0",297overflowY: "hidden",298alignItems: "center",299}}300>301{render_admin_tab()}302{render_sign_in_tab()}303{render_support()}304{is_logged_in ? render_account_tab() : undefined}305{render_balance()}306{render_notification()}307{render_bell()}308{!is_anonymous && (309<ConnectionIndicator310height={pageStyle.height}311pageStyle={pageStyle}312/>313)}314{render_fullscreen()}315</div>316);317}318319function render_project_nav_button(): React.JSX.Element {320return (321<NavTab322style={{323height: `${pageStyle.height}px`,324margin: "0",325overflow: "hidden",326}}327name={"projects"}328active_top_tab={active_top_tab}329tooltip={intl.formatMessage({330id: "page.project_nav.tooltip",331defaultMessage: "Show all the projects on which you collaborate.",332})}333icon="edit"334label={intl.formatMessage(labels.projects)}335/>336);337}338339// register a default drag and drop handler, that prevents340// accidental file drops341// TEST: make sure that usual drag'n'drop activities342// like rearranging tabs and reordering tasks work343function drop(e) {344if (DEBUG) {345e.persist();346}347//console.log "react desktop_app.drop", e348e.preventDefault();349e.stopPropagation();350if (e.dataTransfer.files.length > 0) {351alert_message({352type: "info",353title: "File Drop Rejected",354message:355'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.',356});357}358}359360// Children must define their own padding from navbar and screen borders361// Note that the parent is a flex container362const body = (363<div364style={PAGE_STYLE}365onDragOver={(e) => e.preventDefault()}366onDrop={drop}367>368{insecure_test_mode && <InsecureTestModeBanner />}369{show_file_use && (370<div style={fileUseStyle} className="smc-vfill">371<FileUsePage />372</div>373)}374{show_connection && <ConnectionInfo />}375<VersionWarning />376{cookie_warning && <CookieWarning />}377{local_storage_warning && <LocalStorageWarning />}378{show_i18n && <I18NBanner />}379<VerifyEmail />380{!fullscreen && (381<nav className="smc-top-bar" style={topBarStyle}>382<AppLogo size={pageStyle.height} />383{is_logged_in && render_project_nav_button()}384{!isNarrow ? (385<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />386) : (387// we need an expandable placeholder, otherwise the right-nav-buttons won't align to the right388<div style={{ flex: "1 1 auto" }} />389)}390{render_right_nav()}391</nav>392)}393{fullscreen && render_fullscreen()}394{isNarrow && (395<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />396)}397<ActiveContent />398<PayAsYouGoModal />399<PopconfirmModal />400<SettingsModal />401</div>402);403return (404<ClientContext.Provider value={{ client: webapp_client }}>405{body}406</ClientContext.Provider>407);408};409410411