UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

200 lines (186 loc) 6.77 kB
import { Store } from "@tanstack/store"; import { createStore } from "../editor/BlockNoteExtension.js"; /** * A collaborator of the document. */ export type User = { /** * The {@link User}'s unique identifier */ id: string; /** * The {@link User}'s name/label */ username: string; /** * The {@link User}'s profile image */ avatarUrl: string; /** * The color used to represent the user (e.g. for collaboration cursors). */ color?: string; /** * A lighter variant of {@link color}. */ colorLight?: string; }; /** * A store that retrieves and caches information about users, generic over the * resolved user type `U`. * * Created via {@link createUserStore}. Features that need to resolve user ids to * user information (comments, suggestions, versions) build one internally and * expose it on their extension instance so their UI can read from it. */ export type UserStore<U extends User = User> = { /** * A store mapping user ids to the resolved {@link User} information. */ store: Store<Map<User["id"], U>>; /** * Load information about users based on an array of user ids. * * Users that are already cached or currently being loaded are skipped, so * it is safe to call this often (e.g. on every render). */ loadUsers: (userIds: User["id"][]) => Promise<void>; /** * Re-fetch information about users, ignoring the cache. Users that are * currently being loaded are still skipped to avoid duplicate requests. */ refetchUsers: (userIds: User["id"][]) => Promise<void>; /** * Retrieve information about a user based on their id, if cached. * * The user has to be loaded via `loadUsers` first. */ getUser: (userId: User["id"]) => U | undefined; /** * Manually set information about a user. This is useful if you have a * resolver that returns partial information (e.g. just the username) and you * want to fill in the rest later (e.g. avatarUrl). */ setUser: (user: U | U[]) => void; }; export type UserStoreResolver<U extends User = User> = ( /** * The user ids to resolve. The resolver should return information for all of * these users, or an empty array if none could be resolved. */ userIds: User["id"][], /** * The {@link UserStore} that is calling this resolver. This allows you to return a user synchronously, and update the store later if you need to fetch additional information asynchronously. */ store: UserStore<any>, ) => Promise<U[]>; /** * A resolver callback or an already-built {@link UserStore} — the shape that * user-facing options (comments, collaboration) accept so callers can either let * the extension build a store or pass a shared one. */ export type UserStoreOrResolver<U extends User = User> = | ((userIds: User["id"][], store: UserStore<any>) => Promise<U[]>) | UserStore<any>; /** * Creates a {@link UserStore} that retrieves and caches information about users. * * It does this by calling `resolveUsers` for users that are not yet cached, and * stores the results in a BlockNote store so they can be subscribed to (e.g. via * `useStore` in React). * * `resolveUsers` is called with the ids of users that are not yet cached, and * should return the information for those users. The type of the returned users * is inferred and flows through to {@link UserStore.getUser} and the store, so * you can return a type with additional properties and have them be reported * back. * * See [Comments](https://www.blocknotejs.org/docs/features/collaboration/comments) for more info. */ export function createUserStore<U extends User = User>( resolveUsers: (userIds: User["id"][], store: UserStore<any>) => Promise<U[]>, ): UserStore<U> { if (!resolveUsers) { throw new Error("resolveUsers is required to create a user store"); } const store = createStore(new Map<User["id"], U>()); // Tracks users that are currently being fetched, to avoid duplicate // in-flight requests. This is intentionally kept out of the store as it is // not state that consumers need to subscribe to. const loadingUsers = new Set<User["id"]>(); const userStore: UserStore<U> = { store, async loadUsers(userIds) { const missingUsers = userIds.filter( (id) => !store.state.has(id) && !loadingUsers.has(id), ); await fetchUsers(missingUsers); }, async refetchUsers(userIds) { const usersToFetch = userIds.filter((id) => !loadingUsers.has(id)); await fetchUsers(usersToFetch); }, getUser(userId) { return store.state.get(userId); }, setUser(users) { const usersArray = Array.isArray(users) ? users : [users]; store.setState((prevState) => { const nextState = new Map(prevState); for (const user of usersArray) { nextState.set(user.id, user); } return nextState; }); }, }; async function fetchUsers(userIds: User["id"][]) { if (userIds.length === 0) { return; } for (const id of userIds) { loadingUsers.add(id); } try { const users = await resolveUsers(userIds, userStore); // Only update the store if any users were actually resolved. Emitting // an update when nothing changed (e.g. when the resolver can't find a // user) would needlessly notify subscribers and, combined with a // subscriber that re-triggers loading, could cause an infinite loop. // See https://github.com/TypeCellOS/BlockNote/issues/1548 if (users.length > 0) { store.setState((prevState) => { const nextState = new Map(prevState); for (const user of users) { nextState.set(user.id, user); } return nextState; }); } } finally { for (const id of userIds) { // Remove the users from the loading set. On a next call to `loadUsers` // we will either return the cached user, or retry loading the user if // the request failed. loadingUsers.delete(id); } } } return userStore; } /** * Normalize a {@link UserStoreOrResolver} to a {@link UserStore}: * - an existing store is returned as-is (so a single de-duped cache can be * shared across extensions), * - a resolver function is wrapped with {@link createUserStore}, * - `undefined` yields an empty store that resolves nothing (consumers then fall * back to showing raw user ids). */ export function normalizeToUserStore<U extends User = User>( resolveUsersOrStore?: UserStoreOrResolver<U>, ): UserStore<U> { if (typeof resolveUsersOrStore === "function") { return createUserStore(resolveUsersOrStore); } return resolveUsersOrStore ?? createUserStore<U>(async () => []); }