CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/app/page.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
This defines the entire **desktop** Cocalc page layout and brings in
8
everything on *desktop*, once the user has signed in.
9
*/
10
11
declare var DEBUG: boolean;
12
13
import { useIntl } from "react-intl";
14
import { Avatar } from "@cocalc/frontend/account/avatar/avatar";
15
import { alert_message } from "@cocalc/frontend/alerts";
16
import { Button } from "@cocalc/frontend/antd-bootstrap";
17
import {
18
CSS,
19
React,
20
useActions,
21
useEffect,
22
useState,
23
useTypedRedux,
24
} from "@cocalc/frontend/app-framework";
25
import { Loading } from "@cocalc/frontend/components";
26
import { IconName } from "@cocalc/frontend/components/icon";
27
import { SiteName } from "@cocalc/frontend/customize";
28
import { FileUsePage } from "@cocalc/frontend/file-use/page";
29
import { labels } from "@cocalc/frontend/i18n";
30
import { ProjectsNav } from "@cocalc/frontend/projects/projects-nav";
31
import PayAsYouGoModal from "@cocalc/frontend/purchases/pay-as-you-go/modal";
32
import openSupportTab from "@cocalc/frontend/support/open";
33
import { COLORS } from "@cocalc/util/theme";
34
import { IS_IOS, IS_MOBILE, IS_SAFARI } from "../feature";
35
import { ActiveContent } from "./active-content";
36
import { ConnectionIndicator } from "./connection-indicator";
37
import { ConnectionInfo } from "./connection-info";
38
import { useAppContext } from "./context";
39
import { FullscreenButton } from "./fullscreen-button";
40
import { AppLogo } from "./logo";
41
import { NavTab } from "./nav-tab";
42
import { Notification } from "./notifications";
43
import PopconfirmModal from "./popconfirm-modal";
44
import { HIDE_LABEL_THRESHOLD, NAV_CLASS } from "./top-nav-consts";
45
import { CookieWarning, LocalStorageWarning, VersionWarning } from "./warnings";
46
import { I18NBanner, useShowI18NBanner } from "./i18n-banner";
47
import SettingsModal from "./settings-modal";
48
import InsecureTestModeBanner from "./insecure-test-mode-banner";
49
50
// ipad and ios have a weird trick where they make the screen
51
// actually smaller than 100vh and have it be scrollable, even
52
// when overflow:hidden, which causes massive UI pain to cocalc.
53
// so in that case we make the page_height less. Without this
54
// one little tricky, cocalc is very, very frustrating to use
55
// on mobile safari. See the million discussions over the years:
56
// https://liuhao.im/english/2015/05/29/ios-safari-window-height.html
57
// ...
58
// https://lukechannings.com/blog/2021-06-09-does-safari-15-fix-the-vh-bug/
59
const PAGE_HEIGHT: string =
60
IS_MOBILE || IS_SAFARI
61
? `calc(100vh - env(safe-area-inset-bottom) - ${IS_IOS ? 80 : 20}px)`
62
: "100vh";
63
64
const PAGE_STYLE: CSS = {
65
display: "flex",
66
flexDirection: "column",
67
height: PAGE_HEIGHT, // see note
68
width: "100vw",
69
overflow: "hidden",
70
background: "white",
71
} as const;
72
73
export const Page: React.FC = () => {
74
const page_actions = useActions("page");
75
76
const { pageStyle } = useAppContext();
77
const { isNarrow, fileUseStyle, topBarStyle, projectsNavStyle } = pageStyle;
78
79
const intl = useIntl();
80
81
const open_projects = useTypedRedux("projects", "open_projects");
82
const [show_label, set_show_label] = useState<boolean>(true);
83
useEffect(() => {
84
const next = open_projects.size <= HIDE_LABEL_THRESHOLD;
85
if (next != show_label) {
86
set_show_label(next);
87
}
88
}, [open_projects]);
89
90
useEffect(() => {
91
return () => {
92
page_actions.clear_all_handlers();
93
};
94
}, []);
95
96
const active_top_tab = useTypedRedux("page", "active_top_tab");
97
const show_mentions = active_top_tab === "notifications";
98
const show_connection = useTypedRedux("page", "show_connection");
99
const show_file_use = useTypedRedux("page", "show_file_use");
100
const fullscreen = useTypedRedux("page", "fullscreen");
101
const local_storage_warning = useTypedRedux("page", "local_storage_warning");
102
const cookie_warning = useTypedRedux("page", "cookie_warning");
103
const new_version = useTypedRedux("page", "new_version");
104
105
const account_id = useTypedRedux("account", "account_id");
106
const is_logged_in = useTypedRedux("account", "is_logged_in");
107
const is_anonymous = useTypedRedux("account", "is_anonymous");
108
const doing_anonymous_setup = useTypedRedux(
109
"account",
110
"doing_anonymous_setup",
111
);
112
const when_account_created = useTypedRedux("account", "created");
113
const groups = useTypedRedux("account", "groups");
114
const show_i18n = useShowI18NBanner();
115
116
const is_commercial = useTypedRedux("customize", "is_commercial");
117
const insecure_test_mode = useTypedRedux("customize", "insecure_test_mode");
118
119
function account_tab_icon(): IconName | JSX.Element {
120
if (is_anonymous) {
121
return <></>;
122
} else if (account_id) {
123
return (
124
<Avatar
125
size={20}
126
account_id={account_id}
127
no_tooltip={true}
128
no_loading={true}
129
/>
130
);
131
} else {
132
return "cog";
133
}
134
}
135
136
function render_account_tab(): JSX.Element {
137
const icon = account_tab_icon();
138
let label, style;
139
if (is_anonymous) {
140
let mesg;
141
style = { fontWeight: "bold", opacity: 0 };
142
if (
143
when_account_created &&
144
Date.now() - when_account_created.valueOf() >= 1000 * 60 * 60
145
) {
146
mesg = "Sign Up NOW to avoid losing all of your work!";
147
style.width = "400px";
148
} else {
149
mesg = "Sign Up!";
150
}
151
label = (
152
<Button id="anonymous-sign-up" bsStyle="success" style={style}>
153
{mesg}
154
</Button>
155
);
156
style = { marginTop: "-1px" }; // compensate for using a button
157
/* We only actually show the button if it is still there a few
158
seconds later. This avoids flickering it for a moment during
159
normal sign in. This feels like a hack, but was super
160
quick to implement.
161
*/
162
setTimeout(() => $("#anonymous-sign-up").css("opacity", 1), 3000);
163
} else {
164
label = intl.formatMessage(labels.account);
165
style = undefined;
166
}
167
168
return (
169
<NavTab
170
name="account"
171
label={label}
172
style={style}
173
label_class={NAV_CLASS}
174
icon={icon}
175
active_top_tab={active_top_tab}
176
hide_label={!show_label}
177
/>
178
);
179
}
180
181
function render_admin_tab(): JSX.Element | undefined {
182
if (is_logged_in && groups?.includes("admin")) {
183
return (
184
<NavTab
185
name="admin"
186
label={"Admin"}
187
label_class={NAV_CLASS}
188
icon={"users"}
189
active_top_tab={active_top_tab}
190
hide_label={!show_label}
191
/>
192
);
193
}
194
}
195
196
function sign_in_tab_clicked() {
197
if (active_top_tab === "account") {
198
page_actions.sign_in();
199
}
200
}
201
202
function render_sign_in_tab(): JSX.Element | null {
203
if (is_logged_in) return null;
204
205
let style: CSS | undefined = undefined;
206
if (active_top_tab !== "account") {
207
// Strongly encourage clicking on the sign in tab.
208
// Especially important if user got signed out due
209
// to cookie expiring or being deleted (say).
210
style = { backgroundColor: COLORS.TOP_BAR.SIGN_IN_BG, fontSize: "16pt" };
211
}
212
return (
213
<NavTab
214
name="account"
215
label={intl.formatMessage({
216
id: "page.sign_in.label",
217
defaultMessage: "Sign in",
218
})}
219
label_class={NAV_CLASS}
220
icon="sign-in"
221
on_click={sign_in_tab_clicked}
222
active_top_tab={active_top_tab}
223
style={style}
224
add_inner_style={{ color: "black" }}
225
hide_label={!show_label}
226
/>
227
);
228
}
229
230
function render_support(): JSX.Element | undefined {
231
if (!is_commercial) {
232
return;
233
}
234
// Note: that styled span around the label is just
235
// because I'm too lazy to fix this properly, since
236
// it's all ancient react bootstrap stuff that will
237
// get rewritten.
238
return (
239
<NavTab
240
name={undefined} // does not open a tab, just a popup
241
active_top_tab={active_top_tab} // it's never supposed to be active!
242
label={intl.formatMessage({
243
id: "page.help.label",
244
defaultMessage: "Help",
245
})}
246
label_class={NAV_CLASS}
247
icon={"medkit"}
248
on_click={openSupportTab}
249
hide_label={!show_label}
250
/>
251
);
252
}
253
254
function render_bell(): JSX.Element | undefined {
255
if (!is_logged_in || is_anonymous) return;
256
return (
257
<Notification type="bell" active={show_file_use} pageStyle={pageStyle} />
258
);
259
}
260
261
function render_notification(): JSX.Element | undefined {
262
if (!is_logged_in || is_anonymous) return;
263
return (
264
<Notification
265
type="notifications"
266
active={show_mentions}
267
pageStyle={pageStyle}
268
/>
269
);
270
}
271
272
function render_fullscreen(): JSX.Element | undefined {
273
if (isNarrow || is_anonymous) return;
274
275
return <FullscreenButton pageStyle={pageStyle} />;
276
}
277
278
function render_right_nav(): JSX.Element {
279
return (
280
<div
281
className="smc-right-tabs-fixed"
282
style={{
283
display: "flex",
284
flex: "0 0 auto",
285
height: `${pageStyle.height}px`,
286
margin: "0",
287
overflowY: "hidden",
288
alignItems: "center",
289
}}
290
>
291
{render_admin_tab()}
292
{render_sign_in_tab()}
293
{render_support()}
294
{is_logged_in && render_account_tab()}
295
{render_notification()}
296
{render_bell()}
297
{!is_anonymous && (
298
<ConnectionIndicator
299
height={pageStyle.height}
300
pageStyle={pageStyle}
301
/>
302
)}
303
{render_fullscreen()}
304
</div>
305
);
306
}
307
308
function render_project_nav_button(): JSX.Element {
309
return (
310
<NavTab
311
style={{
312
height: `${pageStyle.height}px`,
313
margin: "0",
314
overflow: "hidden",
315
}}
316
name={"projects"}
317
active_top_tab={active_top_tab}
318
tooltip={intl.formatMessage({
319
id: "page.project_nav.tooltip",
320
defaultMessage: "Show all the projects on which you collaborate.",
321
})}
322
icon="edit"
323
label={intl.formatMessage(labels.projects)}
324
/>
325
);
326
}
327
328
// register a default drag and drop handler, that prevents
329
// accidental file drops
330
// TEST: make sure that usual drag'n'drop activities
331
// like rearranging tabs and reordering tasks work
332
function drop(e) {
333
if (DEBUG) {
334
e.persist();
335
}
336
//console.log "react desktop_app.drop", e
337
e.preventDefault();
338
e.stopPropagation();
339
if (e.dataTransfer.files.length > 0) {
340
alert_message({
341
type: "info",
342
title: "File Drop Rejected",
343
message:
344
'To upload a file, drop it onto the files listing or the "Drop files to upload" area in the +New tab.',
345
});
346
}
347
}
348
349
if (doing_anonymous_setup) {
350
// Don't show the login screen or top navbar for a second
351
// while creating their anonymous account, since that
352
// would just be ugly/confusing/and annoying.
353
// Have to use above style to *hide* the crash warning.
354
const loading_anon = (
355
<div style={{ margin: "auto", textAlign: "center" }}>
356
<h1 style={{ color: COLORS.GRAY }}>
357
<Loading />
358
</h1>
359
<div style={{ color: COLORS.GRAY, width: "50vw" }}>
360
Please give <SiteName /> a couple of seconds to start your project and
361
prepare a file...
362
</div>
363
</div>
364
);
365
return <div style={PAGE_STYLE}>{loading_anon}</div>;
366
}
367
368
// Children must define their own padding from navbar and screen borders
369
// Note that the parent is a flex container
370
return (
371
<div
372
style={PAGE_STYLE}
373
onDragOver={(e) => e.preventDefault()}
374
onDrop={drop}
375
>
376
{insecure_test_mode && <InsecureTestModeBanner />}
377
{show_file_use && (
378
<div style={fileUseStyle} className="smc-vfill">
379
<FileUsePage />
380
</div>
381
)}
382
{show_connection && <ConnectionInfo />}
383
{new_version && <VersionWarning new_version={new_version} />}
384
{cookie_warning && <CookieWarning />}
385
{local_storage_warning && <LocalStorageWarning />}
386
{show_i18n && <I18NBanner />}
387
{!fullscreen && (
388
<nav className="smc-top-bar" style={topBarStyle}>
389
<AppLogo size={pageStyle.height} />
390
{is_logged_in && render_project_nav_button()}
391
{!isNarrow ? (
392
<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />
393
) : (
394
// we need an expandable placeholder, otherwise the right-nav-buttons won't align to the right
395
<div style={{ flex: "1 1 auto" }} />
396
)}
397
{render_right_nav()}
398
</nav>
399
)}
400
{fullscreen && render_fullscreen()}
401
{isNarrow && (
402
<ProjectsNav height={pageStyle.height} style={projectsNavStyle} />
403
)}
404
<ActiveContent />
405
<PayAsYouGoModal />
406
<PopconfirmModal />
407
<SettingsModal />
408
</div>
409
);
410
};
411
412