88 * - Tracking whether current session is from a shared link
99 */
1010
11- import { useState , useEffect , useCallback } from 'react' ;
11+ import React , { useState , useEffect , useCallback } from 'react' ;
1212import { Annotation , type ImageAttachment } from '../types' ;
1313import {
1414 type SharePayload ,
@@ -68,6 +68,12 @@ interface UseSharingResult {
6868
6969 /** Import annotations from a teammate's share URL */
7070 importFromShareUrl : ( url : string ) => Promise < ImportResult > ;
71+
72+ /** Error message when a shared URL failed to load on mount */
73+ shareLoadError : string ;
74+
75+ /** Clear the share load error */
76+ clearShareLoadError : ( ) => void ;
7177}
7278
7379
@@ -76,8 +82,8 @@ export function useSharing(
7682 annotations : Annotation [ ] ,
7783 globalAttachments : ImageAttachment [ ] ,
7884 setMarkdown : ( m : string ) => void ,
79- setAnnotations : ( a : Annotation [ ] ) => void ,
80- setGlobalAttachments : ( g : ImageAttachment [ ] ) => void ,
85+ setAnnotations : React . Dispatch < React . SetStateAction < Annotation [ ] > > ,
86+ setGlobalAttachments : React . Dispatch < React . SetStateAction < ImageAttachment [ ] > > ,
8187 onSharedLoad ?: ( ) => void ,
8288 shareBaseUrl ?: string ,
8389 pasteApiUrl ?: string
@@ -91,12 +97,15 @@ export function useSharing(
9197 const [ shortUrlError , setShortUrlError ] = useState ( '' ) ;
9298 const [ pendingSharedAnnotations , setPendingSharedAnnotations ] = useState < Annotation [ ] | null > ( null ) ;
9399 const [ sharedGlobalAttachments , setSharedGlobalAttachments ] = useState < ImageAttachment [ ] | null > ( null ) ;
100+ const [ shareLoadError , setShareLoadError ] = useState ( '' ) ;
94101
95102 const clearPendingSharedAnnotations = useCallback ( ( ) => {
96103 setPendingSharedAnnotations ( null ) ;
97104 setSharedGlobalAttachments ( null ) ;
98105 } , [ ] ) ;
99106
107+ const clearShareLoadError = useCallback ( ( ) => setShareLoadError ( '' ) , [ ] ) ;
108+
100109 // Load shared state from URL hash (or paste-service short URL)
101110 const loadFromHash = useCallback ( async ( ) => {
102111 try {
@@ -135,9 +144,11 @@ export function useSharing(
135144 }
136145 // Paste fetch failed — short URL path can't fall back to hash parsing
137146 // (the hash contains #key=, not plan data).
147+ setShareLoadError ( 'Failed to load shared plan — the link may be expired or incomplete.' ) ;
138148 return false ;
139149 }
140150
151+ const hash = window . location . hash . slice ( 1 ) ;
141152 const payload = await parseShareHash ( ) ;
142153
143154 if ( payload ) {
@@ -173,9 +184,15 @@ export function useSharing(
173184
174185 return true ;
175186 }
187+
188+ // Hash was present but failed to decompress (likely truncated by browser)
189+ if ( hash ) {
190+ setShareLoadError ( 'Failed to load shared plan — the URL may have been truncated by your browser.' ) ;
191+ }
176192 return false ;
177193 } catch ( e ) {
178194 console . error ( 'Failed to load from share hash:' , e ) ;
195+ setShareLoadError ( 'Failed to load shared plan — an unexpected error occurred.' ) ;
179196 return false ;
180197 }
181198 } , [ setMarkdown , setAnnotations , setGlobalAttachments , onSharedLoad , pasteApiUrl ] ) ;
@@ -297,35 +314,46 @@ export function useSharing(
297314 return { success : true , count : 0 , planTitle, error : 'No annotations found in share link' } ;
298315 }
299316
300- // Deduplicate: skip annotations that already exist (by originalText + type + text)
301- const newAnnotations = importedAnnotations . filter ( imp =>
317+ // Estimate count from current closure (may be slightly stale, but
318+ // the actual merge below uses the latest state via functional updater)
319+ const estimatedNew = importedAnnotations . filter ( imp =>
302320 ! annotations . some ( existing =>
303321 existing . originalText === imp . originalText &&
304322 existing . type === imp . type &&
305323 existing . text === imp . text
306324 )
307325 ) ;
308326
309- if ( newAnnotations . length > 0 ) {
310- // Merge: append new annotations to existing ones
311- setAnnotations ( [ ...annotations , ...newAnnotations ] ) ;
312-
313- // Set as pending so they get applied to DOM highlights
314- setPendingSharedAnnotations ( newAnnotations ) ;
327+ if ( estimatedNew . length > 0 ) {
328+ // Merge using functional updater to avoid stale closure
329+ setAnnotations ( prev => {
330+ const newAnnotations = importedAnnotations . filter ( imp =>
331+ ! prev . some ( existing =>
332+ existing . originalText === imp . originalText &&
333+ existing . type === imp . type &&
334+ existing . text === imp . text
335+ )
336+ ) ;
337+ if ( newAnnotations . length === 0 ) return prev ;
338+ const merged = [ ...prev , ...newAnnotations ] ;
339+ // Set ALL annotations as pending so DOM highlights include originals
340+ setPendingSharedAnnotations ( merged ) ;
341+ return merged ;
342+ } ) ;
315343
316344 // Handle global attachments (deduplicate by path)
317345 if ( payload . g ?. length ) {
318346 const parsed = parseShareableImages ( payload . g ) ?? [ ] ;
319- const existingPaths = new Set ( globalAttachments . map ( g => g . path ) ) ;
320- const newAttachments = parsed . filter ( p => ! existingPaths . has ( p . path ) ) ;
321- if ( newAttachments . length > 0 ) {
322- setGlobalAttachments ( [ ...globalAttachments , ...newAttachments ] ) ;
323- }
347+ setGlobalAttachments ( prev => {
348+ const existingPaths = new Set ( prev . map ( g => g . path ) ) ;
349+ const newAttachments = parsed . filter ( p => ! existingPaths . has ( p . path ) ) ;
350+ return newAttachments . length > 0 ? [ ...prev , ...newAttachments ] : prev ;
351+ } ) ;
324352 setSharedGlobalAttachments ( parsed ) ;
325353 }
326354 }
327355
328- return { success : true , count : newAnnotations . length , planTitle } ;
356+ return { success : true , count : estimatedNew . length , planTitle } ;
329357 } catch ( e ) {
330358 const errorMessage = e instanceof Error ? e . message : 'Failed to decompress share URL' ;
331359 return { success : false , count : 0 , planTitle : '' , error : errorMessage } ;
@@ -346,5 +374,7 @@ export function useSharing(
346374 refreshShareUrl,
347375 generateShortUrl,
348376 importFromShareUrl,
377+ shareLoadError,
378+ clearShareLoadError,
349379 } ;
350380}
0 commit comments