@@ -27,6 +27,7 @@ import {
2727} from "@/types/presence" ;
2828import { useCurrentRelationshipRole } from "@/lib/hooks/use-current-relationship-role" ;
2929import { 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