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

## Content

This tutorial builds on the "[Real-Time Collaborative Text Editor tutorial](/spreadjs/docs/v18/spreadjs-collaboration-server/collaboration-framework/js-collaboration-ot/tutorial-real-time-collaborative-text-editor)" 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](https://cdn.mescius.io/document-site-files/images/7719ad0a-f083-46d7-aff6-f63e2e187c15/text_editor_presence.6e26da.gif)

## Prerequisites

* Complete [Real-Time Collaborative Text Editor tutorial](/spreadjs/docs/v18/spreadjs-collaboration-server/collaboration-framework/js-collaboration-ot/tutorial-real-time-collaborative-text-editor)
* Familiarity with JavaScript and HTML/CSS basics

## Step 1: Install Packages

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

```bash
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.

```javascript
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`

```javascript
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

    ```bash
    npm run build
    ```
2. Start the server

    ```bash
    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.