fix: User presence is not updated when leaving a document
This commit is contained in:
@@ -85,9 +85,7 @@ class WebsocketProvider extends React.Component<Props> {
|
|||||||
stars,
|
stars,
|
||||||
memberships,
|
memberships,
|
||||||
policies,
|
policies,
|
||||||
presence,
|
|
||||||
comments,
|
comments,
|
||||||
views,
|
|
||||||
subscriptions,
|
subscriptions,
|
||||||
fileOperations,
|
fileOperations,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -105,12 +103,6 @@ class WebsocketProvider extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("disconnect", () => {
|
|
||||||
// when the socket is disconnected we need to clear all presence state as
|
|
||||||
// it's no longer reliable.
|
|
||||||
presence.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
// on reconnection, reset the transports option, as the Websocket
|
// on reconnection, reset the transports option, as the Websocket
|
||||||
// connection may have failed (caused by proxy, firewall, browser, ...)
|
// connection may have failed (caused by proxy, firewall, browser, ...)
|
||||||
this.socket.io.on("reconnect_attempt", () => {
|
this.socket.io.on("reconnect_attempt", () => {
|
||||||
@@ -468,33 +460,6 @@ class WebsocketProvider extends React.Component<Props> {
|
|||||||
this.socket.on("leave", (event: any) => {
|
this.socket.on("leave", (event: any) => {
|
||||||
this.socket?.emit("leave", event);
|
this.socket?.emit("leave", event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// received whenever we join a document room, the payload includes
|
|
||||||
// userIds that are present/viewing and those that are editing.
|
|
||||||
this.socket.on("document.presence", (event: any) => {
|
|
||||||
presence.init(event.documentId, event.userIds, event.editingIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
// received whenever a new user joins a document room, aka they
|
|
||||||
// navigate to / start viewing a document
|
|
||||||
this.socket.on("user.join", (event: any) => {
|
|
||||||
presence.touch(event.documentId, event.userId, event.isEditing);
|
|
||||||
views.touch(event.documentId, event.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// received whenever a new user leaves a document room, aka they
|
|
||||||
// navigate away / stop viewing a document
|
|
||||||
this.socket.on("user.leave", (event: any) => {
|
|
||||||
presence.leave(event.documentId, event.userId);
|
|
||||||
views.touch(event.documentId, event.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// received when another client in a document room wants to change
|
|
||||||
// or update it's presence. Currently the only property is whether
|
|
||||||
// the client is in editing state or not.
|
|
||||||
this.socket.on("user.presence", (event: any) => {
|
|
||||||
presence.touch(event.documentId, event.userId, event.isEditing);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import useIsMounted from "~/hooks/useIsMounted";
|
|||||||
import usePageVisibility from "~/hooks/usePageVisibility";
|
import usePageVisibility from "~/hooks/usePageVisibility";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
|
import { AwarenessChangeEvent } from "~/types";
|
||||||
import Logger from "~/utils/Logger";
|
import Logger from "~/utils/Logger";
|
||||||
import { supportsPassiveListener } from "~/utils/browser";
|
import { supportsPassiveListener } from "~/utils/browser";
|
||||||
import { homePath } from "~/utils/routeHelpers";
|
import { homePath } from "~/utils/routeHelpers";
|
||||||
@@ -30,10 +31,6 @@ export type ConnectionStatus =
|
|||||||
| "disconnected"
|
| "disconnected"
|
||||||
| void;
|
| void;
|
||||||
|
|
||||||
type AwarenessChangeEvent = {
|
|
||||||
states: { user: { id: string }; cursor: any; scrollY: number | undefined }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConnectionStatusEvent = { status: ConnectionStatus };
|
type ConnectionStatusEvent = { status: ConnectionStatus };
|
||||||
|
|
||||||
type MessageEvent = {
|
type MessageEvent = {
|
||||||
@@ -108,11 +105,11 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
|||||||
history.replace(homePath());
|
history.replace(homePath());
|
||||||
});
|
});
|
||||||
|
|
||||||
provider.on("awarenessChange", ({ states }: AwarenessChangeEvent) => {
|
provider.on("awarenessChange", (event: AwarenessChangeEvent) => {
|
||||||
states.forEach(({ user, cursor, scrollY }) => {
|
presence.updateFromAwarenessChangeEvent(documentId, event);
|
||||||
if (user) {
|
|
||||||
presence.touch(documentId, user.id, !!cursor);
|
|
||||||
|
|
||||||
|
event.states.forEach(({ user, scrollY }) => {
|
||||||
|
if (user) {
|
||||||
if (scrollY !== undefined && user.id === ui.observingUserId) {
|
if (scrollY !== undefined && user.id === ui.observingUserId) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: scrollY * window.innerHeight,
|
top: scrollY * window.innerHeight,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable, action } from "mobx";
|
import { observable, action } from "mobx";
|
||||||
import { USER_PRESENCE_INTERVAL } from "@shared/constants";
|
import { AwarenessChangeEvent } from "~/types";
|
||||||
|
|
||||||
type DocumentPresence = Map<
|
type DocumentPresence = Map<
|
||||||
string,
|
string,
|
||||||
@@ -15,19 +15,11 @@ export default class PresenceStore {
|
|||||||
|
|
||||||
timeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
timeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||||
|
|
||||||
// called to setup when we get the initial state from document.presence
|
offlineTimeout = 30000;
|
||||||
// websocket message. overrides any existing state
|
|
||||||
@action
|
|
||||||
init(documentId: string, userIds: string[], editingIds: string[]) {
|
|
||||||
this.data.set(documentId, new Map());
|
|
||||||
userIds.forEach((userId) =>
|
|
||||||
this.touch(documentId, userId, editingIds.includes(userId))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// called when a user leave the room – user.leave websocket message.
|
// called when a user leaves the document
|
||||||
@action
|
@action
|
||||||
leave(documentId: string, userId: string) {
|
public leave(documentId: string, userId: string) {
|
||||||
const existing = this.data.get(documentId);
|
const existing = this.data.get(documentId);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -35,23 +27,27 @@ export default class PresenceStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
public updateFromAwarenessChangeEvent(
|
||||||
update(documentId: string, userId: string, isEditing: boolean) {
|
documentId: string,
|
||||||
const existing = this.data.get(documentId) || new Map();
|
event: AwarenessChangeEvent
|
||||||
existing.set(userId, {
|
) {
|
||||||
isEditing,
|
const presence = this.data.get(documentId);
|
||||||
userId,
|
let existingUserIds = (presence ? Array.from(presence.values()) : []).map(
|
||||||
|
(p) => p.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
event.states.forEach((state) => {
|
||||||
|
const { user, cursor } = state;
|
||||||
|
this.update(documentId, user.id, !!cursor);
|
||||||
|
existingUserIds = existingUserIds.filter((id) => id !== user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
existingUserIds.forEach((userId) => {
|
||||||
|
this.leave(documentId, userId);
|
||||||
});
|
});
|
||||||
this.data.set(documentId, existing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// called when a user presence message is received – user.presence websocket
|
public touch(documentId: string, userId: string, isEditing: boolean) {
|
||||||
// message.
|
|
||||||
// While in edit mode a message is sent every USER_PRESENCE_INTERVAL, if
|
|
||||||
// the other clients don't receive within USER_PRESENCE_INTERVAL*2 then a
|
|
||||||
// timeout is triggered causing the users presence to default back to not
|
|
||||||
// editing state as a safety measure.
|
|
||||||
touch(documentId: string, userId: string, isEditing: boolean) {
|
|
||||||
const id = `${documentId}-${userId}`;
|
const id = `${documentId}-${userId}`;
|
||||||
let timeout = this.timeouts.get(id);
|
let timeout = this.timeouts.get(id);
|
||||||
|
|
||||||
@@ -62,20 +58,28 @@ export default class PresenceStore {
|
|||||||
|
|
||||||
this.update(documentId, userId, isEditing);
|
this.update(documentId, userId, isEditing);
|
||||||
|
|
||||||
if (isEditing) {
|
timeout = setTimeout(() => {
|
||||||
timeout = setTimeout(() => {
|
this.leave(documentId, userId);
|
||||||
this.update(documentId, userId, false);
|
}, this.offlineTimeout);
|
||||||
}, USER_PRESENCE_INTERVAL * 2);
|
this.timeouts.set(id, timeout);
|
||||||
this.timeouts.set(id, timeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get(documentId: string): DocumentPresence | null | undefined {
|
@action
|
||||||
|
private update(documentId: string, userId: string, isEditing: boolean) {
|
||||||
|
const existing = this.data.get(documentId) || new Map();
|
||||||
|
existing.set(userId, {
|
||||||
|
isEditing,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
this.data.set(documentId, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(documentId: string): DocumentPresence | null | undefined {
|
||||||
return this.data.get(documentId);
|
return this.data.get(documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
clear() {
|
public clear() {
|
||||||
this.data.clear();
|
this.data.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,3 +202,7 @@ export type WebsocketEvent =
|
|||||||
| WebsocketCollectionUpdateIndexEvent
|
| WebsocketCollectionUpdateIndexEvent
|
||||||
| WebsocketEntityDeletedEvent
|
| WebsocketEntityDeletedEvent
|
||||||
| WebsocketEntitiesEvent;
|
| WebsocketEntitiesEvent;
|
||||||
|
|
||||||
|
export type AwarenessChangeEvent = {
|
||||||
|
states: { user: { id: string }; cursor: any; scrollY: number | undefined }[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const USER_PRESENCE_INTERVAL = 5000;
|
|
||||||
|
|
||||||
export const MAX_AVATAR_DISPLAY = 6;
|
export const MAX_AVATAR_DISPLAY = 6;
|
||||||
|
|
||||||
export const TeamPreferenceDefaults: TeamPreferences = {
|
export const TeamPreferenceDefaults: TeamPreferences = {
|
||||||
|
|||||||
Reference in New Issue
Block a user