Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/app/page.tsx
Views: 687
/*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 { useIntl } from "react-intl";13import { Avatar } from "@cocalc/frontend/account/avatar/avatar";14import { alert_message } from "@cocalc/frontend/alerts";15import { Button } from "@cocalc/frontend/antd-bootstrap";16import {17CSS,18React,19useActions,20useEffect,21useState,22useTypedRedux,23} from "@cocalc/frontend/app-framework";24import { Loading } from "@cocalc/frontend/components";25import { IconName } from "@cocalc/frontend/components/icon";26import { SiteName } from "@cocalc/frontend/customize";27import { FileUsePage } from "@cocalc/frontend/file-use/page";28import { labels } from "@cocalc/frontend/i18n";29import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";30import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";31import openSupportTab from "@cocalc/frontend/support/open";32import { COLORS } from "@cocalc/util/theme";33import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";34import { ActiveContent } from "./active-content";35import { ConnectionIndicator } from "./connection-indicator";36import { ConnectionInfo } from "./connection-info";37import { useAppContext } from "./context";38import { FullscreenButton } from "./fullscreen-button";39import { AppLogo } from "./logo";40import { NavTab } from "./nav-tab";41import { Notification } from "./notifications";42import PopconfirmModal from "./popconfirm-modal";43import { HIDE_LABEL_THRESHOLD, NAV_CLASS } from "./top-nav-consts";44import { CookieWarning, LocalStorageWarning, VersionWarning } from "./warnings";45import { I18NBanner, useShowI18NBanner } from "./i18n-banner";46import SettingsModal from "./settings-modal";47import InsecureTestModeBanner from "./insecure-test-mode-banner";4849// ipad and ios have a weird trick where they make the screen50// actually smaller than 100vh and have it be scrollable, even51// when overflow:hidden, which causes massive UI pain to cocalc.52// so in that case we make the page_height less. Without this53// one little tricky, cocalc is very, very frustrating to use54// on mobile safari. See the million discussions over the years:55// https://liuhao.im/english/2015/05/29/ios-safari-window-height.html56// ...57// https://lukechannings.com/blog/2021-06-09-does-safari-15-fix-the-vh-bug/58const PAGE_HEIGHT: string =59IS_MOBILE || IS_SAFARI60? `calc(100vh - env(safe-area-inset-bottom) - ${IS_IOS ? 80 : 20}px)`61: "100vh";6263const PAGE_STYLE: CSS = {64display: "flex",65flexDirection: "column",66height: PAGE_HEIGHT, // see note67width: "100vw",68overflow: "hidden",69background: "white",70} as const;7172export const Page: React.FC = () => {73const page_actions = useActions("page");7475const { pageStyle } = useAppContext();76const { isNarrow, fileUseStyle, topBarStyle, projectsNavStyle } = pageStyle;7778const intl = useIntl();7980const open_projects = useTypedRedux("projects", "open_projects");81const [show_label, set_show_label] = useState<boolean>(true);82useEffect(() => {83const next = open_projects.size <= HIDE_LABEL_THRESHOLD;84if (next != show_label) {85set_show_label(next);86}87}, [open_projects]);8889useEffect(() => {90return () => {91page_actions.clear_all_handlers();92};93}, []);9495const active_top_tab = useTypedRedux("page", "active_top_tab");96const show_mentions = active_top_tab === "notifications";97const show_connection = useTypedRedux("page", "show_connection");98const show_file_use = useTypedRedux("page", "show_file_use");99const fullscreen = useTypedRedux("page", "fullscreen");100const local_storage_warning = useTypedRedux("page", "local_storage_warning");101const cookie_warning = useTypedRedux("page", "cookie_warning");102const new_version = useTypedRedux("page", "new_version");103104const account_id = useTypedRedux("account", "account_id");105const is_logged_in = useTypedRedux("account", "is_logged_in");106const is_anonymous = useTypedRedux("account", "is_anonymous");107const doing_anonymous_setup = useTypedRedux(108"account",109"doing_anonymous_setup",110);111const when_account_created = useTypedRedux("account", "created");112const groups = useTypedRedux("account", "groups");113const show_i18n = useShowI18NBanner();114115const is_commercial = useTypedRedux("customize", "is_commercial");116const insecure_test_mode = useTypedRedux("customize", "insecure_test_mode");117118function account_tab_icon(): IconName | JSX.Element {119if (is_anonymous) {120return <></>;121} else if (account_id) {122return (123<Avatar124size={20}125account_id={account_id}126no_tooltip={true}127no_loading={true}128/>129);130} else {131return "cog";132}133}134135function render_account_tab(): JSX.Element {136const icon = account_tab_icon();137let label, style;138if (is_anonymous) {139let mesg;140style = { fontWeight: "bold", opacity: 0 };141if (142when_account_created &&143Date.now() - when_account_created.valueOf() >= 1000 * 60 * 60144) {145mesg = "Sign Up NOW to avoid losing all of your work!";146style.width = "400px";147} else {148mesg = "Sign Up!";149}150label = (151<Button id="anonymous-sign-up" bsStyle="success" style={style}>152{mesg}153</Button>154);155style = { marginTop: "-1px" }; // compensate for using a button156/* We only actually show the button if it is still there a few157seconds later. This avoids flickering it for a moment during158normal sign in. This feels like a hack, but was super159quick to implement.160*/161setTimeout(() => $("#anonymous-sign-up").css("opacity", 1), 3000);162} else {163label = intl.formatMessage(labels.account);164style = undefined;165}166167return (168<NavTab169name="account"170label={label}171style={style}172label_class={NAV_CLASS}173icon={icon}174active_top_tab={active_top_tab}175hide_label={!show_label}176/>177);178}179180function render_admin_tab(): JSX.Element | undefined {181if (is_logged_in && groups?.includes("admin")) {182return (183<NavTab184name="admin"185label={"Admin"}186label_class={NAV_CLASS}187icon={"users"}188active_top_tab={active_top_tab}189hide_label={!show_label}190/>191);192}193}194195function sign_in_tab_clicked() {196if (active_top_tab === "account") {197page_actions.sign_in();198}199}200201function render_sign_in_tab(): JSX.Element | null {202if (is_logged_in) return null;203204let style: CSS | undefined = undefined;205if (active_top_tab !== "account") {206// Strongly encourage clicking on the sign in tab.207// Especially important if user got signed out due208// to cookie expiring or being deleted (say).209style = { backgroundColor: COLORS.TOP_BAR.SIGN_IN_BG, fontSize: "16pt" };210}211return (212<NavTab213name="account"214label={intl.formatMessage({215id: "page.sign_in.label",216defaultMessage: "Sign in",217})}218label_class={NAV_CLASS}219icon="sign-in"220on_click={sign_in_tab_clicked}221active_top_tab={active_top_tab}222style={style}223add_inner_style={{ color: "black" }}224hide_label={!show_label}225/>226);227}228229function render_support(): JSX.Element | undefined {230if (!is_commercial) {231return;232}233// Note: that styled span around the label is just234// because I'm too lazy to fix this properly, since235// it's all ancient react bootstrap stuff that will236// get rewritten.237return (238<NavTab239name={undefined} // does not open a tab, just a popup240active_top_tab={active_top_tab} // it's never supposed to be active!241label={intl.formatMessage({242id: "page.help.label",243defaultMessage: "Help",244})}245label_class={NAV_CLASS}246icon={"medkit"}247on_click={openSupportTab}248hide_label={!show_label}249/>250);251}252253function render_bell(): JSX.Element | undefined {254if (!is_logged_in || is_anonymous) return;255return (256<Notification type="bell" active={show_file_use} pageStyle={pageStyle} />257);258}259260function render_notification(): JSX.Element | undefined {261if (!is_logged_in || is_anonymous) return;262return (263<Notification264type="notifications"265active={show_mentions}266pageStyle={pageStyle}267/>268);269}270271function render_fullscreen(): JSX.Element | undefined {272if (isNarrow || is_anonymous) return;273274return <FullscreenButton pageStyle={pageStyle} />;275}276277function render_right_nav(): JSX.Element {278return (279<div280className="smc-right-tabs-fixed"281style={{282display: "flex",283flex: "0 0 auto",284height: `${pageStyle.height}px`,285margin: "0",286overflowY: "hidden",287alignItems: "center",288}}289>290{render_admin_tab()}291{render_sign_in_tab()}292{render_support()}293{is_logged_in && render_account_tab()}294{render_notification()}295{render_bell()}296{!is_anonymous && (297<ConnectionIndicator298height={pageStyle.height}299pageStyle={pageStyle}300/>301)}302{render_fullscreen()}303</div>304);305}306307function render_project_nav_button(): JSX.Element {308return (309<NavTab310style={{311height: `${pageStyle.height}px`,312margin: "0",313overflow: "hidden",314}}315name={"projects"}316active_top_tab={active_top_tab}317tooltip={intl.formatMessage({318id: "page.project_nav.tooltip",319defaultMessage: "Show all the projects on which you collaborate.",320})}321icon="edit"322label={intl.formatMessage(labels.projects)}323/>324);325}326327// register a default drag and drop handler, that prevents328// accidental file drops329// TEST: make sure that usual drag'n'drop activities330// like rearranging tabs and reordering tasks work331function drop(e) {332if (DEBUG) {333e.persist();334}335//console.log "react desktop_app.drop", e336e.preventDefault();337e.stopPropagation();338if (e.dataTransfer.files.length > 0) {339alert_message({340type: "info",341title: "File Drop Rejected",342message:343'To upload a file, drop it onto the files listing or the "Drop files to upload" area in the +New tab.',344});345}346}347348if (doing_anonymous_setup) {349// Don't show the login screen or top navbar for a second350// while creating their anonymous account, since that351// would just be ugly/confusing/and annoying.352// Have to use above style to *hide* the crash warning.353const loading_anon = (354<div style={{ margin: "auto", textAlign: "center" }}>355<h1 style={{ color: COLORS.GRAY }}>356<Loading />357</h1>358<div style={{ color: COLORS.GRAY, width: "50vw" }}>359Please give <SiteName /> a couple of seconds to start your project and360prepare a file...361</div>362</div>363);364return <div style={PAGE_STYLE}>{loading_anon}</div>;365}366367// Children must define their own padding from navbar and screen borders368// Note that the parent is a flex container369return (370<div371style={PAGE_STYLE}372onDragOver={(e) => e.preventDefault()}373onDrop={drop}374>375{insecure_test_mode && <InsecureTestModeBanner />}376{show_file_use && (377<div style={fileUseStyle} className="smc-vfill">378<FileUsePage />379</div>380)}381{show_connection && <ConnectionInfo />}382{new_version && <VersionWarning new_version={new_version} />}383{cookie_warning && <CookieWarning />}384{local_storage_warning && <LocalStorageWarning />}385{show_i18n && <I18NBanner />}386{!fullscreen && (387<nav className="smc-top-bar" style={topBarStyle}>388<AppLogo size={pageStyle.height} />389{is_logged_in && render_project_nav_button()}390{!isNarrow ? (391<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />392) : (393// we need an expandable placeholder, otherwise the right-nav-buttons won't align to the right394<div style={{ flex: "1 1 auto" }} />395)}396{render_right_nav()}397</nav>398)}399{fullscreen && render_fullscreen()}400{isNarrow && (401<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />402)}403<ActiveContent />404<PayAsYouGoModal />405<PopconfirmModal />406<SettingsModal />407</div>408);409};410411412