Files
outline/app/stores/DocumentPresenceStore.ts
Tom Moor bf45e97641 chore: Enforce type import consistency (#10968)
* Update types

* fix circular dep

* type imports

* lint type imports and --fix
2025-12-19 23:07:02 -05:00

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;
}