[]
        
(Showing Draft Content)

Tutorial:Add Presence Functionality to a Real-Time Collaborative Text Editor

This tutorial builds on the "Real-Time Collaborative Text Editor tutorial" by adding Presence functionality, which is used to display online users and their cursor positions. Users can see the real-time status of others in the editor, such as usernames and selection positions, enhancing the collaborative experience.

Preview

When one user inputs characters in the editor, other users can see the cursor position, selected area, username and input content.


text_editor_presence

Prerequisites

Step 1: Install Packages

Run the following command in the project root directory to install the Presence-related npm packages:

npm install @mescius/js-collaboration-presence  @mescius/js-collaboration-presence-client
npm install quill-cursors

Step 2: Update the Server to Support Presence

Modify server.js to add the Presence feature.

import express from "express";
import { createServer } from "http";
import { Server } from "@mescius/js-collaboration";
import OT from "@mescius/js-collaboration-ot";
import { presenceFeature } from "@mescius/js-collaboration-presence";
import richText from "rich-text";

const app = express();
const httpServer = createServer(app);
const server = new Server({ httpServer });

// Register rich-text type
OT.TypesManager.register(richText.type);

// Initialize OT document service
server.useFeature(OT.documentFeature());

// Initialize presence service
server.useFeature(presenceFeature());

// Serve static files
app.use(express.static("public"));

// Start the server
httpServer.listen(8080, () => {
  console.log("Server running at http://localhost:8080");
});

Step 3: Update the Client to Support Presence

We will modify the client files to add the online user cursor display functionality.

Update public/client.js

import { Client } from "@mescius/js-collaboration-client";
import * as OT from "@mescius/js-collaboration-ot-client";
import { Presence } from "@mescius/js-collaboration-presence-client";
import richText from "rich-text";
import Quill from "quill";
import QuillCursors from "quill-cursors";

Quill.register('modules/cursors', QuillCursors);

// Register rich-text type
OT.TypesManager.register(richText.type);

// Connect to server and join room
const connection = new Client().connect("room-id");
const doc = new OT.SharedDoc(connection);
const quill = new Quill("#editor", { theme: "bubble", modules: { cursors: true } });
const cursors = quill.getModule('cursors');

// Initialize Presence
const presence = new Presence(connection);
const userId = `user-${Math.random().toString(36).substring(2, 9)}`; // Generate a random user ID
const DEFAULT_COLOR_SCHEME = ['#0000ff', '#008000', '#9900cc', '#800000', '#00cc33', '#cc6600', '#cc0099'];

// Subscribe to document
doc.subscribe().then(async () => {
    if (!doc.type) {
        try {
            await doc.create([{ insert: "Hi!" }], richText.type.uri, {});
        } catch (err) {
            console.error("Create Document Error:", err);
        }
    }
    quill.setContents(doc.data);
    quill.on("text-change", (delta, oldDelta, source) => {
        if (source !== "user") return;
        doc.submitOp(delta, { source: connection.id });
    });
    doc.on("op", (op, source) => {
        if (source === connection.id) return;
        quill.updateContents(op);

        // Update the content will cause the selection to change, but Quill will not trigger the selection-change event.
        presence.submitLocalStateField('selection', quill.getSelection());
    });
});

// Display remote presences
function updatePresenceList() {
    cursors.clearCursors();

    for (const id in presence.otherStates) {
        const data = presence.otherStates[id];
        cursors.createCursor(id, data.userId, data.color);
        cursors.moveCursor(id, data.selection);
    }
}


// Subscribe to presence updates
presence.subscribe().then(() => {
    updatePresenceList();

    let color = DEFAULT_COLOR_SCHEME[Object.keys(presence.otherStates).length % DEFAULT_COLOR_SCHEME.length];

    presence.submitLocalState({ userId, selection: quill.getSelection(), color });

    // Update local presence on selection change
    quill.on("selection-change", (range) => {
        presence.submitLocalStateField('selection', range);
    });

    presence.on("add", () => updatePresenceList());
    presence.on("update", () => updatePresenceList());
    presence.on("remove", () => updatePresenceList());
})

// Error handling
connection.on("error", (err) => console.error("Connection error:", err.message));
doc.on("error", (err) => console.error("Document error:", err.message));

Step 4: Run and Test

  1. Bundle the client code

    npm run build
  2. Start the server

    npm run start

    You should see the output: Server running at http://localhost:8080.

  3. Test the functionality

    1. Open a browser and visit http://localhost:8080.

    2. Open the same address in multiple windows, edit the content, and observe real-time synchronization.