Skip to content

Commit 4dbd861

Browse files
Merge branch 'outline:main' into main
2 parents 5f1bfd1 + 9e37889 commit 4dbd861

32 files changed

+411
-436
lines changed

app/components/WebsocketProvider.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,13 @@ class WebsocketProvider extends React.Component<Props> {
337337
});
338338

339339
this.socket.on("comments.update", (event: PartialExcept<Comment, "id">) => {
340+
const comment = comments.get(event.id);
341+
342+
// Existing policy becomes invalid when the resolution status has changed and we don't have the latest version.
343+
if (comment?.resolvedAt !== event.resolvedAt) {
344+
policies.remove(event.id);
345+
}
346+
340347
comments.add(event);
341348
});
342349

app/editor/extensions/Multiplayer.ts

+62-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import isEqual from "lodash/isEqual";
2+
import { keymap } from "prosemirror-keymap";
13
import {
24
ySyncPlugin,
35
yCursorPlugin,
46
yUndoPlugin,
57
undo,
68
redo,
7-
} from "@getoutline/y-prosemirror";
8-
import { keymap } from "prosemirror-keymap";
9+
} from "y-prosemirror";
910
import * as Y from "yjs";
1011
import Extension from "@shared/editor/lib/Extension";
12+
import { Second } from "@shared/utils/time";
13+
14+
type UserAwareness = {
15+
user: {
16+
id: string;
17+
};
18+
anchor: object;
19+
head: object;
20+
};
1121

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

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

@@ -32,6 +43,51 @@ export default class Multiplayer extends Extension {
3243
}
3344
};
3445

46+
const userAwarenessCache = new Map<
47+
string,
48+
{ aw: UserAwareness; changedAt: Date }
49+
>();
50+
51+
// The opacity of a remote user's selection.
52+
const selectionOpacity = 70;
53+
54+
// The time in milliseconds after which a remote user's selection will be hidden.
55+
const selectionTimeout = 10 * Second.ms;
56+
57+
// We're hijacking this method to store the last time a user's awareness changed as a side
58+
// effect, and otherwise behaving as the default.
59+
const awarenessStateFilter = (
60+
currentClientId: number,
61+
userClientId: number,
62+
aw: UserAwareness
63+
) => {
64+
if (currentClientId === userClientId) {
65+
return false;
66+
}
67+
68+
const cached = userAwarenessCache.get(aw.user.id);
69+
if (!cached || !isEqual(cached?.aw, aw)) {
70+
userAwarenessCache.set(aw.user.id, { aw, changedAt: new Date() });
71+
}
72+
73+
return true;
74+
};
75+
76+
// Override the default selection builder to add a background color to the selection
77+
// only if the user's awareness has changed recently – this stops selections from lingering.
78+
const selectionBuilder = (u: { id: string; color: string }) => {
79+
const cached = userAwarenessCache.get(u.id);
80+
const opacity =
81+
!cached || cached?.changedAt > new Date(Date.now() - selectionTimeout)
82+
? selectionOpacity
83+
: 0;
84+
85+
return {
86+
style: `background-color: ${u.color}${opacity}`,
87+
class: "ProseMirror-yjs-selection",
88+
};
89+
};
90+
3591
provider.setAwarenessField("user", user);
3692

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

