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