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