Removal of non-collaborative editing code paths (#4210)

This commit is contained in:
Tom Moor
2023-03-28 22:13:42 -04:00
committed by GitHub
parent 3108a26793
commit 7ba6a9379b
27 changed files with 45 additions and 552 deletions

View File

@ -15,7 +15,7 @@ export const restoreRevision = createAction({
section: RevisionSection,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
perform: async ({ t, event, location, activeDocumentId }) => {
perform: async ({ event, location, activeDocumentId }) => {
event?.preventDefault();
if (!activeDocumentId) {
return;
@ -26,26 +26,15 @@ export const restoreRevision = createAction({
});
const revisionId = match?.params.revisionId;
const { team } = stores.auth;
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
if (team?.collaborativeEditing) {
history.push(document.url, {
restore: true,
revisionId,
});
} else {
await document.restore({
revisionId,
});
stores.toasts.showToast(t("Document restored"), {
type: "success",
});
history.push(document.url);
}
history.push(document.url, {
restore: true,
revisionId,
});
},
});

View File

@ -9,7 +9,6 @@ import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
import NudeButton from "~/components/NudeButton";
import Popover from "~/components/Popover";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
@ -20,7 +19,6 @@ type Props = {
function Collaborators(props: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
const team = useCurrentTeam();
const currentUserId = user?.id;
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
const { users, presence, ui } = useStores();
@ -79,8 +77,7 @@ function Collaborators(props: Props) {
const isPresent = presentIds.includes(collaborator.id);
const isEditing = editingIds.includes(collaborator.id);
const isObserving = ui.observingUserId === collaborator.id;
const isObservable =
team.collaborativeEditing && collaborator.id !== user.id;
const isObservable = collaborator.id !== user.id;
return (
<AvatarWithPresence

View File

@ -121,16 +121,11 @@ function InnerDocumentLink(
if (!document) {
return;
}
await documents.update(
{
id: document.id,
text: document.text,
title,
},
{
lastRevision: document.revision,
}
);
await documents.update({
id: document.id,
text: document.text,
title,
});
},
[documents, document]
);

View File

@ -17,7 +17,6 @@ type SaveOptions = {
publish?: boolean;
done?: boolean;
autosave?: boolean;
lastRevision?: number;
};
export default class Document extends ParanoidModel {
@ -53,7 +52,6 @@ export default class Document extends ParanoidModel {
@observable
id: string;
@Field
@observable
text: string;
@ -345,23 +343,10 @@ export default class Document extends ParanoidModel {
@action
save = async (options?: SaveOptions | undefined) => {
const params = this.toAPI();
const collaborativeEditing = this.store.rootStore.auth.team
?.collaborativeEditing;
if (collaborativeEditing) {
delete params.text;
}
this.isSaving = true;
try {
const model = await this.store.save(
{ ...params, id: this.id },
{
lastRevision: options?.lastRevision || this.revision,
...options,
}
);
const model = await this.store.save({ ...params, id: this.id }, options);
// if saving is successful set the new values on the model itself
set(this, { ...params, ...model });

View File

@ -25,10 +25,6 @@ class Team extends BaseModel {
@observable
inviteRequired: boolean;
@Field
@observable
collaborativeEditing: boolean;
@Field
@observable
commenting: boolean;
@ -92,10 +88,7 @@ class Team extends BaseModel {
*/
@computed
get seamlessEditing(): boolean {
return (
this.collaborativeEditing &&
!!this.getPreference(TeamPreference.SeamlessEdit, true)
);
return !!this.getPreference(TeamPreference.SeamlessEdit, true);
}
/**

View File

@ -30,7 +30,6 @@ type Children = (options: {
document: Document;
revision: Revision | undefined;
abilities: Record<string, boolean>;
isEditing: boolean;
readOnly: boolean;
onCreateLink: (title: string) => Promise<string>;
sharedTree: NavigationNode | undefined;
@ -186,21 +185,12 @@ function DataLoader({ match, children }: Props) {
);
}
// We do not want to remount the document when changing from view->edit
// on the multiplayer flag as the doc is guaranteed to be upto date.
const key = team.collaborativeEditing
? ""
: isEditing
? "editing"
: "read-only";
return (
<React.Fragment key={key}>
<React.Fragment>
{children({
document,
revision,
abilities: can,
isEditing,
readOnly:
!isEditing || !can.update || document.isArchived || !!revisionId,
onCreateLink,

View File

@ -39,7 +39,6 @@ import { isModKey } from "~/utils/keyboard";
import {
documentHistoryUrl,
editDocumentUrl,
documentUrl,
updateDocumentUrl,
} from "~/utils/routeHelpers";
import Container from "./Container";
@ -100,9 +99,6 @@ class DocumentScene extends React.Component<Props> {
@observable
isEmpty = true;
@observable
lastRevision: number = this.props.document.revision;
@observable
title: string = this.props.document.title;
@ -116,39 +112,9 @@ class DocumentScene extends React.Component<Props> {
}
componentDidUpdate(prevProps: Props) {
const { auth, document, t } = this.props;
if (prevProps.readOnly && !this.props.readOnly) {
this.updateIsDirty();
}
if (this.props.readOnly || auth.team?.collaborativeEditing) {
this.lastRevision = document.revision;
}
if (
!this.props.readOnly &&
!auth.team?.collaborativeEditing &&
prevProps.document.revision !== this.lastRevision
) {
if (auth.user && document.updatedBy.id !== auth.user.id) {
this.props.toasts.showToast(
t(`Document updated by {{userName}}`, {
userName: document.updatedBy.name,
}),
{
timeout: 30 * 1000,
type: "warning",
action: {
text: "Reload",
onClick: () => {
window.location.href = documentUrl(document);
},
},
}
);
}
}
}
componentWillUnmount() {
@ -332,13 +298,8 @@ class DocumentScene extends React.Component<Props> {
this.isPublishing = !!options.publish;
try {
const savedDocument = await document.save({
...options,
lastRevision: this.lastRevision,
});
const savedDocument = await document.save(options);
this.isEditorDirty = false;
this.lastRevision = savedDocument.revision;
if (options.done) {
this.props.history.push(savedDocument.url);
@ -385,7 +346,7 @@ class DocumentScene extends React.Component<Props> {
};
onChange = (getEditorText: () => string) => {
const { document, auth } = this.props;
const { document } = this.props;
this.getEditorText = getEditorText;
// Keep derived task list in sync
@ -393,25 +354,6 @@ class DocumentScene extends React.Component<Props> {
const total = tasks?.length ?? 0;
const completed = tasks?.filter((t) => t.completed).length ?? 0;
document.updateTasks(total, completed);
// If the multiplayer editor is enabled we're done here as changes are saved
// through the persistence protocol. The rest of this method is legacy.
if (auth.team?.collaborativeEditing) {
return;
}
// document change while read only is presumed to be a checkbox edit,
// in that case we don't delay in saving for a better user experience.
if (this.props.readOnly) {
this.updateIsDirty();
this.onSave({
done: false,
autosave: true,
});
} else {
this.updateIsDirtyDebounced();
this.autosave();
}
};
onHeadingsChange = (headings: Heading[]) => {
@ -449,14 +391,9 @@ class DocumentScene extends React.Component<Props> {
const hasHeadings = this.headings.length > 0;
const showContents =
ui.tocVisible &&
((readOnly && hasHeadings) || team?.collaborativeEditing);
const collaborativeEditing =
team?.collaborativeEditing &&
!document.isArchived &&
!document.isDeleted &&
!revision &&
!isShare;
ui.tocVisible && ((readOnly && hasHeadings) || !readOnly);
const multiplayerEditor =
!document.isArchived && !document.isDeleted && !revision && !isShare;
const canonicalUrl = shareId
? this.props.match.url
@ -506,35 +443,12 @@ class DocumentScene extends React.Component<Props> {
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
<Container justify="center" column auto>
{!readOnly && (
<>
<Prompt
when={
this.isEditorDirty &&
!this.isUploading &&
!team?.collaborativeEditing
}
message={(location, action) => {
if (
// a URL replace matching the current document indicates a title change
// no guard is needed for this transition
action === "REPLACE" &&
location.pathname === editDocumentUrl(document)
) {
return true;
}
return t(
`You have unsaved changes.\nAre you sure you want to discard them?`
) as string;
}}
/>
<Prompt
when={this.isUploading && !this.isEditorDirty}
message={t(
`Images are still uploading.\nAre you sure you want to discard them?`
)}
/>
</>
<Prompt
when={this.isUploading && !this.isEditorDirty}
message={t(
`Images are still uploading.\nAre you sure you want to discard them?`
)}
/>
)}
<Header
document={document}
@ -578,7 +492,7 @@ class DocumentScene extends React.Component<Props> {
id={document.id}
key={embedsDisabled ? "disabled" : "enabled"}
ref={this.editor}
multiplayer={collaborativeEditing}
multiplayer={multiplayerEditor}
shareId={shareId}
isDraft={document.isDraft}
template={document.isTemplate}

View File

@ -1,87 +0,0 @@
import * as React from "react";
import { USER_PRESENCE_INTERVAL } from "@shared/constants";
import { WebsocketContext } from "~/components/WebsocketProvider";
type Props = {
documentId: string;
isEditing: boolean;
presence: boolean;
};
export default class SocketPresence extends React.Component<Props> {
static contextType = WebsocketContext;
previousContext: typeof WebsocketContext;
editingInterval: ReturnType<typeof setInterval> | undefined;
componentDidMount() {
this.editingInterval = this.props.presence
? setInterval(() => {
if (this.props.isEditing) {
this.emitPresence();
}
}, USER_PRESENCE_INTERVAL)
: undefined;
this.setupOnce();
}
componentDidUpdate(prevProps: Props) {
this.setupOnce();
if (prevProps.isEditing !== this.props.isEditing) {
this.emitPresence();
}
}
componentWillUnmount() {
if (this.context) {
this.context.emit("leave", {
documentId: this.props.documentId,
});
this.context.off("authenticated", this.emitJoin);
}
if (this.editingInterval) {
clearInterval(this.editingInterval);
}
}
setupOnce = () => {
if (this.context && this.context !== this.previousContext) {
this.previousContext = this.context;
if (this.context.authenticated) {
this.emitJoin();
}
this.context.on("authenticated", () => {
this.emitJoin();
});
}
};
emitJoin = () => {
if (!this.context) {
return;
}
this.context.emit("join", {
documentId: this.props.documentId,
isEditing: this.props.isEditing,
});
};
emitPresence = () => {
if (!this.context) {
return;
}
this.context.emit("presence", {
documentId: this.props.documentId,
isEditing: this.props.isEditing,
});
};
render() {
return this.props.children || null;
}
}

View File

@ -1,12 +1,10 @@
import * as React from "react";
import { StaticContext } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useLastVisitedPath from "~/hooks/useLastVisitedPath";
import useStores from "~/hooks/useStores";
import DataLoader from "./components/DataLoader";
import Document from "./components/Document";
import SocketPresence from "./components/SocketPresence";
type Params = {
documentSlug: string;
@ -24,7 +22,6 @@ type Props = RouteComponentProps<Params, StaticContext, LocationState>;
export default function DocumentScene(props: Props) {
const { ui } = useStores();
const team = useCurrentTeam();
const { documentSlug, revisionId } = props.match.params;
const currentPath = props.location.pathname;
const [, setLastVisitedPath] = useLastVisitedPath();
@ -52,26 +49,7 @@ export default function DocumentScene(props: Props) {
history={props.history}
location={props.location}
>
{({ document, isEditing, ...rest }) => {
const isActive =
!document.isArchived && !document.isDeleted && !revisionId;
// TODO: Remove once multiplayer is 100% rollout, SocketPresence will
// no longer be required
if (isActive && !team.collaborativeEditing) {
return (
<SocketPresence
documentId={document.id}
isEditing={isEditing}
presence={!team.collaborativeEditing}
>
<Document document={document} {...rest} />
</SocketPresence>
);
}
return <Document document={document} {...rest} />;
}}
{(rest) => <Document {...rest} />}
</DataLoader>
);
}

View File

@ -43,22 +43,20 @@ function Features() {
the experience for all members of the workspace.
</Trans>
</Text>
{team.collaborativeEditing && (
<SettingRow
<SettingRow
name={TeamPreference.SeamlessEdit}
label={t("Seamless editing")}
description={t(
`When enabled documents are always editable for team members that have permission. When disabled there is a separate editing view.`
)}
>
<Switch
id={TeamPreference.SeamlessEdit}
name={TeamPreference.SeamlessEdit}
label={t("Seamless editing")}
description={t(
`When enabled documents are always editable for team members that have permission. When disabled there is a separate editing view.`
)}
>
<Switch
id={TeamPreference.SeamlessEdit}
name={TeamPreference.SeamlessEdit}
checked={team.getPreference(TeamPreference.SeamlessEdit, true)}
onChange={handlePreferenceChange}
/>
</SettingRow>
)}
checked={team.getPreference(TeamPreference.SeamlessEdit, true)}
onChange={handlePreferenceChange}
/>
</SettingRow>
<SettingRow
name={TeamPreference.Commenting}
label={
@ -74,7 +72,6 @@ function Features() {
id={TeamPreference.Commenting}
name={TeamPreference.Commenting}
checked={team.getPreference(TeamPreference.Commenting, false)}
disabled={!team.collaborativeEditing}
onChange={handlePreferenceChange}
/>
</SettingRow>

View File

@ -270,7 +270,6 @@ export default class AuthStore {
name?: string;
avatarUrl?: string | null | undefined;
sharing?: boolean;
collaborativeEditing?: boolean;
defaultCollectionId?: string | null;
subdomain?: string | null | undefined;
allowedDomains?: string[] | null | undefined;

View File

@ -678,7 +678,6 @@ export default class DocumentsStore extends BaseStore<Document> {
publish?: boolean;
done?: boolean;
autosave?: boolean;
lastRevision: number;
}
) {
this.isSaving = true;

View File

@ -65,13 +65,7 @@ export default async function documentUpdater({
document.fullWidth = fullWidth;
}
if (text !== undefined) {
if (user.team?.collaborativeEditing) {
document = DocumentHelper.applyMarkdownToDocument(document, text, append);
} else if (append) {
document.text += text;
} else {
document.text = text;
}
document = DocumentHelper.applyMarkdownToDocument(document, text, append);
}
const changed = document.changed();

View File

@ -21,7 +21,6 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
guestSignin,
documentEmbeds,
memberCollectionCreate,
collaborativeEditing,
defaultCollectionId,
defaultUserRole,
inviteRequired,
@ -56,9 +55,6 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
if (defaultCollectionId !== undefined) {
team.defaultCollectionId = defaultCollectionId;
}
if (collaborativeEditing !== undefined) {
team.collaborativeEditing = collaborativeEditing;
}
if (defaultUserRole !== undefined) {
team.defaultUserRole = defaultUserRole;
}

View File

@ -136,10 +136,6 @@ class Team extends ParanoidModel {
@Column
memberCollectionCreate: boolean;
@Default(true)
@Column
collaborativeEditing: boolean;
@Default("member")
@IsIn([["viewer", "member"]])
@Column

View File

@ -7,7 +7,6 @@ export default function presentTeam(team: Team) {
avatarUrl: team.avatarUrl,
sharing: team.sharing,
memberCollectionCreate: team.memberCollectionCreate,
collaborativeEditing: team.collaborativeEditing,
defaultCollectionId: team.defaultCollectionId,
documentEmbeds: team.documentEmbeds,
guestSignin: team.emailSigninEnabled,

View File

@ -220,46 +220,3 @@ describe("documents.delete", () => {
expect(backlinks.length).toBe(0);
});
});
describe("documents.title_change", () => {
test("should update titles in backlinked documents", async () => {
const newTitle = "test";
const document = await buildDocument();
const otherDocument = await buildDocument();
const previousTitle = otherDocument.title;
// create a doc with a link back
document.text = `[${otherDocument.title}](${otherDocument.url})`;
await document.save();
// ensure the backlinks are created
const processor = new BacklinksProcessor();
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
data: { title: document.title, autosave: false, done: true },
ip,
});
// change the title of the linked doc
otherDocument.title = newTitle;
await otherDocument.save();
// does the text get updated with the new title
await processor.perform({
name: "documents.title_change",
documentId: otherDocument.id,
collectionId: otherDocument.collectionId,
teamId: otherDocument.teamId,
actorId: otherDocument.createdById,
createdAt: new Date().toISOString(),
data: {
previousTitle,
title: newTitle,
},
ip,
});
await document.reload();
expect(document.text).toBe(`[${newTitle}](${otherDocument.url})`);
});
});

View File

@ -1,15 +1,14 @@
import { Op } from "sequelize";
import { Document, Backlink, Team } from "@server/models";
import { Document, Backlink } from "@server/models";
import { Event, DocumentEvent, RevisionEvent } from "@server/types";
import parseDocumentIds from "@server/utils/parseDocumentIds";
import slugify from "@server/utils/slugify";
import BaseProcessor from "./BaseProcessor";
export default class BacklinksProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = [
"documents.publish",
"documents.update",
"documents.title_change",
//"documents.title_change",
"documents.delete",
];
@ -98,49 +97,7 @@ export default class BacklinksProcessor extends BaseProcessor {
break;
}
const document = await Document.findByPk(event.documentId);
if (!document) {
return;
}
// TODO: Handle re-writing of titles into CRDT
const team = await Team.findByPk(document.teamId);
if (team?.collaborativeEditing) {
break;
}
// update any link titles in documents that lead to this one
const backlinks = await Backlink.findAll({
where: {
documentId: event.documentId,
},
include: [
{
model: Document,
as: "reverseDocument",
},
],
});
await Promise.all(
backlinks.map(async (backlink) => {
const previousUrl = `/doc/${slugify(previousTitle)}-${
document.urlId
}`;
// find links in the other document that lead to this one and have
// the old title as anchor text. Go ahead and update those to the
// new title automatically
backlink.reverseDocument.text = backlink.reverseDocument.text.replace(
`[${previousTitle}](${previousUrl})`,
`[${title}](${document.url})`
);
await backlink.reverseDocument.save({
silent: true,
hooks: false,
});
})
);
break;
}

View File

@ -45,15 +45,6 @@ exports[`#documents.search should require authentication 1`] = `
}
`;
exports[`#documents.update should fail if document lastRevision does not match 1`] = `
{
"error": "invalid_request",
"message": "Document has changed since last revision",
"ok": false,
"status": 400,
}
`;
exports[`#documents.update should require authentication 1`] = `
{
"error": "authentication_required",

View File

@ -2456,7 +2456,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
},
});
const body = await res.json();
@ -2478,7 +2477,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
publish: true,
},
});
@ -2503,7 +2501,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
collectionId: collection.id,
publish: true,
},
@ -2526,7 +2523,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
collectionId: collection.id,
publish: true,
},
@ -2551,7 +2547,6 @@ describe("#documents.update", () => {
id: template.id,
title: "Updated title",
text: "Updated text",
lastRevision: template.revisionCount,
publish: true,
},
});
@ -2582,7 +2577,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
publish: true,
},
});
@ -2603,27 +2597,11 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
});
it("should fail if document lastRevision does not match", async () => {
const { user, document } = await seed();
const res = await server.post("/api/documents.update", {
body: {
token: user.getJwtToken(),
id: document.id,
text: "Updated text",
lastRevision: 123,
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body).toMatchSnapshot();
});
it("should update document details for children", async () => {
const { user, document, collection } = await seed();
collection.documentStructure = [
@ -2670,7 +2648,6 @@ describe("#documents.update", () => {
token: admin.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revisionCount,
},
});
const body = await res.json();
@ -2694,7 +2671,6 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
@ -2709,7 +2685,6 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
@ -2722,7 +2697,6 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Additional text",
lastRevision: document.revisionCount,
append: true,
},
});
@ -2738,7 +2712,6 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revisionCount,
title: "Updated Title",
append: true,
},
@ -2754,7 +2727,6 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revisionCount,
title: "Updated Title",
text: "",
},
@ -2770,7 +2742,6 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revisionCount,
title: document.title,
text: document.text,
},
@ -2851,7 +2822,6 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revisionCount,
collectionId: collection.id,
publish: true,
},

View File

@ -880,7 +880,6 @@ router.post(
text,
fullWidth,
publish,
lastRevision,
templateId,
collectionId,
append,
@ -908,10 +907,6 @@ router.post(
authorize(user, "publish", collection);
}
if (lastRevision && lastRevision !== document.revisionCount) {
throw InvalidRequestError("Document has changed since last revision");
}
collection = await sequelize.transaction(async (transaction) => {
await documentUpdater({
document,

View File

@ -190,9 +190,6 @@ export const DocumentsUpdateSchema = BaseSchema.extend({
/** Boolean to denote if the doc should be published */
publish: z.boolean().optional(),
/** Revision to compare against document revision count */
lastRevision: z.number().optional(),
/** Doc template Id */
templateId: z.string().uuid().nullish(),

View File

@ -18,8 +18,6 @@ export const TeamsUpdateSchema = BaseSchema.extend({
documentEmbeds: z.boolean().optional(),
/** Whether team members are able to create new collections */
memberCollectionCreate: z.boolean().optional(),
/** Whether collaborative editing is enabled */
collaborativeEditing: z.boolean().optional(),
/** The default landing collection for the team */
defaultCollectionId: z.string().uuid().nullish(),
/** The default user role */

View File

@ -8,7 +8,7 @@ import Logger from "@server/logging/Logger";
import Metrics from "@server/logging/Metrics";
import * as Tracing from "@server/logging/tracer";
import { traceFunction } from "@server/logging/tracing";
import { Document, Collection, View, User } from "@server/models";
import { Collection, User } from "@server/models";
import { can } from "@server/policies";
import ShutdownHelper, { ShutdownOrder } from "@server/utils/ShutdownHelper";
import { getUserForJWT } from "@server/utils/jwt";
@ -186,58 +186,6 @@ async function authenticated(io: IO.Server, socket: SocketWithAuth) {
Metrics.increment("websockets.collections.join");
}
}
// user is joining a document channel, because they have navigated to
// view a document.
if (event.documentId) {
const document = await Document.findByPk(event.documentId, {
userId: user.id,
});
if (can(user, "read", document)) {
const room = `document-${event.documentId}`;
await View.touch(event.documentId, user.id, event.isEditing);
const editing = await View.findRecentlyEditingByDocument(
event.documentId
);
await socket.join(room);
Metrics.increment("websockets.documents.join");
// let everyone else in the room know that a new user joined
io.to(room).emit("user.join", {
userId: user.id,
documentId: event.documentId,
isEditing: event.isEditing,
});
// let this user know who else is already present in the room
try {
const socketIds = await io.in(room).allSockets();
// because a single user can have multiple socket connections we
// need to make sure that only unique userIds are returned. A Map
// makes this easy.
const userIds = new Map();
for (const socketId of socketIds) {
const userId = await Redis.defaultClient.hget(socketId, "userId");
userIds.set(userId, userId);
}
socket.emit("document.presence", {
documentId: event.documentId,
userIds: Array.from(userIds.keys()),
editingIds: editing.map((view) => view.userId),
});
} catch (err) {
if (err) {
Logger.error("Error getting clients for room", err);
return;
}
}
}
}
});
// allow the client to request to leave rooms
@ -246,56 +194,6 @@ async function authenticated(io: IO.Server, socket: SocketWithAuth) {
await socket.leave(`collection-${event.collectionId}`);
Metrics.increment("websockets.collections.leave");
}
if (event.documentId) {
const room = `document-${event.documentId}`;
await socket.leave(room);
Metrics.increment("websockets.documents.leave");
io.to(room).emit("user.leave", {
userId: user.id,
documentId: event.documentId,
});
}
});
socket.on("disconnecting", () => {
socket.rooms.forEach((room) => {
if (room.startsWith("document-")) {
const documentId = room.replace("document-", "");
io.to(room).emit("user.leave", {
userId: user.id,
documentId,
});
}
});
});
socket.on("presence", async (event) => {
Metrics.increment("websockets.presence");
const room = `document-${event.documentId}`;
if (event.documentId && socket.rooms.has(room)) {
await View.touch(event.documentId, user.id, event.isEditing);
io.to(room).emit("user.presence", {
userId: user.id,
documentId: event.documentId,
isEditing: event.isEditing,
});
socket.on("typing", async (event) => {
const room = `document-${event.documentId}`;
if (event.documentId && socket.rooms[room]) {
io.to(room).emit("user.typing", {
userId: user.id,
documentId: event.documentId,
commentId: event.commentId,
});
}
});
}
});
}

View File

@ -123,7 +123,6 @@ export function buildTeam(overrides: Record<string, any> = {}) {
return Team.create(
{
name: `Team ${count}`,
collaborativeEditing: false,
authenticationProviders: [
{
name: "slack",

View File

@ -11,7 +11,6 @@ export const seed = async () => {
const team = await Team.create(
{
name: "Team",
collaborativeEditing: false,
authenticationProviders: [
{
name: "slack",

View File

@ -70,7 +70,6 @@
"Download {{ platform }} app": "Download {{ platform }} app",
"Log out": "Log out",
"Restore revision": "Restore revision",
"Document restored": "Document restored",
"Copy link": "Copy link",
"Link copied": "Link copied",
"Dark": "Dark",
@ -326,6 +325,7 @@
"Manual sort": "Manual sort",
"Delete comment": "Delete comment",
"Comment options": "Comment options",
"Document restored": "Document restored",
"Document options": "Document options",
"Restore": "Restore",
"Choose a collection": "Choose a collection",
@ -442,8 +442,6 @@
"Cancel": "Cancel",
"No comments yet": "No comments yet",
"Error updating comment": "Error updating comment",
"Document updated by {{userName}}": "Document updated by {{userName}}",
"You have unsaved changes.\nAre you sure you want to discard them?": "You have unsaved changes.\nAre you sure you want to discard them?",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"Viewed by": "Viewed by",
"only you": "only you",