4197
return [
4298
ySyncPlugin(type),
43-
yCursorPlugin(provider.awareness),
99+
yCursorPlugin(provider.awareness, {
100+
awarenessStateFilter,
101+
selectionBuilder,
102+
}),
44103
yUndoPlugin(),
45104
keymap({
46105
"Mod-z": undo,

app/menus/CommentMenu.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function CommentMenu({
7575
title: `${t("Edit")}…`,
7676
icon: <EditIcon />,
7777
onClick: onEdit,
78-
visible: can.update,
78+
visible: can.update && !comment.isResolved,
7979
},
8080
actionToMenuItem(
8181
resolveCommentFactory({

app/models/ApiKey.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ class ApiKey extends ParanoidModel {
1616
@observable
1717
expiresAt?: string;
1818

19-
/** An optional datetime that the API key was last used at. */
19+
/** Timestamp that the API key was last used. */
2020
@observable
2121
lastActiveAt?: string;
2222

23+
/** The user ID that the API key belongs to. */
24+
userId: string;
25+
2326
/** The plain text value of the API key, only available on creation. */
2427
value: string;
2528

app/models/Comment.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ class Comment extends Model {
9999
* Whether the comment is resolved
100100
*/
101101
@computed
102-
public get isResolved() {
103-
return !!this.resolvedAt;
102+
public get isResolved(): boolean {
103+
return !!this.resolvedAt || !!this.parentComment?.isResolved;
104104
}
105105

106106
/**

app/scenes/Document/components/CommentThread.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ function CommentThread({
8080
});
8181
const can = usePolicy(document);
8282

83+
const canReply = can.comment && !thread.isResolved;
84+
8385
const highlightedCommentMarks = editor
8486
?.getComments()
8587
.filter((comment) => comment.id === thread.id);
@@ -105,7 +107,7 @@ function CommentThread({
105107
const handleClickThread = () => {
106108
history.replace({
107109
// Clear any commentId from the URL when explicitly focusing a thread
108-
search: "",
110+
search: thread.isResolved ? "resolved=" : "",
109111
pathname: location.pathname.replace(/\/history$/, ""),
110112
state: { commentId: thread.id },
111113
});
@@ -214,7 +216,7 @@ function CommentThread({
214216
))}
215217

216218
<ResizingHeightContainer hideOverflow={false} ref={replyRef}>
217-
{(focused || draft || commentsInThread.length === 0) && can.comment && (
219+
{(focused || draft || commentsInThread.length === 0) && canReply && (
218220
<Fade timing={100}>
219221
<CommentForm
220222
onSaveDraft={onSaveDraft}
@@ -232,7 +234,7 @@ function CommentThread({
232234
</Fade>
233235
)}
234236
</ResizingHeightContainer>
235-
{!focused && !recessed && !draft && can.comment && (
237+
{!focused && !recessed && !draft && canReply && (
236238
<Reply onClick={() => setAutoFocus(true)}>{t("Reply")}</Reply>
237239
)}
238240
</Thread>

app/scenes/Settings/ApiKeys.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import Text from "~/components/Text";
1313
import { createApiKey } from "~/actions/definitions/apiKeys";
1414
import useActionContext from "~/hooks/useActionContext";
1515
import useCurrentTeam from "~/hooks/useCurrentTeam";
16+
import useCurrentUser from "~/hooks/useCurrentUser";
1617
import usePolicy from "~/hooks/usePolicy";
1718
import useStores from "~/hooks/useStores";
1819
import ApiKeyListItem from "./components/ApiKeyListItem";
1920

2021
function ApiKeys() {
2122
const team = useCurrentTeam();
23+
const user = useCurrentUser();
2224
const { t } = useTranslation();
2325
const { apiKeys } = useStores();
2426
const can = usePolicy(team);
@@ -79,7 +81,8 @@ function ApiKeys() {
7981
</Text>
8082
<PaginatedList
8183
fetch={apiKeys.fetchPage}
82-
items={apiKeys.orderedData}
84+
items={apiKeys.personalApiKeys}
85+
options={{ userId: user.id }}
8386
heading={<h2>{t("Personal keys")}</h2>}
8487
renderItem={(apiKey: ApiKey) => (
8588
<ApiKeyListItem

app/stores/ApiKeysStore.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { computed } from "mobx";
12
import ApiKey from "~/models/ApiKey";
23
import RootStore from "./RootStore";
34
import Store, { RPCAction } from "./base/Store";
@@ -8,4 +9,12 @@ export default class ApiKeysStore extends Store<ApiKey> {
89
constructor(rootStore: RootStore) {
910
super(rootStore, ApiKey);
1011
}
12+
13+
@computed
14+
get personalApiKeys() {
15+
const userId = this.rootStore.auth.user?.id;
16+
return userId
17+
? this.orderedData.filter((key) => key.userId === userId)
18+
: [];
19+
}
1120
}

app/utils/routeHelpers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export function settingsPath(section?: string): string {
2525
}
2626

2727
export function commentPath(document: Document, comment: Comment): string {
28-
return `${documentPath(document)}?commentId=${comment.id}`;
28+
return `${documentPath(document)}?commentId=${comment.id}${
29+
comment.isResolved ? "&resolved=" : ""
30+
}`;
2931
}
3032

3133
export function collectionPath(url: string, section?: string): string {

package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"@babel/plugin-transform-class-properties": "^7.24.7",
5959
"@babel/plugin-transform-destructuring": "^7.24.8",
6060
"@babel/plugin-transform-regenerator": "^7.24.7",
61-
"@babel/preset-env": "^7.25.7",
61+
"@babel/preset-env": "^7.25.8",
6262
"@babel/preset-react": "^7.24.7",
6363
"@benrbray/prosemirror-math": "^0.2.2",
6464
"@bull-board/api": "^4.2.2",
@@ -73,7 +73,6 @@
7373
"@fortawesome/free-solid-svg-icons": "^6.5.2",
7474
"@fortawesome/react-fontawesome": "^0.2.2",
7575
"@getoutline/react-roving-tabindex": "^3.2.4",
76-
"@getoutline/y-prosemirror": "^1.0.18",
7776
"@hocuspocus/extension-throttle": "1.1.2",
7877
"@hocuspocus/provider": "1.1.2",
7978
"@hocuspocus/server": "1.1.2",
@@ -111,7 +110,7 @@
111110
"dotenv": "^16.4.5",
112111
"email-providers": "^1.14.0",
113112
"emoji-mart": "^5.6.0",
114-
"emoji-regex": "^10.3.0",
113+
"emoji-regex": "^10.4.0",
115114
"es6-error": "^4.1.1",
116115
"fast-deep-equal": "^3.1.3",
117116
"fetch-retry": "^5.0.6",
@@ -244,6 +243,7 @@
244243
"winston": "^3.13.0",
245244
"ws": "^7.5.10",
246245
"y-indexeddb": "^9.0.11",
246+
"y-prosemirror": "^1.2.12",
247247
"y-protocols": "^1.0.6",
248248
"yauzl": "^2.10.0",
249249
"yjs": "^13.6.1",
@@ -329,7 +329,7 @@
329329
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.4",
330330
"browserslist-to-esbuild": "^1.2.0",
331331
"concurrently": "^8.2.2",
332-
"discord-api-types": "^0.37.101",
332+
"discord-api-types": "^0.37.102",
333333
"eslint": "^8.57.0",
334334
"eslint-config-prettier": "^8.10.0",
335335
"eslint-import-resolver-typescript": "^3.6.3",
@@ -347,14 +347,14 @@
347347
"jest-environment-jsdom": "^29.7.0",
348348
"jest-fetch-mock": "^3.0.3",
349349
"lint-staged": "^13.3.0",
350-
"nodemon": "^3.1.4",
350+
"nodemon": "^3.1.7",
351351
"postinstall-postinstall": "^2.1.0",
352352
"prettier": "^2.8.8",
353353
"react-refresh": "^0.14.0",
354354
"rimraf": "^2.5.4",
355355
"rollup-plugin-webpack-stats": "^0.4.1",
356356
"terser": "^5.32.0",
357-
"typescript": "^5.4.5",
357+
"typescript": "^5.6.3",
358358
"vite-plugin-static-copy": "^0.17.0",
359359
"yarn-deduplicate": "^6.0.2"
360360
},

plugins/google/server/auth/google.ts

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
7474
if (!domain && !team) {
7575
const userExists = await User.count({
7676
where: { email: profile.email.toLowerCase() },
77+
include: [
78+
{
79+
association: "team",
80+
required: true,
81+
},
82+
],
7783
});
7884

7985
// Users cannot create a team with personal gmail accounts

server/commands/documentCollaborativeUpdater.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { yDocToProsemirrorJSON } from "@getoutline/y-prosemirror";
21
import isEqual from "fast-deep-equal";
32
import uniq from "lodash/uniq";
43
import { Node } from "prosemirror-model";
4+
import { yDocToProsemirrorJSON } from "y-prosemirror";
55
import * as Y from "yjs";
66
import { ProsemirrorData } from "@shared/types";
77
import { schema, serializer } from "@server/editor";

server/commands/teamCreator.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ async function findAvailableSubdomain(team: Team, requestedSubdomain: string) {
9090
let append = 0;
9191

9292
for (;;) {
93-
const existing = await Team.findOne({ where: { subdomain } });
93+
const existing = await Team.findOne({
94+
where: { subdomain },
95+
paranoid: false,
96+
});
9497

9598
if (existing) {
9699
// subdomain was invalid or already used, try another

server/logging/Logger.ts

-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import chalk from "chalk";
44
import isArray from "lodash/isArray";
55
import isEmpty from "lodash/isEmpty";
66
import isObject from "lodash/isObject";
7-
import isString from "lodash/isString";
87
import winston from "winston";
98
import env from "@server/env";
109
import Metrics from "@server/logging/Metrics";
@@ -226,12 +225,6 @@ class Logger {
226225
return "[…]" as any as T;
227226
}
228227

229-
if (isString(input)) {
230-
if (sensitiveFields.some((field) => input.includes(field))) {
231-
return "[Filtered]" as any as T;
232-
}
233-
}
234-
235228
if (isArray(input)) {
236229
return input.map((item) => this.sanitize(item, level + 1)) as any as T;
237230
}

0 commit comments

Comments
 (0)