diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6c7f3ac31..38af23c00 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -116,12 +116,6 @@ jobs: env: CI: true - - name: Install Playwright - run: npx playwright install --with-deps - # if: steps.playwright-cache.outputs.cache-hit != 'true' - # env: - # PLAYWRIGHT_BROWSERS_PATH: 0 # https://github.com/microsoft/playwright/blob/main/docs/src/ci.md#caching-browsers - - name: Start HocusPocus server run: RUNNER_TRACKING_ID="" && npm run start:server & env: @@ -132,7 +126,13 @@ jobs: env: CI: true - # Actually build and run react code and run tests against that + - name: Install Playwright + run: npx playwright install --with-deps + # if: steps.playwright-cache.outputs.cache-hit != 'true' + # env: + # PLAYWRIGHT_BROWSERS_PATH: 0 # https://github.com/microsoft/playwright/blob/main/docs/src/ci.md#caching-browsers + + # Actually build and run react code and run playwright tests against that - name: Build and run preview # Wait on config file needed for vite dev server: https://github.com/jeffbski/wait-on/issues/78 run: npm run start:preview & npx wait-on http://localhost:4173 -c ./packages/editor/wait-on.conf.json diff --git a/README.md b/README.md index f3a844be0..83448c528 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ We've written about the main functionality of TypeCell in the [manual](https://w Another good way to learn is to check out some notebooks from our community: -ยป [View demo notebooks](/docs/demos.md) +ยป [View demo notebooks](/docs/Demos.md) # Feedback ๐Ÿ™‹โ€โ™‚๏ธ๐Ÿ™‹โ€โ™€๏ธ diff --git a/packages/editor/public/_docs/interactive-introduction.md b/packages/editor/public/_docs/Live coding tutorial.md similarity index 80% rename from packages/editor/public/_docs/interactive-introduction.md rename to packages/editor/public/_docs/Live coding tutorial.md index 330981ed2..d3ace6390 100644 --- a/packages/editor/public/_docs/interactive-introduction.md +++ b/packages/editor/public/_docs/Live coding tutorial.md @@ -1,9 +1,11 @@ -# Introduction to TypeCell +# TypeCell Live Coding tutorial -Welcome to TypeCell. A TypeCell document is a live, interactive programming environment for Javascript / Typescript +TypeCell documents contain a live, interactive programming environment for Javascript / Typescript running in your browser. -In this introduction, we will go through the basics of using TypeCell. +In this introduction, we will go through the basics of coding in TypeCell. + +__This document is completely editable. Follow the steps below!__ ## Cats @@ -56,10 +58,12 @@ export default ( ); ``` -Uh oh, what's this? I forgot to add a cell defining our friend. Can you do it for me? +Uh oh, what's this? I forgot to add a code block defining a friend for our cat. Can you do it for me? + +Add a code block by clicking on the + next to a block when hovering it (or type "/"), and select "code block". -A cell is a container for code & output. To add one, click on the + above or below another cell. -You can do it wherever you like. +_**Hint:** Our friend only needs a name for now. Use the same structure as you already +used for your cat (first code block), but only include the "name" field and export it as the variable named "friend"._ ```typescript // @default-collapsed @@ -74,17 +78,16 @@ export default ( ); ``` -Hint: Our friend only needs a name for now. Use the same structure as you already -used for your cat, but only include the `name` field. -Notice how we use `$.friend.name` in the cell above. Whenever you `export` a variable, you can access it across -the document by using the `$` symbol. In other words, `$` is a store for all variables that you want + +Notice how we use _$.friend.name_ in the cell above. Whenever you _export_ a variable, you can access it across +the document by using the _$_ symbol. In other words, _$_ is a store for all variables that you want to access across cells! Exported variables are also displayed below the cell. Code cells automatically run when: - You change the code of a cell -- Any of the reactive variables the cell references (from `$`) are changed +- Any of the reactive variables the cell references (from _$_) are changed ## Feeding neighbors @@ -172,16 +175,17 @@ export default ( ); ``` -We have now stored the number of dry & wet food required (we exported variables `dryFoodToPrepare` and `wetFoodToPrepare`). +We have now stored the number of dry & wet food required (we exported variables "dryFoodToPrepare" and "wetFoodToPrepare"). We also visualize them nicely with a friendly message and emojis using React & JSX. -See the default `export` at the end of the cell above. +See the default "export" at the end of the cell above. -React? JSX? What's this now? React is a Javascript framework that's used +_React? JSX? What's this now? React is a Javascript framework that's used to create user interfaces. We won't go too in depth on it here, but you can -check out the documentation at https://reactjs.org/docs/getting-started.html. +check out the documentation at https://reactjs.org/docs/getting-started.html._ -JSX is part of React, and makes it easy to create type-safe HTML elements. In TypeCell, just `export` JSX elements to create user interfaces or visualize data in your notebook. +_JSX is part of React, and makes it easy to create type-safe HTML elements. In TypeCell, just "export" JSX elements to create user interfaces or visualize data in your notebook._ +### Input fields Next, we'll create some user input fields to indicate how much food we have prepared. The built-in TypeCell Input library makes this easy: @@ -293,22 +297,25 @@ export default ( ); ``` -Go ahead, play with the inputs above to adjust how much food to prepare! +**Go ahead, play with the inputs above to adjust how much food to prepare!** These are just 2 of the many input types that TypeCell supports. To see the other choices, make sure to try the TypeCell inputs tutorial. -_Tip: expand the 3 cells above to see how they work._ +_**Tip:** expand the 3 cells above to see how they work._ ## Final notes -We hope this introduction has given you a sense of how TypeCell and reactive notebooks work. +We hope this introduction has given you a sense of how TypeCell interactive documents work. The live feedback and Reactive programming model should be pretty powerful. -There are a lot more features to discover, for example, -did you know you can import any NPM package you like, or even compose different notebooks? -Try creating your own notebook to give it a try, or have a look at the other examples. + +There are a lot more features to discover. For example, +did you know you can import any NPM package you like, or even compose different documents? + +Try signup up and create your own documents to give it a try, or have a look at the other examples. **Have fun using TypeCell!** -This tutorial is inspired by [pluto.jl](https://github.com/fonsp/Pluto.jl), thanks Fons & Nicholas! + +_This tutorial is inspired by [pluto.jl](https://github.com/fonsp/Pluto.jl), thanks Fons & Nicholas!_ diff --git a/packages/editor/public/_docs/README.md b/packages/editor/public/_docs/README.md index a89e1bbbe..6b345f39d 100644 --- a/packages/editor/public/_docs/README.md +++ b/packages/editor/public/_docs/README.md @@ -1,14 +1,26 @@ # Welcome to TypeCell -Hi there ๐Ÿ‘‹ ! Welcome to the community preview of TypeCell, an experimental _live notebook programming_ environment for the web. +Hi there ๐Ÿ‘‹ ! Welcome to the community preview of TypeCell, an open source platform for _live_, _interactive_ documents. + +A TypeCell document (like the one you're looking at!) is similar to a document in _Notion_ or _Google Docs_. Go ahead and try editing this page. + +However, TypeCell comes with a _Live Programming Environment_, that makes it possible to create your own blocks and build rich, interactive documents: + +```typescript +export default ( +
+ Hello from React! +
+); +``` This guide should help you to get started, and learn more about the ins & outs. ## Tutorial -Complete the tutorial to get familiar with TypeCell: +Complete the tutorial to get familiar with programming in TypeCell: -ยป [Interactive introduction](/docs/interactive-introduction.md) +- [TypeCell live coding tutorial](/docs/Live%20coding%20tutorial.md) ## Reference manual @@ -25,7 +37,7 @@ We've written about the main functionality of TypeCell in the [manual](/docs/man Another good way to learn is to check out some notebooks from our community: -ยป [View demo notebooks](/docs/demos.md) +- [View demo notebooks](/docs/Demos.md) # Feedback โค๏ธ diff --git a/packages/editor/public/_docs/demos.md b/packages/editor/public/_docs/demos.md index 19eb5977e..424029146 100644 --- a/packages/editor/public/_docs/demos.md +++ b/packages/editor/public/_docs/demos.md @@ -11,4 +11,4 @@ To showcase some of TypeCell's features, explore these demos from our community: ## Built something exciting? -Let us know [on discord](https://discord.gg/TcJ9TRC3SV) or [Matrix](https://matrix.to/#/#typecell-space:matrix.org) if you'd like to feature your own demo on this page! +Let us know [on discord](https://discord.gg/TcJ9TRC3SV) if you'd like to feature your own demo on this page! diff --git a/packages/editor/public/_docs/index.json b/packages/editor/public/_docs/index.json index cf80d999b..bb8cd0d30 100644 --- a/packages/editor/public/_docs/index.json +++ b/packages/editor/public/_docs/index.json @@ -1,14 +1,13 @@ { "title": "Docs", "items": [ - "demos.md", - "interactive-introduction.md", + "Demos.md", + "Live coding tutorial.md", "manual/1. Notebooks and cells.md", "manual/2. TypeScript and exports.md", "manual/3. Reactive variables.md", "manual/4. Inputs.md", "manual/5. Imports and NPM.md", - "manual/6. Collaboration.md", "README.md" ] } diff --git a/packages/editor/public/_docs/manual/1. Notebooks and cells.md b/packages/editor/public/_docs/manual/1. Notebooks and cells.md index 8f4bebddf..7d9048ad9 100644 --- a/packages/editor/public/_docs/manual/1. Notebooks and cells.md +++ b/packages/editor/public/_docs/manual/1. Notebooks and cells.md @@ -1,28 +1,20 @@ -# Notebooks and Cells +# Blocks and code blocks -The page you're looking at is called a _notebook_. -It's basically an interactive document that mixes _code_ and _text_ (documentation). +The page you're looking at is an interactive document that mixes _code_ and _text_. A document consists of different blocks, highlighted when you hover over them. TypeCell supports all kind of blocks, like _headings_, _paragraphs_, _lists_, or more advanced _code blocks_. -## Creating and reordering cells +## Creating and reordering Blocks -When you hover over a cell, click the `+` sign to insert a new cell above or below. +When you hover over a block, click the `+` sign to insert a new block. You can also type "/" anywhere to open the slash-menu that allows you to add a new block. -To reorder a cell, hover next to it (try it out on the left of this text), and simply drag and drop the cell. +To reorder a block, hover next to it (try it out on the left of this text), and simply drag and drop the block via the drag handle (โ‹ฎโ‹ฎ). -To view the source code of a cell, hover over the cell and click the caret Show / hide code on the top left. -You'll notice the text you're reading now is written in Markdown. +## Code blocks -## Cell types - -TypeCell currently supports a number of languages. You can view / change the language of a cell in the bottom-right of the cell's editor. - -### Markdown - -Useful for writing text / documentation. Markdown cells are collapsed by default (code cells are expanded by default). +TypeCell Code Blocks currently supports Typescript and CSS. You can view / change the language of a code block in the bottom-right of the code block editor. ### CSS -Use CSS to easily style the output of other cells (those written in Markdown or TypeScript). +Use CSS to easily style the output of other code blocks (those written in Markdown or TypeScript). ```css .redText { @@ -32,16 +24,18 @@ Use CSS to easily style the output of other cells (those written in Markdown or ```typescript export default ( -
This text is red, styled by the CSS cell above.
+
+ This text is red, styled by the CSS code block above. +
); -```` +``` ### TypeScript / JavaScript ```typescript -export let message = "This is a TypeScript cell"; +export let message = "This is a TypeScript code block"; ``` -TypeScript cells execute automatically as you type. Try editing the `message` above. +TypeScript code blocks execute automatically as you type. Try editing the `message` above. -You've learned the basics! Continue to learn more about writing code using TypeScript cells. +You've learned the basics! Continue to learn more about writing code using TypeScript code blocks. diff --git a/packages/editor/public/_docs/manual/2. TypeScript and exports.md b/packages/editor/public/_docs/manual/2. TypeScript and exports.md index 9de5f037b..9e493fb96 100644 --- a/packages/editor/public/_docs/manual/2. TypeScript and exports.md +++ b/packages/editor/public/_docs/manual/2. TypeScript and exports.md @@ -1,15 +1,17 @@ # TypeScript and exports -TypeScript cells are the main way to write code in TypeCell. +TypeScript code blocks are the main way to write code in TypeCell. You'll get all the benefits of the [Monaco Editor](https://microsoft.github.io/monaco-editor/) while writing code, the same editor that powers VS Code! -Note that TypeScript code still executes, even if there are type errors. +## Plain JavaScript + +Note that TypeScript code always executes, even if there are type errors. This allows you to quickly write and test code, but still get hints about possible bugs. -Note that this means that any JavaScript code works in TypeCell as well (i.e.: you don't need to type everything if you don't want to). +_This means that any **plain JavaScript** code works in TypeCell as well (you're not forced add types for everything)._ -In the example below, you'll notice that we get an error because we assign a `number` to a `string` variable, but the code still executes regardless. +In the example below, you'll notice that we get an error because we assign a "number" to a "string" variable, but the code still executes regardless. ```typescript export let message = "hello"; @@ -18,7 +20,7 @@ message = 4; ## Exports -You can export variables from your code, and they'll show up as _output_ of the cell. Above, we've exported a single `message` variable. +You can export variables from your code, and they'll show up as _output_ of the cell. Above, we've exported a single "message" variable. You can also export multiple variables from a cell, and the _inspector_ will help you to view the output: @@ -54,7 +56,7 @@ export let reactElement = ( ## The `default` export -You use a `default` export to indicate which variable should be displayed in the output. +You use a "default" export to indicate which variable should be displayed in the output. The following cell exports 2 variables, but only one is displayed in the output: @@ -64,8 +66,8 @@ export let myNum = 42; export default
The number is: {myNum}
; ``` -Now you might ask; what's the use of exporting `myNum` if you don't see it in the output? +Now you might ask; what's the use of exporting "myNum" if you don't see it in the output? -This is because exported variables can be reused across cells and notebooks; one of most powerful features of TypeCell! +This is because exported variables can be reused across blocks and documents; one of most powerful features of TypeCell! Continue to learn more about exported variables and Reactivity. diff --git a/packages/editor/public/_docs/manual/3. Reactive variables.md b/packages/editor/public/_docs/manual/3. Reactive variables.md index 3209a3184..02d8392fa 100644 --- a/packages/editor/public/_docs/manual/3. Reactive variables.md +++ b/packages/editor/public/_docs/manual/3. Reactive variables.md @@ -1,15 +1,15 @@ # Reactive variables -This is where things get interesting! Your code can reference variables exported by other cells. +This is where things get interesting! Your code can reference variables exported by other blocks. -Code cells in TypeCell (re)evaluate when: +Code blocks in TypeCell (re)evaluate when: - The code of the cell changes (i.e.: you're editing the code) -- A variable the cell depends upon updates +- A variable the block depends upon updates ## The `$` variable -Exports of cells are available under the `$` variable. Have a look at the example below, and change the `name` variable to your own name. Notice how the greeting in the cell below updates automatically. +Exports of cells are available under the "$" variable. Have a look at the example below, and change the "name" variable to your own name. Notice how the greeting in the cell below updates automatically. ```typescript export let name = "Anonymous coder"; @@ -23,8 +23,8 @@ export let greeting = ( ); ``` -Tip: type `$.` in a TypeScript cell, and the editor (Intellisense) will display a list of all exported variables you can reference. +_Tip: type "$." in a TypeScript cell, and the editor (Intellisense) will display a list of all exported variables you can reference._ ## Interactive Tutorial -The Reactive model of TypeCell is quite powerful. If you haven't already, follow the [interactive introduction](/docs/interactive-introduction.md) or have a look at the [demos](/docs/demos.md) to get some hands-on experience. +The Reactive model of TypeCell is quite powerful. If you haven't already, follow the [live coding tutorial](/docs/Live%20coding%20tutorial.md) or have a look at the [demos](/docs/Demos.md) to get some hands-on experience. diff --git a/packages/editor/public/_docs/manual/4. Inputs.md b/packages/editor/public/_docs/manual/4. Inputs.md index 733554215..8c86425f5 100644 --- a/packages/editor/public/_docs/manual/4. Inputs.md +++ b/packages/editor/public/_docs/manual/4. Inputs.md @@ -1,6 +1,6 @@ # Working with user inputs -In your notebook, you'll often want the viewer to be able to control input variables, without changing code. +In your interactive document, you'll often want the viewer to be able to control input variables, without changing code. Of course, you can create input elements using HTML / React. For example, like this: @@ -130,7 +130,7 @@ export default $.select; ### Numbers & Ranges -You can user *number* and *range* input types to allow the user to enter numbers. Make sure to explicitly pass `` to guide the type system that the edited variable is a number. +You can user *number* and *range* input types to allow the user to enter numbers. Make sure to explicitly pass "" to guide the type system that the edited variable is a number. ```typescript diff --git a/packages/editor/public/_docs/manual/5. Imports and NPM.md b/packages/editor/public/_docs/manual/5. Imports and NPM.md index 3be44edc8..1a6fdf709 100644 --- a/packages/editor/public/_docs/manual/5. Imports and NPM.md +++ b/packages/editor/public/_docs/manual/5. Imports and NPM.md @@ -4,8 +4,9 @@ TypeCell supports importing code from NPM, or from other TypeCell documents. ## Importing other notebooks -You can split your code into multiple notebooks. -This is a great way to create reusable components that you can use across notebooks, or even share with the community. +You can split your code into multiple documents. + +This is a great way to create reusable components that you can use across documents, or even share with the community. `import * as myNotebook from "!@username/notebook";` @@ -17,12 +18,12 @@ import * as myNotebook from "!@yousef/demo-message"; export default myNotebook.message; ``` -**TypeCell documents are designed to be as "live" as possible**: when you change the code of your imported notebook, -the notebook that imports the code will update live, as-you-type. +**๐Ÿš€ TypeCell documents are designed to be as "live" as possible**: when you change the code of your imported document, +the document that imports the code will update live, as-you-type. ## NPM -In TypeCell, you can also use any library from [NPM](https://www.npmjs.com/). Simply import the library in a TypeScript cell, and we'll try to resolve it (including TypeScript types) automatically. +In TypeCell, you can also use any library from [NPM](https://www.npmjs.com/). Simply import the library in a code block, and we'll try to resolve it (including TypeScript types) automatically. ### Example @@ -43,10 +44,10 @@ setInterval(() => myConfetti({ particleCount: 70, origin: { y: 0 } }), 500); ### Compatibility -Libraries are loaded in your browser via [Skypack](https://www.skypack.dev/). -Skypack support is best for modern libraries with ESM support, but not all NPM libraries are compatible. +Libraries are loaded in your browser via [ESM.sh](https://www.esm.sh/). +ESM.sh support is best for modern libraries with ESM support, but not all NPM libraries are compatible. -NPM imports are work in progress. Can't get a library to work? Let us know on [Discord](https://discord.gg/TcJ9TRC3SV) or [Matrix](https://matrix.to/#/#typecell-space:matrix.org)! +Can't get a library to work? Let us know on [Discord](https://discord.gg/TcJ9TRC3SV)! #### TypeScript diff --git a/packages/editor/public/_docs/manual/6. Collaboration.md b/packages/editor/public/_docs/manual/6. Collaboration.md deleted file mode 100644 index c83c6c4cb..000000000 --- a/packages/editor/public/_docs/manual/6. Collaboration.md +++ /dev/null @@ -1,3 +0,0 @@ -# Collaborating with others - -WIP \ No newline at end of file diff --git a/packages/editor/src/app/documentRenderers/DocumentView.tsx b/packages/editor/src/app/documentRenderers/DocumentView.tsx index 8725d427e..0beed5e36 100644 --- a/packages/editor/src/app/documentRenderers/DocumentView.tsx +++ b/packages/editor/src/app/documentRenderers/DocumentView.tsx @@ -50,30 +50,6 @@ const DocumentView = observer((props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.id.toString(), props.sessionStore]); - // if we're a fresh sign up, load changes made as guest. Not great to have this logic here - React.useEffect(() => { - const doc = connection?.tryDoc; - if (!doc) { - return; - } - - if ( - props.sessionStore.tryUser?.type === "user" && - props.sessionStore.tryUser.isSignUp && - (doc.type === "!richtext" || doc.type === "!notebook") - ) { - props.sessionStore.documentCoordinator?.loadFromGuest( - doc.identifier.toString(), - doc.ydoc - ); - } - }, [ - connection, - connection?.tryDoc, - props.sessionStore.documentCoordinator, - props.sessionStore.tryUser, - ]); - if (!connection) { return null; } diff --git a/packages/editor/src/app/documentRenderers/profile/ProfileRenderer.tsx b/packages/editor/src/app/documentRenderers/profile/ProfileRenderer.tsx index 5841239b7..a63e5a280 100644 --- a/packages/editor/src/app/documentRenderers/profile/ProfileRenderer.tsx +++ b/packages/editor/src/app/documentRenderers/profile/ProfileRenderer.tsx @@ -71,7 +71,7 @@ const ProfileRenderer: React.FC = observer((props) => {
Joined {joinedDate}
)} - {forkedDocs &&

Workspaces

} + {forkedDocs?.length &&

Workspaces

}
- {forkedDocs &&

Forked documents

} + {forkedDocs?.length &&

Forked documents

}
{forkedDocs.map((f) => ( <> diff --git a/packages/editor/src/app/main/components/documentMenu/DocumentMenu.tsx b/packages/editor/src/app/main/components/documentMenu/DocumentMenu.tsx index 902af4503..caef80ef3 100644 --- a/packages/editor/src/app/main/components/documentMenu/DocumentMenu.tsx +++ b/packages/editor/src/app/main/components/documentMenu/DocumentMenu.tsx @@ -8,7 +8,6 @@ import { VscKebabVertical } from "react-icons/vsc"; import { useLocation, useNavigate } from "react-router-dom"; import { Identifier } from "../../../../identifiers/Identifier"; import { MatrixIdentifier } from "../../../../identifiers/MatrixIdentifier"; -import { openAsMarkdown } from "../../../../integrations/markdown/export"; import { DocumentResource } from "../../../../store/DocumentResource"; import { SessionStore } from "../../../../store/local/SessionStore"; import { @@ -96,37 +95,42 @@ export const DocumentMenu: React.FC = observer((props) => { -
  • -
  • - ( -
    - -
    - )} - placement="bottom-end"> - {props.document instanceof DocumentResource && ( + {canEditPermissions && ( + <> +
  • +
  • + ( +
    + +
    + )} + placement="bottom-end"> + {/* {props.document instanceof DocumentResource && ( openAsMarkdown(props.document.doc)}> Export as markdown - )} - {canEditPermissions && ( - OpenPermissionsDialog(navigate)}> - Permissions - - )} -
    -
  • + )} */} + {canEditPermissions && ( + OpenPermissionsDialog(navigate)}> + Permissions + + )} + + + + )} {canEditPermissions && permissionsArea} diff --git a/packages/editor/src/app/main/components/startscreen/StartScreen.module.css b/packages/editor/src/app/main/components/startscreen/StartScreen.module.css index 78ebe7546..8e0a36ba6 100644 --- a/packages/editor/src/app/main/components/startscreen/StartScreen.module.css +++ b/packages/editor/src/app/main/components/startscreen/StartScreen.module.css @@ -75,7 +75,7 @@ .perks { font-size: 16px; - max-width: var(--content-max-width); + max-width: calc(var(--content-max-width) - 104px); margin: 0 auto; /* padding: 4em 0; */ /* position: relative; */ @@ -224,22 +224,30 @@ button { color: #5f5f5f; } +.footer, +.footer h4, +.footer a, +.footer a:hover { + color: #fff; +} .footer { text-align: left; - background-color: #f6f6f6; - position: relative; - padding: 1em 1em 0 1em; + background-color: #131519; } .footer > div { - height: 100%; + /* grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); */ + padding: 90px 40px; + max-width: calc(var(--vp-layout-max-width) + 16px); + /* display: grid; */ + display: flex; + gap: 100px; + margin: 0 auto; + justify-content: center; } .footer .links { - display: flex; - flex: 1 1; - width: 100%; - padding: 1em 0; + flex: auto; } .footer .links ul { @@ -247,24 +255,10 @@ button { padding: 0; } -.links ul a { - color: #a4a4a4; -} - .links ul a:hover { text-decoration: none; } -.links ul a span { - text-decoration: underline; -} - -.links ul a:before { - content: "โ€บ "; - display: inline; - text-decoration: none; -} - .footer .bottom { font-size: 0.9em; bottom: 0; @@ -334,6 +328,7 @@ button { } :root { + --vp-layout-max-width: 1440px; --content-max-width: 1480px; } @@ -531,7 +526,8 @@ button { font-weight: 700; color: var(--vp-c-text-1); --border-thickness: 2px; - box-shadow: inset 0 1px 0 0 rgba(0, 0, 0, 0.1), + box-shadow: + inset 0 1px 0 0 rgba(0, 0, 0, 0.1), inset 0 0 0 var(--border-thickness) var(--vp-c-text-1); backdrop-filter: blur(4px); transition: 0.16s ease; @@ -542,7 +538,8 @@ button { .ctaButtons a:hover { --border-thickness: 1px; background-color: #fff; - box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.1), + box-shadow: + inset 0 1px 0 0 rgba(255, 255, 255, 0.1), inset 0 0 0 var(--border-thickness) var(--vp-c-brand-lighter), 0 0 24px -10px var(--vp-c-brand-lighter); transform: translateY(-2px); diff --git a/packages/editor/src/app/main/components/startscreen/StartScreen.tsx b/packages/editor/src/app/main/components/startscreen/StartScreen.tsx index 8410c8c48..317f42583 100644 --- a/packages/editor/src/app/main/components/startscreen/StartScreen.tsx +++ b/packages/editor/src/app/main/components/startscreen/StartScreen.tsx @@ -250,28 +250,26 @@ export const StartScreen = observer((props: { sessionStore: SessionStore }) => {
    ); diff --git a/packages/editor/src/app/routes/ownerAlias.tsx b/packages/editor/src/app/routes/ownerAlias.tsx index f13017509..1c3072ea2 100644 --- a/packages/editor/src/app/routes/ownerAlias.tsx +++ b/packages/editor/src/app/routes/ownerAlias.tsx @@ -80,19 +80,19 @@ export const OwnerAliasRoute = observer( .select() .eq("name", owner) .eq("is_username", true) - .single(); + .limit(1); if (error) { setAliasResolveStatus("error"); return; } - if (!data) { + if (!data || !data.length) { setAliasResolveStatus("not-found"); return; } - const nanoId = data.document_nano_id; + const nanoId = data[0].document_nano_id; const id = new TypeCellIdentifier( uri.URI.from({ scheme: "typecell", // TODO diff --git a/packages/editor/src/app/supabase-auth/SupabaseSessionStore.ts b/packages/editor/src/app/supabase-auth/SupabaseSessionStore.ts index 6f2f631ef..44be39bc8 100644 --- a/packages/editor/src/app/supabase-auth/SupabaseSessionStore.ts +++ b/packages/editor/src/app/supabase-auth/SupabaseSessionStore.ts @@ -159,9 +159,9 @@ export class SupabaseSessionStore extends SessionStore { .select() .eq("name", username) .eq("is_username", true) - .single(); + .limit(1); - if (data) { + if (data?.length) { return "not-available"; } } diff --git a/packages/editor/src/store/BackgroundSyncer.browsertest.ts b/packages/editor/src/store/BackgroundSyncer.browsertest.ts index 3ed7390db..a5fb62cc3 100644 --- a/packages/editor/src/store/BackgroundSyncer.browsertest.ts +++ b/packages/editor/src/store/BackgroundSyncer.browsertest.ts @@ -1,10 +1,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-expressions */ -import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; import { enableMobxBindings } from "@syncedstore/yjs-reactive-bindings"; -import { createWsProvider } from "@typecell-org/shared-test"; import { expect } from "chai"; import * as mobx from "mobx"; import { when } from "mobx"; @@ -12,6 +10,7 @@ import { async } from "vscode-lib"; import { loginAsNewRandomUser } from "../../tests/util/loginUtil"; import { SupabaseSessionStore } from "../app/supabase-auth/SupabaseSessionStore"; import { DocConnection } from "./DocConnection"; +import { DocumentInfo } from "./yjs-sync/DocumentCoordinator"; import { TypeCellRemote } from "./yjs-sync/remote/TypeCellRemote"; async function initSessionStore(name: string) { @@ -30,14 +29,12 @@ async function initSessionStore(name: string) { describe("BackgroundSyncer tests", () => { let sessionStoreAlice: SupabaseSessionStore; let sessionStoreBob: SupabaseSessionStore; - let wsProvider: HocuspocusProviderWebsocket; before(async () => { enableMobxBindings(mobx); }); beforeEach(async () => { - wsProvider = createWsProvider("ws://localhost:1234"); // initialize the main user we're testing // await coordinator.initialize(); @@ -53,7 +50,6 @@ describe("BackgroundSyncer tests", () => { await sessionStoreBob.supabase.auth.signOut(); sessionStoreBob.dispose(); sessionStoreBob = undefined as any; - wsProvider.destroy(); }); /** @@ -71,6 +67,7 @@ describe("BackgroundSyncer tests", () => { const reload = await DocConnection.load(doc.identifier, sessionStoreAlice); const reloadedDoc = await reload.waitForDoc(); expect(reloadedDoc.ydoc.getMap("test").get("hello")).to.eq("world"); + reload.dispose(); }); it("creates document remotely that was created offline earlier", async () => { @@ -87,13 +84,13 @@ describe("BackgroundSyncer tests", () => { ).to.eq(2); [...sessionStoreAlice.documentCoordinator!.documents.values()].forEach( - (doc) => { + (doc: DocumentInfo) => { expect(doc.exists_at_remote).to.be.false; } ); TypeCellRemote.Offline = false; - + await when( () => sessionStoreAlice.coordinators?.backgroundSyncer @@ -101,8 +98,9 @@ describe("BackgroundSyncer tests", () => { ); [...sessionStoreAlice.documentCoordinator!.documents.values()].forEach( (doc) => { + expect(doc.exists_at_remote).to.be.true; } ); - }); + }).timeout(10000); }); diff --git a/packages/editor/src/store/BackgroundSyncer.ts b/packages/editor/src/store/BackgroundSyncer.ts index bbffab946..1686ac90b 100644 --- a/packages/editor/src/store/BackgroundSyncer.ts +++ b/packages/editor/src/store/BackgroundSyncer.ts @@ -59,7 +59,6 @@ export class BackgroundSyncer extends lifecycle.Disposable { for (const id of this.identifiersToSync) { if (!ids.includes(id)) { // cleanup - console.log("bg syncer unload", id); this.identifiersToSync.delete(id); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const connection = this.loadedConnections.get(id)!; diff --git a/packages/editor/src/store/local/SessionStore.ts b/packages/editor/src/store/local/SessionStore.ts index 57d250b01..ad9e7bda2 100644 --- a/packages/editor/src/store/local/SessionStore.ts +++ b/packages/editor/src/store/local/SessionStore.ts @@ -3,7 +3,7 @@ import { makeObservable, observable, reaction, - runInAction, + runInAction } from "mobx"; import { lifecycle } from "vscode-lib"; import { Identifier } from "../../identifiers/Identifier"; @@ -18,6 +18,7 @@ import { DocumentCoordinator } from "../yjs-sync/DocumentCoordinator"; * (e.g.: is the user logged in, what is the user name, etc) */ export abstract class SessionStore extends lifecycle.Disposable { + public disposed = false; public profileDoc: DocConnection | undefined = undefined; public get profile() { return this.profileDoc?.tryDoc?.getSpecificType(ProfileResource); @@ -106,29 +107,44 @@ export abstract class SessionStore extends lifecycle.Disposable { return; } + + (async () => { + // await when(() => this.initializingCoordinators === false); + if (this.disposed) { + throw new Error("already disposed"); + } + const user = this.user; const coordinator = new DocumentCoordinator(userPrefix); const coordinators = { userPrefix, coordinator: coordinator, aliasStore: new AliasCoordinator(userPrefix), backgroundSyncer: - typeof this.user !== "string" && this.user.type !== "guest-user" + typeof user !== "string" && user.type !== "guest-user" ? new BackgroundSyncer(coordinator, this) : undefined, }; await coordinators.coordinator.initialize(); + + if (typeof user !== "string" && user.type === "user" && user.isSignUp) { + await coordinators.coordinator.copyFromGuest(); + } + await coordinators.aliasStore.initialize(); await coordinators.backgroundSyncer?.initialize(); runInAction(() => { - if (this.userPrefix === userPrefix) { - // console.log("set coordinators", userPrefix); + if (this.userPrefix === userPrefix && !this.disposed) { this.coordinators = coordinators; - if (typeof this.user !== "string" && this.user.type === "user") { + if (typeof user !== "string" && user.type === "user") { this.profileDoc = this.loadProfile - ? DocConnection.load(this.user.profileId, this) + ? DocConnection.load(user.profileId, this) : undefined; } + } else { + coordinators.aliasStore.dispose(); + coordinators.coordinator.dispose(); + coordinators.backgroundSyncer?.dispose(); } }); })(); @@ -139,6 +155,16 @@ export abstract class SessionStore extends lifecycle.Disposable { this._register({ dispose, }); + + this._register({ + dispose: () => { + this.disposed = true; + this.coordinators?.coordinator.dispose(); + this.coordinators?.aliasStore.dispose(); + this.coordinators?.backgroundSyncer?.dispose(); + this.profileDoc?.dispose(); + }, + }); } public get userPrefix() { diff --git a/packages/editor/src/store/yjs-sync/DocumentCoordinator.ts b/packages/editor/src/store/yjs-sync/DocumentCoordinator.ts index 02980e8ef..ea80e89a7 100644 --- a/packages/editor/src/store/yjs-sync/DocumentCoordinator.ts +++ b/packages/editor/src/store/yjs-sync/DocumentCoordinator.ts @@ -18,6 +18,35 @@ export type DocumentInfo = { needs_save_since: Date | undefined; }; +function COORDINATOR_IDB_ID(userId: string) { + return userId + "-coordinator"; +} + +function DOC_IDB_ID(userId: string, docId: string) { + return userId + "-doc-" + docId; +} + +async function awaitSynced(provider: IndexeddbPersistence) { + return await new Promise((resolve) => { + provider.once("synced", () => { + resolve(); + }); + }); +} + +// https://blog.testdouble.com/posts/2019-05-14-locking-with-promises/ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const lockify = (f: any) => { + let lock = Promise.resolve(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...params: any) => { + const result = lock.then(() => f(...params)); + lock = result.catch(() => { + //noop + }); + return result; + }; +}; export class DocumentCoordinator extends lifecycle.Disposable { private loadedDocuments = new Map(); @@ -29,17 +58,65 @@ export class DocumentCoordinator extends lifecycle.Disposable { this.doc = new Y.Doc(); makeYDocObservable(this.doc); this.indexedDBProvider = new IndexeddbPersistence( - userId + "-coordinator", + COORDINATOR_IDB_ID(userId), this.doc ); this._register({ dispose: () => { + this.indexedDBProvider.destroy(); this.doc.destroy(); }, }); } + public copyFromGuest = lockify(this.copyFromGuestNoLock.bind(this)); + + private async copyFromGuestNoLock() { + if (this.documents.size > 0) { + throw new Error("copyFromGuest: target already has documents!"); + } + const guestCoordinatorID = COORDINATOR_IDB_ID("user-tc-guest"); + if (!(await databaseExists(guestCoordinatorID))) { + return false; + } + + // copy coordinator + const guestIDB = new IndexeddbPersistence(guestCoordinatorID, this.doc); + + await guestIDB.whenSynced; + + // copy docs + const docs = this.documents.values(); + for (const doc of docs) { + const typedDoc = doc as DocumentInfo; + + const ydoc = new Y.Doc(); + + const guestDocIDB = new IndexeddbPersistence( + DOC_IDB_ID("user-tc-guest", typedDoc.id), + ydoc + ); + await awaitSynced(guestDocIDB); + + if (typedDoc.needs_save_since) { + const targetDocIDB = new IndexeddbPersistence( + DOC_IDB_ID(this.userId, typedDoc.id), + ydoc + ); + await awaitSynced(targetDocIDB); + ydoc.destroy(); + targetDocIDB.destroy(); + } else { + ydoc.destroy(); + } + guestDocIDB.destroy(); + await guestDocIDB.clearData(); + } + guestIDB.destroy(); // needed because theoretically a ydoc transaction can happen while the indexeddb is being destroyed + await guestIDB.clearData(); + } + public async initialize() { await this.indexedDBProvider.whenSynced; } @@ -77,7 +154,7 @@ export class DocumentCoordinator extends lifecycle.Disposable { exists_at_remote: false, }; - this.documents.set(idStr, meta); + this.documents.set(idStr, { ...meta }); const ret = this.loadDocument(identifier, targetYDoc); if (ret === "not-found") { @@ -110,7 +187,7 @@ export class DocumentCoordinator extends lifecycle.Disposable { exists_at_remote: true, }; - this.documents.set(idStr, meta); + this.documents.set(idStr, { ...meta }); const ret = this.loadDocument(identifier, targetYDoc); if (ret === "not-found") { @@ -143,7 +220,7 @@ export class DocumentCoordinator extends lifecycle.Disposable { return "changed"; } - const doc = await this.loadDocument(identifier, new Y.Doc()); + const doc = this.loadDocument(identifier, new Y.Doc()); if (doc === "not-found") { throw new Error("unexpected: doc not found"); } @@ -191,12 +268,12 @@ export class DocumentCoordinator extends lifecycle.Disposable { if (meta.needs_save_since === undefined) { meta.needs_save_since = new Date(); - this.documents.set(idStr, meta); + this.documents.set(idStr, { ...meta }); } }); const idbProvider = new IndexeddbPersistence( - this.userId + "-doc-" + idStr, + DOC_IDB_ID(this.userId, idStr), targetYDoc ); @@ -238,36 +315,12 @@ export class DocumentCoordinator extends lifecycle.Disposable { public async markSynced(localDoc: LocalDoc) { localDoc.meta.needs_save_since = undefined; - this.documents.set(localDoc.meta.id, localDoc.meta); + this.documents.set(localDoc.meta.id, { ...localDoc.meta }); } public async markCreated(localDoc: LocalDoc) { localDoc.meta.exists_at_remote = true; - this.documents.set(localDoc.meta.id, localDoc.meta); - } - - public async loadFromGuest(identifier: string, targetYdoc: Y.Doc) { - const dbname = "user-tc-guest-doc-" + identifier; // bit hacky, "officially" we don't know the exact source name prefix here - const dbExists = await databaseExists(dbname); - - if (dbExists) { - const guestIndexedDBProvider = new IndexeddbPersistence( - dbname, - targetYdoc - ); - // wait for sync - await new Promise((resolve) => { - guestIndexedDBProvider.once("synced", () => { - resolve(); - }); - }); - guestIndexedDBProvider.destroy(); - console.log("applied changes from guest"); - return true; - } else { - console.log("did not apply changes from guest"); - return false; - } + this.documents.set(localDoc.meta.id, { ...localDoc.meta }); } } diff --git a/packages/editor/src/store/yjs-sync/SyncManager.browsertest.ts b/packages/editor/src/store/yjs-sync/SyncManager.browsertest.ts index 00f97f30e..0289ab07e 100644 --- a/packages/editor/src/store/yjs-sync/SyncManager.browsertest.ts +++ b/packages/editor/src/store/yjs-sync/SyncManager.browsertest.ts @@ -142,6 +142,7 @@ describe("SyncManager tests", () => { ); expect(manager.state.localDoc.meta.create_source).eq("remote"); + manager.dispose(); }); it("cannot load an unknown remote document offline", async () => { @@ -171,6 +172,7 @@ describe("SyncManager tests", () => { ); expect(loadedManager.state.localDoc.meta.create_source).eq("remote"); + manager.dispose(); }); it("can load a known remote document", async () => { diff --git a/packages/editor/src/store/yjs-sync/SyncManager.ts b/packages/editor/src/store/yjs-sync/SyncManager.ts index 288510e97..b848d308d 100644 --- a/packages/editor/src/store/yjs-sync/SyncManager.ts +++ b/packages/editor/src/store/yjs-sync/SyncManager.ts @@ -48,8 +48,8 @@ export class SyncManager extends lifecycle.Disposable { const remoteStatus = this.remote.status; if (this.state.status === "loading") { if (remoteStatus === "loaded") { - // throw new Error("not possible"); // TODO: is this safe? - console.error( + // TODO: fix / diagnose when this occurs + console.warn( "should not be possible, doc status 'loading', but remote 'loaded'" ); return "loading"; @@ -254,6 +254,8 @@ export class SyncManager extends lifecycle.Disposable { throw new Error("logged out while loading"); } + // hacky fix for docs / httpidentifier, so that if there are no changes we fetch the latest state from the server + // (works with the rest of this workaround below) if (this.identifier instanceof HttpsIdentifier) { await this.sessionStore.documentCoordinator.clearIfNotChanged( this.identifier @@ -281,7 +283,8 @@ export class SyncManager extends lifecycle.Disposable { }; }); - // hacky fix for docs / httpidentifier + // hacky fix for docs / httpidentifier, if we have a local copy of an https document, we don't want to sync + // (because FetchRemote would return a new document that's different / unsyncable with the local copy) if (!(this.identifier instanceof HttpsIdentifier)) { return this.startSyncing(); } diff --git a/packages/editor/src/store/yjs-sync/remote/TypeCellRemote.ts b/packages/editor/src/store/yjs-sync/remote/TypeCellRemote.ts index ee1085c31..55160b71e 100644 --- a/packages/editor/src/store/yjs-sync/remote/TypeCellRemote.ts +++ b/packages/editor/src/store/yjs-sync/remote/TypeCellRemote.ts @@ -59,6 +59,7 @@ export class TypeCellRemote extends Remote { } public static set Offline(val: boolean) { + console.log("change fake offline mode", val) if (val) { wsProviders.forEach((wsProvider) => { wsProvider?.disconnect(); diff --git a/packages/editor/src/styles/github-markdown.css b/packages/editor/src/styles/github-markdown.css index c30c1a0b9..efd84003e 100644 --- a/packages/editor/src/styles/github-markdown.css +++ b/packages/editor/src/styles/github-markdown.css @@ -1012,4 +1012,8 @@ Typecell / BlockNote } .markdown-body p { display: inline; + } + + .markdown-body a { + cursor:pointer; } \ No newline at end of file