Skip to content

Commit a68fda9

Browse files
committed
nudge new and returning users in the right direction by ensuring they have a project and can create a file with just one click.
1 parent e19b292 commit a68fda9

File tree

11 files changed

+136
-32
lines changed

11 files changed

+136
-32
lines changed

src/packages/frontend/app/query-params.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ export function init_query_params(): void {
7979
// not have session in the URL, so we can share url's without infected
8080
// other user's session.
8181
QueryParams.remove("session");
82+
8283
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Do something somewhat friendly when a user signs in for the first time,
3+
either after creating an account or being signed out.
4+
5+
For now:
6+
7+
- ensure they are a collab on at least one project
8+
- open the most recent project they actively used and show the +New page
9+
10+
That's it for now.
11+
*/
12+
13+
import { delay } from "awaiting";
14+
import { redux } from "@cocalc/frontend/app-framework";
15+
import { once } from "@cocalc/util/async-utils";
16+
import { webapp_client } from "@cocalc/frontend/webapp-client";
17+
import { cmp } from "@cocalc/util/misc";
18+
import { QueryParams } from "@cocalc/frontend/misc/query-params";
19+
20+
export default async function signInAction() {
21+
const signIn = QueryParams.get("sign-in");
22+
if (signIn == null) {
23+
return;
24+
}
25+
QueryParams.remove("sign-in");
26+
await delay(1); // so projects store is created (not in sync initial load loop)
27+
const project_id = await getProject();
28+
const actions = redux.getActions("projects");
29+
actions.open_project({ project_id, switch_to: true, target: "new" });
30+
await actions.start_project(project_id);
31+
}
32+
33+
async function create(title = "My First Project") {
34+
const project_id = await webapp_client.project_client.create({
35+
title,
36+
description: "",
37+
});
38+
const projects = redux.getStore("projects");
39+
// wait until projects_map is loaded, so we know what projects the users has (probably)
40+
while (projects.getIn(["project_map", project_id]) == null) {
41+
await once(projects, "change");
42+
}
43+
return project_id;
44+
}
45+
46+
async function getProject(): Promise<string> {
47+
const projects = redux.getStore("projects");
48+
// wait until projects_map is loaded, so we know what projects the users has (probably)
49+
while (projects.get("project_map") == null) {
50+
await once(projects, "change");
51+
}
52+
const account = redux.getStore("account");
53+
while (account.get("created") == null) {
54+
await once(account, "change");
55+
}
56+
57+
const created = account.get("created");
58+
let project_map = projects.get("project_map")!;
59+
if (project_map.size == 0) {
60+
// no known projects -- could be a new account, or could be an old account and no *recent* projects
61+
if (
62+
(created?.valueOf() ?? Date.now()) >=
63+
Date.now() - 2 * 24 * 60 * 60 * 1000
64+
) {
65+
// new account -- make a project
66+
return await create("My First Project");
67+
} else {
68+
// old account but no projects -- try loading all.
69+
const projectActions = redux.getActions("projects");
70+
await projectActions.load_all_projects();
71+
project_map = projects.get("project_map")!;
72+
if (project_map.size == 0) {
73+
// still nothing -- just create
74+
return await create();
75+
}
76+
}
77+
}
78+
79+
const account_id = account.get("account_id");
80+
81+
// now there should be at least one project in project_map.
82+
// Is there a non-deleted non-hidden project?
83+
const options: any[] = [];
84+
for (const [_, project] of project_map) {
85+
if (project.get("deleted")) {
86+
continue;
87+
}
88+
if (project.getIn(["users", account_id, "hide"])) {
89+
continue;
90+
}
91+
options.push(project);
92+
}
93+
if (options.length == 0) {
94+
return await create();
95+
}
96+
97+
// Sort the projects by when YOU were last active on the project, or if you were
98+
// never active on any project, by when the projects was last_edited.
99+
const usedByYou = options.filter((x) => x.getIn(["last_active", account_id]));
100+
101+
if (usedByYou.length == 0) {
102+
// you were never active on any project, so just return project most recently edited
103+
options.sort((x, y) => -cmp(x.get("last_edited"), y.get("last_edited")));
104+
return options[0].get("project_id");
105+
}
106+
107+
usedByYou.sort(
108+
(x, y) =>
109+
-cmp(
110+
x.getIn(["last_active", account_id]),
111+
y.getIn(["last_active", account_id]),
112+
),
113+
);
114+
return usedByYou[0].get("project_id");
115+
}

src/packages/frontend/compute/public-templates.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export default function PublicTemplates({
313313
if (loading) {
314314
return (
315315
<div style={{ maxWidth: "1200px", margin: "15px auto", ...style }}>
316-
Loading Templates... <Spin />
316+
Loading Templates... <Spin delay={3000} />
317317
</div>
318318
);
319319
}

src/packages/frontend/entry-point.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import "./launch/actions";
2525

2626
// Various jquery plugins:
2727
import "./jquery-plugins";
28-
import '@ant-design/v5-patch-for-react-19';
28+
import "@ant-design/v5-patch-for-react-19";
2929

3030
// Initialize app stores, actions, etc.
3131
import { init as initJqueryPlugins } from "./jquery-plugins";

src/packages/frontend/projects/actions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,10 @@ export class ProjectsActions extends Actions<ProjectsState> {
931931
project_id: string,
932932
options: { disablePayAsYouGo?: boolean } = {},
933933
): Promise<boolean> => {
934-
if (!(await allow_project_to_run(project_id))) {
934+
if (
935+
!(await allow_project_to_run(project_id)) ||
936+
!store.getIn(["project_map", project_id])
937+
) {
935938
return false;
936939
}
937940
if (!options.disablePayAsYouGo) {

src/packages/frontend/session.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { bind_methods } from "@cocalc/util/misc";
1818
import target from "@cocalc/frontend/client/handle-target";
1919
import { load_target } from "./history";
2020
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
21+
import signInAction from "@cocalc/frontend/app/sign-in-action";
2122

2223
const log = (..._args) => {
2324
// console.log("session: ", ..._args);
@@ -127,6 +128,9 @@ class SessionManager {
127128
this._initialized = true;
128129
// ... and load a target URL
129130
SessionManager.load_url_target();
131+
// and finally possibly do a sign in action if the user just signed up
132+
// or signed in after a while:
133+
await signInAction();
130134
} catch (err) {
131135
console.warn("Error restoring session:", err);
132136
}

src/packages/next/components/auth/sign-in.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ function SignIn0(props: SignInProps) {
140140
{strategies == null
141141
? "Sign in"
142142
: haveSSO
143-
? requiredSSO != null
144-
? "Sign in using your single sign-on provider"
145-
: "Sign in using your email address or a single sign-on provider."
146-
: "Sign in using your email address."}
143+
? requiredSSO != null
144+
? "Sign in using your single sign-on provider"
145+
: "Sign in using your email address or a single sign-on provider."
146+
: "Sign in using your email address."}
147147
</div>
148148
<form>
149149
{haveSSO && (
@@ -194,7 +194,6 @@ function SignIn0(props: SignInProps) {
194194
{requiredSSO == null && (
195195
<>
196196
<Button
197-
disabled={signingIn || !email || !(password?.length >= 6)}
198197
shape="round"
199198
size="large"
200199
type="primary"

src/packages/next/components/auth/sign-up.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
GoogleReCaptchaProvider,
1010
useGoogleReCaptcha,
1111
} from "react-google-recaptcha-v3";
12-
1312
import Markdown from "@cocalc/frontend/editors/slate/static-markdown";
1413
import { MAX_PASSWORD_LENGTH } from "@cocalc/util/auth";
1514
import {
@@ -38,7 +37,7 @@ const LINE: CSSProperties = { margin: "15px 0" } as const;
3837
interface SignUpProps {
3938
minimal?: boolean; // use a minimal interface with less explanation and instructions (e.g., for embedding in other pages)
4039
requiresToken?: boolean; // will be determined by API call if not given.
41-
onSuccess?: (opts?: {}) => void; // if given, call after sign up *succeeds*.
40+
onSuccess?: () => void; // if given, call after sign up *succeeds*.
4241
has_site_license?: boolean;
4342
publicPathId?: string;
4443
showSignIn?: boolean;
@@ -173,7 +172,7 @@ function SignUp0({
173172
if (result.issues && len(result.issues) > 0) {
174173
setIssues(result.issues);
175174
} else {
176-
onSuccess?.({});
175+
onSuccess?.();
177176
}
178177
} catch (err) {
179178
setIssues({ error: `${err}` });

src/packages/next/locales/en/index.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"ar": "Arabic",
33
"chat-text": "<p><strong><A>CoCalc's chatrooms</A></strong> offer a range of features to enhance communication and collaboration:</p> <ul> <li><strong>@-mentions</strong> for directly addressing collaborators or querying a language model</li> <li><strong>Emoticons</strong> support for expressing emotions</li> <li><strong>LaTeX formula</strong> rendering between $ signs</li> <li><strong>Threads</strong> and <strong>Hashtags</strong> for easy topic organization</li> <li><strong>Image insertion</strong> via upload or drag-and-drop</li> <li><strong>Markdown syntax</strong> for text formatting and link insertion</li> <li><strong>Notifications</strong> system with a bell icon for alerting users to new activity</li> </ul>",
44
"chat-title": "Chat Rooms",
5-
"compute-servers-text": "<p>Extend your CoCalc projects with powerful <strong>compute servers</strong>. They give you much more power, GPU options, and flexibility for your computations. From within your project, spin up and connect to a powerful machine and tell your terminals and Jupyter Notebooks to run on these machines.</p><p>These servers optionally come with <strong>very competitively priced GPU support</strong>, they are <strong>billed by the second</strong> and your files are <strong>seamlessly synchronized</strong> between your project and servers.</p><p><A1>Read the docs</A1> and <A2>check out some applications</A2>.</p>",
5+
"compute-servers-text": "<p>Extend your CoCalc projects with <strong>compute servers</strong>. Spin up and connect to a powerful machine or GPU and run your terminals and Jupyter Notebooks on these machines.</p><p>These come with <strong>very competitively priced GPU support</strong>, they are <strong>billed by the second</strong> and your files are <strong>seamlessly synchronized</strong> between your project and servers.</p><p><A1>Read the docs</A1> and <A2>check out some applications</A2>.</p>",
66
"compute-servers-title": "Compute Servers with GPU support",
77
"de": "German",
88
"en": "English",

src/packages/next/pages/auth/sign-in.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function Home({ customize }) {
2323
<Layout>
2424
<Header page="sign-in" />
2525
<Layout.Content style={{ backgroundColor: "white" }}>
26-
<SignIn onSuccess={() => router.push("/")} />
26+
<SignIn onSuccess={() => router.push("/app?sign-in")} />
2727
<Footer />
2828
</Layout.Content>
2929
</Layout>

0 commit comments

Comments
 (0)