mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
bf45e97641
* Update types * fix circular dep * type imports * lint type imports and --fix
133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import { observable, action } from "mobx";
|
|
import type { AwarenessChangeEvent } from "~/types";
|
|
import type RootStore from "./RootStore";
|
|
|
|
type DocumentPresence = Map<
|
|
string,
|
|
{
|
|
isEditing: boolean;
|
|
userId: string;
|
|
}
|
|
>;
|
|
|
|
export default class PresenceStore {
|
|
@observable
|
|
data: Map<string, DocumentPresence> = new Map();
|
|
|
|
constructor(rootStore: RootStore) {
|
|
this.rootStore = rootStore;
|
|
}
|
|
|
|
/**
|
|
* Removes a user from the presence store
|
|
*
|
|
* @param documentId ID of the document to remove the user from
|
|
* @param userId ID of the user to remove
|
|
*/
|
|
@action
|
|
public leave(documentId: string, userId: string) {
|
|
const existing = this.data.get(documentId);
|
|
|
|
if (existing) {
|
|
existing.delete(userId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the presence store based on an awareness event from YJS
|
|
*
|
|
* @param documentId ID of the document the event is for
|
|
* @param clientId ID of the client the event is for
|
|
* @param event The awareness event
|
|
*/
|
|
public updateFromAwarenessChangeEvent(
|
|
documentId: string,
|
|
clientId: number,
|
|
event: AwarenessChangeEvent
|
|
) {
|
|
const presence = this.data.get(documentId);
|
|
let existingUserIds = (presence ? Array.from(presence.values()) : []).map(
|
|
(p) => p.userId
|
|
);
|
|
|
|
event.states.forEach((state) => {
|
|
const { user, cursor } = state;
|
|
|
|
// To avoid loops we only want to update the presence for the current user
|
|
// if it is also the current client.
|
|
const isCurrentUser = this.rootStore.auth.currentUserId === user?.id;
|
|
const isCurrentClient = clientId === state.clientId;
|
|
|
|
if (user && (!isCurrentUser || !isCurrentClient)) {
|
|
this.update(documentId, user.id, !!cursor);
|
|
existingUserIds = existingUserIds.filter((id) => id !== user.id);
|
|
}
|
|
});
|
|
|
|
existingUserIds.forEach((userId) => {
|
|
this.leave(documentId, userId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates the presence store to indicate that a user is present in a document
|
|
* and then removes the user after a timeout of inactivity.
|
|
*
|
|
* @param documentId ID of the document to update
|
|
* @param userId ID of the user to update
|
|
* @param isEditing Whether the user is "editing" the document
|
|
*/
|
|
public touch(documentId: string, userId: string, isEditing: boolean) {
|
|
const id = `${documentId}-${userId}`;
|
|
let timeout = this.timeouts.get(id);
|
|
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
this.timeouts.delete(id);
|
|
}
|
|
|
|
this.update(documentId, userId, isEditing);
|
|
|
|
timeout = setTimeout(() => {
|
|
this.leave(documentId, userId);
|
|
}, this.offlineTimeout);
|
|
this.timeouts.set(id, timeout);
|
|
}
|
|
|
|
/**
|
|
* Updates the presence store to indicate that a user is present in a document.
|
|
*
|
|
* @param documentId ID of the document to update
|
|
* @param userId ID of the user to update
|
|
* @param isEditing Whether the user is "editing" the document
|
|
*/
|
|
@action
|
|
private update(documentId: string, userId: string, isEditing: boolean) {
|
|
const presence = this.data.get(documentId) || new Map();
|
|
const existing = presence.get(userId);
|
|
|
|
if (!existing || existing.isEditing !== isEditing) {
|
|
presence.set(userId, {
|
|
isEditing,
|
|
userId,
|
|
});
|
|
this.data.set(documentId, presence);
|
|
}
|
|
}
|
|
|
|
public get(documentId: string): DocumentPresence | null | undefined {
|
|
return this.data.get(documentId);
|
|
}
|
|
|
|
@action
|
|
public clear() {
|
|
this.data.clear();
|
|
}
|
|
|
|
private timeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
|
|
private offlineTimeout = 30000;
|
|
|
|
private rootStore: RootStore;
|
|
}
|