feat: Enhance real-time collaboration features with user awareness and document sharing

- Added user information (UserID, UserName, UserAvatar) to Client struct for presence tracking.
- Implemented failure handling in the broadcastMessage function to manage send failures and disconnect clients if necessary.
- Introduced document ownership and sharing capabilities:
  - Added OwnerID and Is_Public fields to Document model.
  - Created DocumentShare model for managing document sharing with permissions.
  - Implemented functions for creating, listing, and managing document shares in the Postgres store.
- Added user management functionality:
  - Created User model and associated functions for user management in the Postgres store.
  - Implemented session management with token hashing for security.
- Updated database schema with migrations for users, sessions, and document shares.
- Enhanced frontend Yjs integration with awareness event logging for user connections and disconnections.
This commit is contained in:
M1ngdaXie
2026-01-03 12:59:53 -08:00
parent 37d89b13b9
commit 7f5f32179b
21 changed files with 2064 additions and 232 deletions

View File

@@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import {
createYjsDocument,
destroyYjsDocument,
getRandomColor,
getRandomName,
type YjsProviders,
createYjsDocument,
destroyYjsDocument,
getRandomColor,
getRandomName,
type YjsProviders,
} from "../lib/yjs";
import { useAutoSave } from "./useAutoSave";
@@ -19,7 +19,6 @@ export const useYjsDocument = (documentId: string) => {
let mounted = true;
let currentProviders: YjsProviders | null = null;
// Create Yjs document and providers
const initializeDocument = async () => {
const yjsProviders = await createYjsDocument(documentId);
currentProviders = yjsProviders;
@@ -30,19 +29,75 @@ export const useYjsDocument = (documentId: string) => {
}
// Set user info for awareness
const userName = getRandomName();
const userColor = getRandomColor();
yjsProviders.awareness.setLocalStateField("user", {
name: getRandomName(),
color: getRandomColor(),
name: userName,
color: userColor,
});
// NEW: Add awareness event logging
const handleAwarenessChange = ({
added,
updated,
removed,
}: {
added: number[];
updated: number[];
removed: number[];
}) => {
const states = yjsProviders.awareness.getStates();
added.forEach((clientId) => {
const state = states.get(clientId);
const user = state?.user;
console.log(
`[Awareness] User connected: ${
user?.name || "Unknown"
} (ID: ${clientId})`,
{
color: user?.color,
clientId,
}
);
});
updated.forEach((clientId) => {
const state = states.get(clientId);
const user = state?.user;
console.log(
`[Awareness] User updated: ${
user?.name || "Unknown"
} (ID: ${clientId})`
);
});
removed.forEach((clientId) => {
console.log(`[Awareness] User disconnected (ID: ${clientId})`);
});
console.log(`[Awareness] Total connected users: ${states.size}`);
};
yjsProviders.awareness.on("change", handleAwarenessChange);
// Listen for sync status
yjsProviders.indexeddbProvider.on("synced", () => {
console.log("IndexedDB synced");
setSynced(true);
});
yjsProviders.websocketProvider.on("status", (event: { status: string }) => {
console.log("WebSocket status:", event.status);
yjsProviders.websocketProvider.on(
"status",
(event: { status: string }) => {
console.log("WebSocket status:", event.status);
}
);
// Log local user info
console.log(`[Awareness] Local user initialized: ${userName}`, {
color: userColor,
clientId: yjsProviders.awareness.clientID,
});
setProviders(yjsProviders);
@@ -54,10 +109,13 @@ export const useYjsDocument = (documentId: string) => {
return () => {
mounted = false;
if (currentProviders) {
console.log("[Awareness] Cleaning up local user");
currentProviders.awareness.setLocalState(null);
destroyYjsDocument(currentProviders);
}
};
}, [documentId]);
return { providers, synced };
};

View File

@@ -1,4 +1,5 @@
import { IndexeddbPersistence } from "y-indexeddb";
import { Awareness } from "y-protocols/awareness";
import { WebsocketProvider } from "y-websocket";
import * as Y from "yjs";
import { documentsApi } from "../api/document";
@@ -9,7 +10,7 @@ export interface YjsProviders {
ydoc: Y.Doc;
websocketProvider: WebsocketProvider;
indexeddbProvider: IndexeddbPersistence;
awareness: any;
awareness: Awareness;
}
export const createYjsDocument = async (documentId: string): Promise<YjsProviders> => {