Skip to content

Commit 4143523

Browse files
committed
Fix collaboration cursor disappearing on interaction
Resolved issue where collaboration cursors would disappear when clicking between browsers by fixing critical useEffect cleanup bug. Key fixes: - Prevent provider disconnect on dependency changes (only on session change) - Prevent extension recreation with synced event guard - Add provider reinitialization guard - Fix awareness state type (Array not Map) - Generate unique colors per user - Remove mouse position tracking
1 parent 5795d44 commit 4143523

File tree

1 file changed

+57
-44
lines changed

1 file changed

+57
-44
lines changed

src/components/ui/coaching-sessions/editor-cache-context.tsx

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from "@/types/presence";
2828
import { useCurrentRelationshipRole } from "@/lib/hooks/use-current-relationship-role";
2929
import { logoutCleanupRegistry } from "@/lib/hooks/logout-cleanup-registry";
30+
import { generateCollaborativeUserColor } from "@/lib/tiptap-utils";
3031

3132
/**
3233
* EditorCacheProvider manages TipTap collaboration lifecycle:
@@ -86,6 +87,9 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
8687
const yDocRef = useRef<Y.Doc | null>(null);
8788
const lastSessionIdRef = useRef<string | null>(null);
8889

90+
// Generate a consistent color for this user session
91+
const userColor = useMemo(() => generateCollaborativeUserColor(), []);
92+
8993
const [cache, setCache] = useState<EditorCacheState>({
9094
yDoc: null,
9195
collaborationProvider: null,
@@ -126,11 +130,35 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
126130
user: userSession.display_name,
127131
});
128132

133+
// Awareness initialization: establishes user presence in collaborative session
134+
// IMPORTANT: Set awareness BEFORE synced event so CollaborationCaret has user data
135+
const userPresence = createConnectedPresence({
136+
userId: userSession.id,
137+
name: userSession.display_name,
138+
relationshipRole: userRole,
139+
color: userColor,
140+
});
141+
142+
// IMPORTANT: Only set our custom "presence" field
143+
// Let CollaborationCaret manage the "user" field to avoid conflicts
144+
if (provider.awareness) {
145+
provider.awareness.setLocalStateField("presence", userPresence);
146+
}
147+
129148
// Provider event handlers: sync completion enables collaborative editing
149+
// IMPORTANT: Track if we've already created extensions to prevent recreation
150+
let extensionsCreated = false;
151+
130152
provider.on("synced", () => {
153+
if (extensionsCreated) {
154+
return;
155+
}
156+
157+
extensionsCreated = true;
158+
131159
const collaborativeExtensions = createExtensions(doc, provider, {
132160
name: userSession.display_name,
133-
color: "#ffcc00",
161+
color: userColor,
134162
});
135163

136164
setCache((prev) => ({
@@ -144,27 +172,12 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
144172
}));
145173
});
146174

147-
// Awareness initialization: establishes user presence in collaborative session
148-
const userPresence = createConnectedPresence({
149-
userId: userSession.id,
150-
name: userSession.display_name,
151-
relationshipRole: userRole,
152-
color: "#ffcc00",
153-
});
154-
155-
provider.setAwarenessField("user", {
156-
name: userSession.display_name,
157-
color: "#ffcc00",
158-
});
159-
160-
provider.setAwarenessField("presence", userPresence);
161-
162175
providerRef.current = provider;
163176

164177
// Awareness synchronization: tracks all connected users for presence indicators
165178
provider.on(
166179
"awarenessChange",
167-
({ states }: { states: Map<string, { presence?: AwarenessData }> }) => {
180+
({ states }: { states: Array<{ clientId: number; [key: string]: any }> }) => {
168181
const updatedUsers = new Map<string, UserPresence>();
169182
let currentUserPresence: UserPresence | null = null;
170183

@@ -197,44 +210,34 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
197210
userId: userSession.id,
198211
name: userSession.display_name,
199212
relationshipRole: userRole,
200-
color: "#ffcc00",
201-
});
202-
provider.setAwarenessField("presence", connectedPresence);
203-
provider.setAwarenessField("user", {
204-
name: userSession.display_name,
205-
color: "#ffcc00",
213+
color: userColor,
206214
});
215+
// Only update our custom "presence" field on reconnect
216+
// CollaborationCaret will handle the "user" field
217+
if (provider.awareness) {
218+
provider.awareness.setLocalStateField("presence", connectedPresence);
219+
}
207220
});
208221

209222
provider.on("disconnect", () => {
210223
const disconnectedPresence = createDisconnectedPresence(userPresence);
211-
provider.setAwarenessField("presence", disconnectedPresence);
212-
});
213-
214-
// Mouse tracking: enables collaborative cursor positioning
215-
const handleMouseMove = (event: MouseEvent) => {
216-
if (providerRef.current) {
217-
providerRef.current.setAwarenessField("user", {
218-
name: userSession.display_name,
219-
color: "#ffcc00",
220-
mouseX: event.clientX,
221-
mouseY: event.clientY,
222-
});
224+
if (provider.awareness) {
225+
provider.awareness.setLocalStateField("presence", disconnectedPresence);
223226
}
224-
};
225-
226-
document.addEventListener("mousemove", handleMouseMove);
227+
});
227228

228229
// Graceful disconnect on page unload
229230
const handleBeforeUnload = () => {
230231
const disconnectedPresence = createDisconnectedPresence(userPresence);
231-
provider.setAwarenessField("presence", disconnectedPresence);
232+
// Use low-level awareness API to match CollaborationCaret
233+
if (provider.awareness) {
234+
provider.awareness.setLocalStateField("presence", disconnectedPresence);
235+
}
232236
};
233237

234238
window.addEventListener("beforeunload", handleBeforeUnload);
235239

236240
return () => {
237-
document.removeEventListener("mousemove", handleMouseMove);
238241
window.removeEventListener("beforeunload", handleBeforeUnload);
239242
};
240243
} catch (error) {
@@ -274,6 +277,11 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
274277

275278
// Provider initialization or fallback to offline mode
276279
if (jwt && !tokenError && userSession) {
280+
// Skip if provider already exists and session hasn't changed
281+
if (providerRef.current && lastSessionIdRef.current === sessionId) {
282+
return;
283+
}
284+
277285
initializeProvider();
278286
} else {
279287
// Fallback to offline mode when token is unavailable
@@ -294,8 +302,11 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
294302
}
295303

296304
return () => {
297-
if (providerRef.current) {
305+
// IMPORTANT: Only disconnect on unmount or session change
306+
// Don't disconnect if dependencies change but provider should stay
307+
if (providerRef.current && lastSessionIdRef.current !== sessionId) {
298308
providerRef.current.disconnect();
309+
providerRef.current = null;
299310
}
300311
};
301312
}, [
@@ -316,9 +327,11 @@ export const EditorCacheProvider: React.FC<EditorCacheProviderProps> = ({
316327

317328
if (provider) {
318329
try {
319-
// Clear user presence to signal departure
320-
provider.setAwarenessField("presence", null);
321-
provider.setAwarenessField("user", null);
330+
// Clear our custom presence field on logout
331+
// CollaborationCaret will clean up the "user" field
332+
if (provider.awareness) {
333+
provider.awareness.setLocalStateField("presence", null);
334+
}
322335

323336
// Graceful provider shutdown
324337
provider.disconnect();

0 commit comments

Comments
 (0)