Skip to content

Commit

Permalink
Merge branch 'outline:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
pesaventofilippo authored Oct 18, 2024
2 parents 5f1bfd1 + 9e37889 commit 4dbd861
Show file tree
Hide file tree
Showing 32 changed files with 411 additions and 436 deletions.
7 changes: 7 additions & 0 deletions app/components/WebsocketProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,13 @@ class WebsocketProvider extends React.Component<Props> {
});

this.socket.on("comments.update", (event: PartialExcept<Comment, "id">) => {
const comment = comments.get(event.id);

// Existing policy becomes invalid when the resolution status has changed and we don't have the latest version.
if (comment?.resolvedAt !== event.resolvedAt) {
policies.remove(event.id);
}

comments.add(event);
});

Expand Down
65 changes: 62 additions & 3 deletions app/editor/extensions/Multiplayer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import isEqual from "lodash/isEqual";
import { keymap } from "prosemirror-keymap";
import {
ySyncPlugin,
yCursorPlugin,
yUndoPlugin,
undo,
redo,
} from "@getoutline/y-prosemirror";
import { keymap } from "prosemirror-keymap";
} from "y-prosemirror";
import * as Y from "yjs";
import Extension from "@shared/editor/lib/Extension";
import { Second } from "@shared/utils/time";

type UserAwareness = {
user: {
id: string;
};
anchor: object;
head: object;
};

