Skip to content

Commit

Permalink
Collaborative editing (outline#1660)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommoor authored Sep 11, 2021
1 parent 0a99878 commit 801f668
Show file tree
Hide file tree
Showing 144 changed files with 3,551 additions and 309 deletions.
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ REDIS_URL=redis://localhost:6479
URL=http://localhost:3000
PORT=3000

# ALPHA – See [documentation](docs/SERVICES.md) on running the alpha version of
# the collaboration server.
COLLABORATION_URL=

# To support uploading of images for avatars and document attachments an
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
# however if you want to keep all file storage local an alternative such as
Expand Down
5 changes: 5 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
.*/node_modules/react-side-effect/.*
.*/node_modules/fbjs/.*
.*/node_modules/config-chain/.*
.*/node_modules/yjs/.*
.*/node_modules/y-prosemirror/.*
.*/node_modules/y-protocols/.*
.*/node_modules/y-indexeddb/.*
.*/node_modules/lib0/.*
.*/server/scripts/.*
*.test.js

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ up:
docker-compose up -d redis postgres s3
yarn install --pure-lockfile
yarn sequelize db:migrate
yarn dev
yarn dev:watch

build:
docker-compose build --pull outline
Expand Down
4 changes: 2 additions & 2 deletions Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
web: node ./build/server/index.js --services=web,websockets
worker: node ./build/server/index.js --services=worker
web: yarn start --services=web,websockets
worker: yarn start --services=worker
47 changes: 22 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


<p align="center">
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
</p>
Expand Down Expand Up @@ -30,7 +28,6 @@ Outline requires the following dependencies:
- AWS S3 bucket or compatible API for file storage
- Slack or Google developer application for authentication


## Self-Hosted Production

### Docker
Expand All @@ -41,16 +38,20 @@ For a manual self-hosted production installation these are the recommended steps
1. Download the latest official Docker image, new releases are available around the middle of every month:

`docker pull outlinewiki/outline`

1. Using the [.env.sample](.env.sample) as a reference, set the required variables in your production environment. You can export the environment variables directly, or create a `.env` file and pass it to the docker image like so:

`docker run --env-file=.env outlinewiki/outline`

1. Setup the database with `yarn db:migrate`. Production assumes an SSL connection to the database by default, if
Postgres is on the same machine and is not SSL you can migrate with `yarn db:migrate --env=production-ssl-disabled`, for example:
Postgres is on the same machine and is not SSL you can migrate with `yarn db:migrate --env=production-ssl-disabled`, for example:

`docker run --rm outlinewiki/outline yarn db:migrate`

1. Start the container:

`docker run outlinewiki/outline`

1. Visit http://you_server_ip:3000 and you should be able to see Outline page

> Port number can be changed using the `PORT` environment variable
Expand Down Expand Up @@ -79,29 +80,27 @@ If you're running Outline by cloning this repository, run the following command
yarn run upgrade
```


## Local Development

For contributing features and fixes you can quickly get an environment running using Docker by following these steps:

1. Install these dependencies if you don't already have them
1. [Docker for Desktop](https://www.docker.com)
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
1. [Yarn](https://yarnpkg.com)
1. [Docker for Desktop](https://www.docker.com)
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
1. [Yarn](https://yarnpkg.com)
1. Clone this repo
1. Register a Slack app at https://api.slack.com/apps
1. Copy the file `.env.sample` to `.env`
1. Fill out the following fields:
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. Configure your Slack app's Oauth & Permissions settings
1. Slack recently prevented the use of `http` protocol for localhost. For local development, you can use a tool like [ngrok](https://ngrok.com) or a package like `mkcert`. ([How to use HTTPS for local development](https://web.dev/how-to-use-local-https/))
1. Add `https://my_ngrok_address/auth/slack.callback` as an Oauth redirect URL
1. Ensure that the bot token scope contains at least `users:read`
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. Configure your Slack app's Oauth & Permissions settings
1. Slack recently prevented the use of `http` protocol for localhost. For local development, you can use a tool like [ngrok](https://ngrok.com) or a package like `mkcert`. ([How to use HTTPS for local development](https://web.dev/how-to-use-local-https/))
1. Add `https://my_ngrok_address/auth/slack.callback` as an Oauth redirect URL
1. Ensure that the bot token scope contains at least `users:read`
1. Run `make up`. This will download dependencies, build and launch a development version of Outline


# Contributing

Outline is built and maintained by a small team – we'd love your help to fix bugs and add features!
Expand All @@ -110,18 +109,16 @@ Before submitting a pull request please let the core team know by creating or co

If you’re looking for ways to get started, here's a list of ways to help us improve Outline:

* [Translation](TRANSLATION.md) into other languages
* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
* Performance improvements, both on server and frontend
* Developer happiness and documentation
* Bugs and other issues listed on GitHub

- [Translation](docs/TRANSLATION.md) into other languages
- Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
- Performance improvements, both on server and frontend
- Developer happiness and documentation
- Bugs and other issues listed on GitHub

## Architecture

If you're interested in contributing or learning more about the Outline codebase
please refer to the [architecture document](ARCHITECTURE.md) first for a high level overview of how the application is put together.

please refer to the [architecture document](docs/ARCHITECTURE.md) first for a high level overview of how the application is put together.

## Debugging

Expand All @@ -145,7 +142,7 @@ make test
make watch
```

Once the test database is created with `make test` you may individually run
Once the test database is created with `make test` you may individually run
frontend and backend tests directly.

```shell
Expand Down
59 changes: 59 additions & 0 deletions app/components/ConnectionStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @flow
import { observer } from "mobx-react";
import { DisconnectedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade";
import NudeButton from "components/NudeButton";
import Tooltip from "components/Tooltip";
import useStores from "hooks/useStores";

function ConnectionStatus() {
const { ui } = useStores();
const theme = useTheme();
const { t } = useTranslation();

return ui.multiplayerStatus === "connecting" ||
ui.multiplayerStatus === "disconnected" ? (
<Tooltip
tooltip={
<Centered>
<strong>{t("Server connection lost")}</strong>
<br />
{t("Edits you make will sync once you’re online")}
</Centered>
}
placement="bottom"
>
<Button>
<Fade>
<DisconnectedIcon color={theme.sidebarText} />
</Fade>
</Button>
</Tooltip>
) : null;
}

const Button = styled(NudeButton)`
display: none;
position: fixed;
bottom: 0;
right: 32px;
margin: 24px;
${breakpoint("tablet")`
display: block;
`};
@media print {
display: none;
}
`;

const Centered = styled.div`
text-align: center;
`;

export default observer(ConnectionStatus);
49 changes: 48 additions & 1 deletion app/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { lighten } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import { Extension } from "rich-markdown-editor";
import styled, { withTheme } from "styled-components";
import embeds from "shared/embeds";
import { light } from "shared/theme";
import UiStore from "stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import useMediaQuery from "hooks/useMediaQuery";
import useToasts from "hooks/useToasts";
import { type Theme } from "types";
Expand All @@ -30,6 +31,8 @@ export type Props = {|
grow?: boolean,
disableEmbeds?: boolean,
ui?: UiStore,
style?: Object,
extensions?: Extension[],
shareId?: ?string,
autoFocus?: boolean,
template?: boolean,
Expand Down Expand Up @@ -246,6 +249,50 @@ const StyledEditor = styled(RichMarkdownEditor)`
}
}
}
.ProseMirror {
.ProseMirror-yjs-cursor {
position: relative;
margin-left: -1px;
margin-right: -1px;
border-left: 1px solid black;
border-right: 1px solid black;
height: 1em;
word-break: normal;
&:after {
content: "";
display: block;
position: absolute;
left: -8px;
right: -8px;
top: 0;
bottom: 0;
}
> div {
opacity: 0;
position: absolute;
top: -1.8em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-style: normal;
line-height: normal;
user-select: none;
white-space: nowrap;
color: white;
padding: 2px 6px;
font-weight: 500;
border-radius: 4px;
pointer-events: none;
left: -1px;
}
&:hover {
> div {
opacity: 1;
transition: opacity 100ms ease-in-out;
}
}
}
}
`;

const EditorTooltip = ({ children, ...props }) => (
Expand Down
4 changes: 2 additions & 2 deletions app/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) {
}, []);

return (
<Wrapper align="center" isCompact={isScrolled} shrink={false}>
<Wrapper align="center" shrink={false}>
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null}
{isScrolled ? (
<Title align="center" justify="flex-start" onClick={handleClickTitle}>
Expand Down Expand Up @@ -95,7 +95,7 @@ const Wrapper = styled(Flex)`
}
${breakpoint("tablet")`
padding: ${(props) => (props.isCompact ? "12px" : `24px 24px 0`)};
padding: 16px 16px 0;
justify-content: "center";
`};
`;
Expand Down
2 changes: 1 addition & 1 deletion app/components/PageTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import { cdnPath } from "../../shared/utils/urls";
import useStores from "hooks/useStores";
import { cdnPath } from "utils/urls";

type Props = {|
title: string,
Expand Down
47 changes: 37 additions & 10 deletions app/components/PlaceholderDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,45 @@ import Fade from "components/Fade";
import Flex from "components/Flex";
import PlaceholderText from "components/PlaceholderText";

export default function PlaceholderDocument(props: Object) {
export default function PlaceholderDocument({
includeTitle,
delay,
}: {
includeTitle?: boolean,
delay?: number,
}) {
const content = (
<>
<PlaceholderText delay={0.2} />
<PlaceholderText delay={0.4} />
<PlaceholderText delay={0.6} />
</>
);

if (includeTitle === false) {
return (
<DelayedMount delay={delay}>
<Fade>
<Flex column auto>
{content}
</Flex>
</Fade>
</DelayedMount>
);
}

return (
<DelayedMount>
<DelayedMount delay={delay}>
<Wrapper>
<Flex column auto {...props}>
<PlaceholderText height={34} maxWidth={70} />
<PlaceholderText delay={0.2} maxWidth={40} />
<br />
<PlaceholderText delay={0.2} />
<PlaceholderText delay={0.4} />
<PlaceholderText delay={0.6} />
</Flex>
<Fade>
<Flex column auto>
<PlaceholderText height={34} maxWidth={70} />
<PlaceholderText delay={0.2} maxWidth={40} />
<br />

{content}
</Flex>
</Fade>
</Wrapper>
</DelayedMount>
);
Expand Down
3 changes: 2 additions & 1 deletion app/components/Sidebar/components/TeamButton.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import { observer } from "mobx-react";
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
Expand Down Expand Up @@ -84,4 +85,4 @@ const Header = styled.button`
}
`;

export default TeamButton;
export default observer(TeamButton);
9 changes: 9 additions & 0 deletions app/hooks/useCurrentToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow
import invariant from "invariant";
import useStores from "./useStores";

export default function useCurrentToken() {
const { auth } = useStores();
invariant(auth.token, "token is required");
return auth.token;
}
Loading

0 comments on commit 801f668

Please sign in to comment.