export default class Multiplayer extends Extension {
get name() {
Expand All @@ -18,6 +28,7 @@ export default class Multiplayer extends Extension {
const { user, provider, document: doc } = this.options;
const type = doc.get("default", Y.XmlFragment);

// Assign a user to a client ID once they've made a change and then remove the listener
const assignUser = (tr: Y.Transaction) => {
const clientIds = Array.from(doc.store.clients.keys());

Expand All @@ -32,6 +43,51 @@ export default class Multiplayer extends Extension {
}
};

const userAwarenessCache = new Map<
string,
{ aw: UserAwareness; changedAt: Date }
>();

// The opacity of a remote user's selection.
const selectionOpacity = 70;

// The time in milliseconds after which a remote user's selection will be hidden.
const selectionTimeout = 10 * Second.ms;

// We're hijacking this method to store the last time a user's awareness changed as a side
// effect, and otherwise behaving as the default.
const awarenessStateFilter = (
currentClientId: number,
userClientId: number,
aw: UserAwareness
) => {
if (currentClientId === userClientId) {
return false;
}

const cached = userAwarenessCache.get(aw.user.id);
if (!cached || !isEqual(cached?.aw, aw)) {
userAwarenessCache.set(aw.user.id, { aw, changedAt: new Date() });
}

return true;
};

// Override the default selection builder to add a background color to the selection
// only if the user's awareness has changed recently – this stops selections from lingering.
const selectionBuilder = (u: { id: string; color: string }) => {
const cached = userAwarenessCache.get(u.id);
const opacity =
!cached || cached?.changedAt > new Date(Date.now() - selectionTimeout)
? selectionOpacity
: 0;

return {
style: `background-color: ${u.color}${opacity}`,
class: "ProseMirror-yjs-selection",
};
};

provider.setAwarenessField("user", user);

// only once an actual change has been made do we add the userId <> clientId
Expand All @@ -40,7 +96,10 @@ export default class Multiplayer extends Extension {

return [
ySyncPlugin(type),
yCursorPlugin(provider.awareness),
yCursorPlugin(provider.awareness, {
awarenessStateFilter,
selectionBuilder,
}),
yUndoPlugin(),
keymap({
"Mod-z": undo,
Expand Down
2 changes: 1 addition & 1 deletion app/menus/CommentMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function CommentMenu({
title: `${t("Edit")}…`,
icon: <EditIcon />,
onClick: onEdit,
visible: can.update,
visible: can.update && !comment.isResolved,
},
actionToMenuItem(
resolveCommentFactory({
Expand Down
5 changes: 4 additions & 1 deletion app/models/ApiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ class ApiKey extends ParanoidModel {
@observable
expiresAt?: string;

/** An optional datetime that the API key was last used at. */
/** Timestamp that the API key was last used. */
@observable
lastActiveAt?: string;

/** The user ID that the API key belongs to. */
userId: string;

/** The plain text value of the API key, only available on creation. */
value: string;

Expand Down
4 changes: 2 additions & 2 deletions app/models/Comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ class Comment extends Model {
* Whether the comment is resolved
*/
@computed
public get isResolved() {
return !!this.resolvedAt;
public get isResolved(): boolean {
return !!this.resolvedAt || !!this.parentComment?.isResolved;
}

/**
Expand Down
8 changes: 5 additions & 3 deletions app/scenes/Document/components/CommentThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ function CommentThread({
});
const can = usePolicy(document);

const canReply = can.comment && !thread.isResolved;

const highlightedCommentMarks = editor
?.getComments()
.filter((comment) => comment.id === thread.id);
Expand All @@ -105,7 +107,7 @@ function CommentThread({
const handleClickThread = () => {
history.replace({
// Clear any commentId from the URL when explicitly focusing a thread
search: "",
search: thread.isResolved ? "resolved=" : "",
pathname: location.pathname.replace(/\/history$/, ""),
state: { commentId: thread.id },
});
Expand Down Expand Up @@ -214,7 +216,7 @@ function CommentThread({
))}

<ResizingHeightContainer hideOverflow={false} ref={replyRef}>
{(focused || draft || commentsInThread.length === 0) && can.comment && (
{(focused || draft || commentsInThread.length === 0) && canReply && (
<Fade timing={100}>
<CommentForm
onSaveDraft={onSaveDraft}
Expand All @@ -232,7 +234,7 @@ function CommentThread({
</Fade>
)}
</ResizingHeightContainer>
{!focused && !recessed && !draft && can.comment && (
{!focused && !recessed && !draft && canReply && (
<Reply onClick={() => setAutoFocus(true)}>{t("Reply")}</Reply>
)}
</Thread>
Expand Down
5 changes: 4 additions & 1 deletion app/scenes/Settings/ApiKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import Text from "~/components/Text";
import { createApiKey } from "~/actions/definitions/apiKeys";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import ApiKeyListItem from "./components/ApiKeyListItem";

function ApiKeys() {
const team = useCurrentTeam();
const user = useCurrentUser();
const { t } = useTranslation();
const { apiKeys } = useStores();
const can = usePolicy(team);
Expand Down Expand Up @@ -79,7 +81,8 @@ function ApiKeys() {
</Text>
<PaginatedList
fetch={apiKeys.fetchPage}
items={apiKeys.orderedData}
items={apiKeys.personalApiKeys}
options={{ userId: user.id }}
heading={<h2>{t("Personal keys")}</h2>}
renderItem={(apiKey: ApiKey) => (
<ApiKeyListItem
Expand Down
9 changes: 9 additions & 0 deletions app/stores/ApiKeysStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { computed } from "mobx";
import ApiKey from "~/models/ApiKey";
import RootStore from "./RootStore";
import Store, { RPCAction } from "./base/Store";
Expand All @@ -8,4 +9,12 @@ export default class ApiKeysStore extends Store<ApiKey> {
constructor(rootStore: RootStore) {
super(rootStore, ApiKey);
}

@computed
get personalApiKeys() {
const userId = this.rootStore.auth.user?.id;
return userId
? this.orderedData.filter((key) => key.userId === userId)
: [];
}
}
4 changes: 3 additions & 1 deletion app/utils/routeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function settingsPath(section?: string): string {
}

export function commentPath(document: Document, comment: Comment): string {
return `${documentPath(document)}?commentId=${comment.id}`;
return `${documentPath(document)}?commentId=${comment.id}${
comment.isResolved ? "&resolved=" : ""
}`;
}

export function collectionPath(url: string, section?: string): string {
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-transform-destructuring": "^7.24.8",
"@babel/plugin-transform-regenerator": "^7.24.7",
"@babel/preset-env": "^7.25.7",
"@babel/preset-env": "^7.25.8",
"@babel/preset-react": "^7.24.7",
"@benrbray/prosemirror-math": "^0.2.2",
"@bull-board/api": "^4.2.2",
Expand All @@ -73,7 +73,6 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@getoutline/react-roving-tabindex": "^3.2.4",
"@getoutline/y-prosemirror": "^1.0.18",
"@hocuspocus/extension-throttle": "1.1.2",
"@hocuspocus/provider": "1.1.2",
"@hocuspocus/server": "1.1.2",
Expand Down Expand Up @@ -111,7 +110,7 @@
"dotenv": "^16.4.5",
"email-providers": "^1.14.0",
"emoji-mart": "^5.6.0",
"emoji-regex": "^10.3.0",
"emoji-regex": "^10.4.0",
"es6-error": "^4.1.1",
"fast-deep-equal": "^3.1.3",
"fetch-retry": "^5.0.6",
Expand Down Expand Up @@ -244,6 +243,7 @@
"winston": "^3.13.0",
"ws": "^7.5.10",
"y-indexeddb": "^9.0.11",
"y-prosemirror": "^1.2.12",
"y-protocols": "^1.0.6",
"yauzl": "^2.10.0",
"yjs": "^13.6.1",
Expand Down Expand Up @@ -329,7 +329,7 @@
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.4",
"browserslist-to-esbuild": "^1.2.0",
"concurrently": "^8.2.2",
"discord-api-types": "^0.37.101",
"discord-api-types": "^0.37.102",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-import-resolver-typescript": "^3.6.3",
Expand All @@ -347,14 +347,14 @@
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"lint-staged": "^13.3.0",
"nodemon": "^3.1.4",
"nodemon": "^3.1.7",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8",
"react-refresh": "^0.14.0",
"rimraf": "^2.5.4",
"rollup-plugin-webpack-stats": "^0.4.1",
"terser": "^5.32.0",
"typescript": "^5.4.5",
"typescript": "^5.6.3",
"vite-plugin-static-copy": "^0.17.0",
"yarn-deduplicate": "^6.0.2"
},
Expand Down
6 changes: 6 additions & 0 deletions plugins/google/server/auth/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
if (!domain && !team) {
const userExists = await User.count({
where: { email: profile.email.toLowerCase() },
include: [
{
association: "team",
required: true,
},
],
});

// Users cannot create a team with personal gmail accounts
Expand Down
2 changes: 1 addition & 1 deletion server/commands/documentCollaborativeUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { yDocToProsemirrorJSON } from "@getoutline/y-prosemirror";
import isEqual from "fast-deep-equal";
import uniq from "lodash/uniq";
import { Node } from "prosemirror-model";
import { yDocToProsemirrorJSON } from "y-prosemirror";
import * as Y from "yjs";
import { ProsemirrorData } from "@shared/types";
import { schema, serializer } from "@server/editor";
Expand Down
5 changes: 4 additions & 1 deletion server/commands/teamCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ async function findAvailableSubdomain(team: Team, requestedSubdomain: string) {
let append = 0;

for (;;) {
const existing = await Team.findOne({ where: { subdomain } });
const existing = await Team.findOne({
where: { subdomain },
paranoid: false,
});

if (existing) {
// subdomain was invalid or already used, try another
Expand Down
7 changes: 0 additions & 7 deletions server/logging/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import chalk from "chalk";
import isArray from "lodash/isArray";
import isEmpty from "lodash/isEmpty";
import isObject from "lodash/isObject";
import isString from "lodash/isString";
import winston from "winston";
import env from "@server/env";
import Metrics from "@server/logging/Metrics";
Expand Down Expand Up @@ -226,12 +225,6 @@ class Logger {
return "[…]" as any as T;
}

if (isString(input)) {
if (sensitiveFields.some((field) => input.includes(field))) {
return "[Filtered]" as any as T;
}
}

if (isArray(input)) {
return input.map((item) => this.sanitize(item, level + 1)) as any as T;
}
Expand Down
Loading

0 comments on commit 4dbd861

Please sign in to comment.